Change expired password during login

This solution document will give you an idea on how to configure the login flow to detect expired password and prompt the user for a password change.

This document will use the username, password and OTP scenario as an example, but can be replaced by any other authentication flow.

Prereqs

This will work with federated setups, but part of the authentication has to be made with "internal authenticators". You will need to find the proper internal authenticator that matches the SAML authenticator to be replaced.

Please make sure to replace all references to match your environment.

The templates files used in this document can be found on https://files.phenixid.se in the folder PhenixID MFA PAS/Integrations/Change expired password during login.
According to example below, templates should be placed in the authentication module (mods/com.phenixidentity~auth-http~<version>/templates).

Authenticators

SAMLDataSave - start

This will be the point of start for this flow.

<p>{
		"name": "SAMLDataSave",
		"id": "start",
		"alias": "start",
		"configuration": {
			"nextAuthenticator": "internal-UidPWDOTP",
			"idpID": "idp"
		}
	}</p>
Click to copy

PostUidPasswordAndOTP - internal-UidPWDOTP

This is the authenticator that performs the username, password and OTP validation.

This is also the step where the password expiration is detected and stored in the session.

<p>{
		"id": "internal-UidPWDOTP",
		"alias": "internal-UidPWDOTP",
		"name": "PostUidPasswordAndOTP",
		"configuration": {
			"userValidationPipeID": "UserLookupAndAuthWithLDAP",
			"otpValidationPipeID": "ValidateSentOtp",
			"successURL": "/saml/authenticate/dispatch/"
		}
	}</p>
Click to copy

Dispatch - dispatch

The user will be dispatched to the password change if the password is expired.

The user will be dispatched to the "sso authenticator" if the password not is expired.

<p>{
		"name": "Dispatch",
		"id": "dispatch",
		"alias": "dispatch",
		"configuration": {
			"idpID": "idp",
			"mapping": [
				{
					"authenticator": "changepwd",
					"expression": "session.properties().getValueOrDefault('changepwd','').equals('true')"
				},
				{
					"authenticator": "sso",
					"expression": "!session.properties().getValueOrDefault('changepwd','').equals('true')"
				}
			]
		}
	}</p>
Click to copy

Registration - changepwd

This authenticator will perform the password change.

<p>{
		"alias": "changepwd",
		"id": "changepwd",
		"name": "Registration",
		"configuration": {
			"stages": [
				{
					"pipeid": "ChangePWDPipe",
					"template": "pwchange",
					"sessionValues": [
						"username",
						"customError",
						"dummy"
					],
					"translation": []
				},
				{
					"pipeid": "",
					"template": "pwchange-redir",
					"sessionValues": [
						"redirect_to_saml_uri"
					],
					"translation": []
				}
			]
		}
	}</p>
Click to copy

PostUidAndPasswordSAML - sso

This authenticator will create the SAML assertion and login the user to the SP.

<p>{
		"name": "PostUidAndPasswordSAML",
		"id": "sso",
		"alias": "sso",
		"configuration": {
			"pipeID": "PostUid_and_Password_SAML_SSO",
			"idpID": "idp"
		}
	}</p>
Click to copy

Pipes

UserLookupAndAuthWithLDAP

This pipe will perform username and password validation, generate and send OTP.

The session will be updated if the password is expired.

<p>{
		"id": "UserLookupAndAuthWithLDAP",
		"valves": [
			{
				"name": "LDAPSearchValve",
				"config": {
					"connection_ref": "8d7362da-15c4-4178-888d-086b337e6f33",
					"base_dn": "dc=company,dc=local",
					"scope": "SUB",
					"size_limit": "0",
					"filter_template": "(&amp;(objectclass=*)(samaccountname={{request.username}}))",
					"attributes": "commonName,uid,mail,mobile"
				}
			},
			{
				"name": "LDAPBindValve",
				"config": {
					"connection_ref": "8d7362da-15c4-4178-888d-086b337e6f33",
					"password_param_name": "password",
					"allowed_error_codes": "532,773"
				}
			},
			{
				"name": "SessionLoadValve",
				"config": {
					"id": "{{request.session_id}}",
					"exec_if_expr": "flow.firstItem().containsProperty('LDAP_BIND_ERROR_CODE')"
				}
			},
			{
				"name": "SessionPropertyReplaceValve",
				"config": {
					"name": "changepwd",
					"value": "true",
					"exec_if_expr": "flow.firstItem().containsProperty('LDAP_BIND_ERROR_CODE')"
				}
			},
			{
				"name": "SessionPropertyReplaceValve",
				"config": {
					"name": "oldpassword",
					"value": "{{request.password}}",
					"exec_if_expr": "flow.firstItem().containsProperty('LDAP_BIND_ERROR_CODE')"
				}
			},
			{
				"name": "SessionPropertyReplaceValve",
				"config": {
					"name": "username",
					"value": "{{request.username}}",
					"exec_if_expr": "flow.firstItem().containsProperty('LDAP_BIND_ERROR_CODE')"
				}
			},
			{
				"name": "SessionPersistValve",
				"config": {
					"exec_if_expr": "flow.firstItem().containsProperty('LDAP_BIND_ERROR_CODE')"
				}
			},
			{
				"name": "OTPGeneratorValve",
				"config": {
					"length": "6",
					"name": "generated_otp"
				}
			},
			{
				"name": "OTPBySMSValve",
				"enabled": "true",
				"config": {
					"userid_param_name": "username",
					"gw_username": "GatewayAccount",
					"gw_password": "GatewayPassword"
				}
			}
		]
	}</p>
Click to copy

ValidateSentOtp

This pipe will validate the OTP.

<p>{
		"id": "ValidateSentOtp",
		"valves": [
			{
				"name": "SessionLoadValve",
				"config": {
					"id": "{{request.session_id}}"
				}
			},
			{
				"name": "OTPValidationValve",
				"enabled": "true",
				"config": {
					"provided_otp_param_name": "{{request.otp}}",
					"generated_otp_param_name": "generated_otp"
				}
			}
		]
	}</p>
Click to copy

ChangePWDPipe

This pipe will do the password change.

<p>{
		"id": "ChangePWDPipe",
		"valves": [
			{
				"name": "SessionLoadValve",
				"config": {
					"id": "{{request.session_id}}"
				}
			},
			{
				"name": "LDAPSearchValve",
				"config": {
					"connection_ref": "8d7362da-15c4-4178-888d-086b337e6f33",
					"base_dn": "dc=company,dc=local",
					"scope": "SUB",
					"size_limit": "0",
					"filter_template": "(&amp;(objectclass=*)(samaccountname={{session.username}}))",
					"attributes": "commonName,uid,mail,mobile,pwdLastSet"
				}
			},
			{
				"name": "ADPasswordChangeValve",
				"config": {
					"connection_ref": "8d7362da-15c4-4178-888d-086b337e6f33",
					"value": "{{request.pwdnew}}",
					"unlock": "false",
					"current_password_param_name": "{{session.oldpassword}}"
				}
			},
			{
				"name": "SessionPropertyRemoveValve",
				"config": {
					"name": "changepwd"
				}
			},
			{
				"name": "SessionPropertyReplaceValve",
				"config": {
					"name": "redirect_to_saml_uri",
					"value": "/saml/authenticate/sso"
				}
			},
			{
				"name": "SessionPersistValve",
				"config": {}
			}
		]
	}</p>
Click to copy

PostUid_and_Password_SAML_SSO

This pipe will issue the SAML assertion and redirect the end user to the SP.

<p>{
		"id": "PostUid_and_Password_SAML_SSO",
		"valves": [
			{
				"name": "FlowFailValve",
				"config": {
					"message": "No current session",
					"skip_if_expr": "request.authenticatedrequest=='true'"
				}
			},
			{
				"name": "SessionLoadValve",
				"config": {
					"id": "{{request.session_id}}"
				}
			},
			{
				"name": "LDAPSearchValve",
				"config": {
					"connection_ref": "8d7362da-15c4-4178-888d-086b337e6f33",
					"base_dn": "dc=company,dc=local",
					"scope": "SUB",
					"size_limit": "0",
					"filter_template": "(sAMAccountName={{request.username}})",
					"attributes": "sAMAccountName"
				}
			},
			{
				"name": "AssertionProvider",
				"config": {
					"targetEntityID": "idp",
					"sourceID": "https://apache.phenixid.se/saml",
					"nameIDAttribute": "sAMAccountName"
				}
			}
		]
	}
</p>
Click to copy