feat: password age policy (#8132)

# Which Problems Are Solved

Some organizations / customers have the requirement, that there users
regularly need to change their password.
ZITADEL already had the possibility to manage a `password age policy` (
thought the API) with the maximum amount of days a password should be
valid, resp. days after with the user should be warned of the upcoming
expiration.
The policy could not be managed though the Console UI and was not
checked in the Login UI.

# How the Problems Are Solved

- The policy can be managed in the Console UI's settings sections on an
instance and organization level.
- During an authentication in the Login UI, if a policy is set with an
expiry (>0) and the user's last password change exceeds the amount of
days set, the user will be prompted to change their password.
- The prompt message of the Login UI can be customized in the Custom
Login Texts though the Console and API on the instance and each
organization.
- The information when the user last changed their password is returned
in the Auth, Management and User V2 API.
- The policy can be retrieved in the settings service as `password
expiry settings`.

# Additional Changes

None.

# Additional Context

- closes #8081

---------

Co-authored-by: Tim Möhlmann <tim+github@zitadel.com>
This commit is contained in:
Livio Spring
2024-06-18 13:27:44 +02:00
committed by GitHub
parent 65f787cc02
commit fb8cd18f93
93 changed files with 1250 additions and 487 deletions

View File

@@ -2655,7 +2655,7 @@ service AdminService {
tags: "Settings";
tags: "Password Settings";
summary: "Get Password Age Settings";
description: "Not implemented"
description: "Returns the password age settings configured on the instance. It affects all organizations, that do not have a custom setting configured. The settings specify the expiry of password, after which a user is forced to change it on the next login.";
responses: {
key: "200";
value: {
@@ -2679,7 +2679,7 @@ service AdminService {
tags: "Settings";
tags: "Password Settings";
summary: "Update Password Age Settings";
description: "Not implemented"
description: "Updates the default password complexity settings configured on the instance. It affects all organizations, that do not have a custom setting configured. The settings specify the expiry of password, after which a user is forced to change it on the next login.";
responses: {
key: "200";
value: {
@@ -6736,18 +6736,10 @@ message GetPasswordAgePolicyResponse {
}
message UpdatePasswordAgePolicyRequest {
uint32 max_age_days = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "Maximum days since last password change"
example: "\"365\""
}
];
uint32 expire_warn_days = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "Days before the password expiry the user gets notified to change the password"
example: "\"10\""
}
];
// Amount of days after which a password will expire. The user will be forced to change the password on the following authentication.
uint32 max_age_days = 1;
// Amount of days after which the user should be notified of the upcoming expiry. ZITADEL will not notify the user.
uint32 expire_warn_days = 2;
}
message UpdatePasswordAgePolicyResponse {

View File

@@ -4742,7 +4742,6 @@ service ManagementService {
};
}
// The password age policy is not used at the moment
rpc GetPasswordAgePolicy(GetPasswordAgePolicyRequest) returns (GetPasswordAgePolicyResponse) {
option (google.api.http) = {
get: "/policies/password/age"
@@ -4756,7 +4755,7 @@ service ManagementService {
tags: "Settings";
tags: "Password Settings";
summary: "Get Password Age Settings";
description: "Not implemented";
description: "Returns the password age settings configured on the organization. The settings specify the expiry of password, after which a user is forced to change it on the next login.";
parameters: {
headers: {
name: "x-zitadel-orgid";
@@ -4768,7 +4767,6 @@ service ManagementService {
};
}
// The password age policy is not used at the moment
rpc GetDefaultPasswordAgePolicy(GetDefaultPasswordAgePolicyRequest) returns (GetDefaultPasswordAgePolicyResponse) {
option (google.api.http) = {
get: "/policies/default/password/age"
@@ -4782,7 +4780,7 @@ service ManagementService {
tags: "Settings";
tags: "Password Settings";
summary: "Get Default Password Age Settings";
description: "Not implemented";
description: "Returns the default password age settings configured on the instance. The settings specify the expiry of password, after which a user is forced to change it on the next login.";
parameters: {
headers: {
name: "x-zitadel-orgid";
@@ -4794,7 +4792,6 @@ service ManagementService {
};
}
// The password age policy is not used at the moment
rpc AddCustomPasswordAgePolicy(AddCustomPasswordAgePolicyRequest) returns (AddCustomPasswordAgePolicyResponse) {
option (google.api.http) = {
post: "/policies/password/age"
@@ -4809,7 +4806,7 @@ service ManagementService {
tags: "Settings";
tags: "Password Settings";
summary: "Add Password Age Settings";
description: "Not implemented";
description: "Create new password age settings for the organization. This will overwrite the settings of the instance for this organization. The settings specify the expiry of password, after which a user is forced to change it on the next login.";
parameters: {
headers: {
name: "x-zitadel-orgid";
@@ -4821,7 +4818,6 @@ service ManagementService {
};
}
// The password age policy is not used at the moment
rpc UpdateCustomPasswordAgePolicy(UpdateCustomPasswordAgePolicyRequest) returns (UpdateCustomPasswordAgePolicyResponse) {
option (google.api.http) = {
put: "/policies/password/age"
@@ -4836,7 +4832,7 @@ service ManagementService {
tags: "Settings";
tags: "Password Settings";
summary: "Update Password Age Settings";
description: "Not implemented";
description: "Update the password age settings of the organization. The settings specify the expiry of password, after which a user is forced to change it on the next login.";
parameters: {
headers: {
name: "x-zitadel-orgid";
@@ -4848,7 +4844,6 @@ service ManagementService {
};
}
// The password age policy is not used at the moment
rpc ResetPasswordAgePolicyToDefault(ResetPasswordAgePolicyToDefaultRequest) returns (ResetPasswordAgePolicyToDefaultResponse) {
option (google.api.http) = {
delete: "/policies/password/age"
@@ -4862,7 +4857,7 @@ service ManagementService {
tags: "Settings";
tags: "Password Settings";
summary: "Reset Password Age Settings to Default";
description: "Not implemented";
description: "Remove the password age settings of the organization and therefore use the default settings on the instance.. The settings specify the expiry of password, after which a user is forced to change it on the next login.";
parameters: {
headers: {
name: "x-zitadel-orgid";
@@ -10604,7 +10599,9 @@ message GetDefaultPasswordAgePolicyResponse {
}
message AddCustomPasswordAgePolicyRequest {
// Amount of days after which a password will expire. The user will be forced to change the password on the following authentication.
uint32 max_age_days = 1;
// Amount of days after which the user should be notified of the upcoming expiry. ZITADEL will not notify the user.
uint32 expire_warn_days = 2;
}
@@ -10613,7 +10610,9 @@ message AddCustomPasswordAgePolicyResponse {
}
message UpdateCustomPasswordAgePolicyRequest {
// Amount of days after which a password will expire. The user will be forced to change the password on the following authentication.
uint32 max_age_days = 1;
// Amount of days after which the user should be notified of the upcoming expiry. ZITADEL will not notify the user.
uint32 expire_warn_days = 2;
}

View File

@@ -310,23 +310,20 @@ message PasswordComplexityPolicy {
message PasswordAgePolicy {
zitadel.v1.ObjectDetails details = 1;
// Amount of days after which a password will expire. The user will be forced to change the password on the following authentication.
uint64 max_age_days = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "Maximum days since last password change"
example: "\"365\""
}
];
// Amount of days after which the user should be notified of the upcoming expiry. ZITADEL will not notify the user.
uint64 expire_warn_days = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "Days before the password expiry the user gets notified to change the password"
example: "\"10\""
}
];
bool is_default = 4 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "defines if the organization's admin changed the policy"
}
];
// If true, the returned values represent the instance settings, e.g. by an organization without custom settings.
bool is_default = 4;
}
message LockoutPolicy {

View File

@@ -41,3 +41,20 @@ message PasswordComplexitySettings {
}
];
}
message PasswordExpirySettings {
// Amount of days after which a password will expire. The user will be forced to change the password on the following authentication.
uint64 max_age_days = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"365\""
}
];
// Amount of days after which the user should be notified of the upcoming expiry. ZITADEL will not notify the user.
uint64 expire_warn_days = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"10\""
}
];
// resource_owner_type returns if the settings is managed on the organization or on the instance
ResourceOwnerType resource_owner_type = 3;
}

View File

@@ -204,6 +204,30 @@ service SettingsService {
};
}
// Get the password expiry settings
rpc GetPasswordExpirySettings (GetPasswordExpirySettingsRequest) returns (GetPasswordExpirySettingsResponse) {
option (google.api.http) = {
get: "/v2beta/settings/password/expiry"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "policy.read"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Get the password expiry settings";
description: "Return the password expiry settings for the requested context"
responses: {
key: "200"
value: {
description: "OK";
}
};
};
}
// Get the current active branding settings
rpc GetBrandingSettings (GetBrandingSettingsRequest) returns (GetBrandingSettingsResponse) {
option (google.api.http) = {
@@ -358,6 +382,15 @@ message GetPasswordComplexitySettingsResponse {
zitadel.settings.v2beta.PasswordComplexitySettings settings = 2;
}
message GetPasswordExpirySettingsRequest {
zitadel.object.v2beta.RequestContext ctx = 1;
}
message GetPasswordExpirySettingsResponse {
zitadel.object.v2beta.Details details = 1;
zitadel.settings.v2beta.PasswordExpirySettings settings = 2;
}
message GetBrandingSettingsRequest {
zitadel.object.v2beta.RequestContext ctx = 1;
}

View File

@@ -272,6 +272,7 @@ message PasswordChangeScreenText {
string new_password_confirm_label = 5 [(validate.rules).string = {max_len: 200}];
string cancel_button_text = 6 [(validate.rules).string = {max_len: 100}];
string next_button_text = 7 [(validate.rules).string = {max_len: 100}];
string expired_description = 8 [(validate.rules).string = {max_len: 500}];
}
message PasswordChangeDoneScreenText {

View File

@@ -65,6 +65,8 @@ message Human {
Profile profile = 1;
Email email = 2;
Phone phone = 3;
// The time the user last changed their password.
google.protobuf.Timestamp password_changed = 4;
}
message Machine {

View File

@@ -5,6 +5,7 @@ package zitadel.user.v2beta;
option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v2beta;user";
import "google/api/field_behavior.proto";
import "google/protobuf/timestamp.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
import "zitadel/object/v2beta/object.proto";
@@ -172,6 +173,8 @@ message HumanUser {
HumanPhone phone = 8;
// User is required to change the used password on the next login.
bool password_change_required = 9;
// The time the user last changed their password.
google.protobuf.Timestamp password_changed = 10;
}
message User {