This PR sets up the first bit of functionality to allow some readers in the ACTIVE state, to reset their password using a passcode sent to their email instead of a link.
If the user is ACTIVE, then they'll be in one of 3 states:
ACTIVE users - has email + password authenticator (okta idx email verified)
ACTIVE users - has only password authenticator (okta idx email not verified)
ACTIVE users - has only email authenticator (SOCIAL users - no password, or passcode only users (not implemented yet))
This PR adds in the ability for users in states 1 and 3 to reset their passwords using a passcode.
This functionality has been added behind the usePasscodesResetPassword feature flag.
This allows us to merge this into production and iron out any bugs we find while developing this feature for the other user states. The flag will eventually be removed when all user states for reset password have been migrated to passcodes.
The general IDX API flow for users in these state is:
Call the POST /oauth2/<custom_auth_server>/v1/interact endpoint
Same parameters auth an authorization code flow call (/authorize endpoint)
We only want to allow the profile application to do this (this is the gateway one)
And maybe the sample application for testing
This returns a single key in the response body - interaction_handle
Call the POST /idp/idx/introspect endpoint with the interaction_handle in the post body
This returns a number of things in the response body, effectively all the information needed to generate a login form
The only thing we need from this is the stateHandle key, which identifies the current authentication request
There is an expiresAt key here, which initially is set to 2 hours in the future
The other things here include information on how to generate a login form, and the social options (and links) available to do this
You can also call this endpoint at any other time with the stateHandle in the body to get the current transaction state to see if it’s valid
Call the POST /idp/idx/identify endpoint with the email, rememberMe=true, and stateHandle in the body
This returns an object very similar to the /introspect endpoint but with different things
The expiresAt key has now changed to 5 mins
The remediation key has everything in it relating to how to resolve the current request
This also lists which authenticators the user has available, and lets us identify the users in each given state
If there is both the password and email authenticator, then the user is in state 1
Continue to the next step
If there is only a password authenticator, then the user is in state 2
This will be implemented in a future PR
If there is only an email authenticator then the user is in state 3
Set a placeholder password for the user, this will give them the password authenticator, and start again from step 1
In remediation.value[1].value array, there should be 2 authenticators listed, Email and Password
To reset password be need to first select Password option
Call POST /idp/idx/challenge with stateHandleauthenticator:methodType=password,id=password_authenticator_id
Normally a form to submit a password, however we want to call the recover option
Call POST /idp/idx/recover with stateHandle
Remediation should have authenticator-verification-data with authenticator type Email
Call POST /idp/idx/challenge with stateHandleauthenticator:methodType=email,id=email_authenticator_id
User should be sent a passcode, recover email
Call the POST /idp/idx/challenge/answer with the following shape
{“credentials”: { “passcode”: “<passcode>” }, “stateHandle”: “<state_handle>” } as request body
Remediation with reset-authenticator with how to set a new password, and optionally revoke sessions
Call the POST /idp/idx/challenge/answer with the following shape
{“credentials”: { “passcode”: “<password>” }, “stateHandle”: “<state_handle>” } as request body
This authenticates the code, if valid returns a 200 with the following
A basic user object
Set-cookie with idx cookie and value, set this
Redirect the user to /login/token/redirect?stateToken=${stateHandle.split('~')[0]}
The idx cookie returned in the previous call doesn’t set a global session, you have to redirect the user to this endpoint for it to be enforced
The stateToken is the stateHandle everything before the first ~ character
This will redirect the user to the callback defined in the interact call at the start and completes the interaction code flow
Steps 1 and 2 are provided by the startIdxFlow method.
Steps 3 - 6 are provided by the new changePasswordEmailIdx method.
Step 7 is submitPasscode inside the new POST /reset-password/code method
Steps 8-9 is already provided for us the registration passcode flow, with the exception we add a check for the reset-authenticator inside the introspect response to make sure the user is in the correct state.
Here's more information about each individual commit in detail:
Update changePassword.ts/checkPasswordToken.ts for password (re)set with passcodes
We update the IDX API handlers in both files to handle the case for password (re)set, which is to add a check for the query parameter (usePasscodesResetPassword) and validateIntrospectRemediation for the reset-authenticator name.
Add /reset-password/codeGET and POST methods to handle passcodes
GET /reset-password/code - Essentially the email sent page, but for when using passcodes. Users can end up at this URL after calling POST /reset-password/code and the code was incorrect/there was an error.
POST /reset-password/code - Endpoint to handle passcode submitted from the email sent page. It tries to submit the passcode and redirect the user to the correct page to set or reset their password.
Add POST /reset-password/code/resend to handle resend functionality
Simple endpoint that just reruns the sendEmailInOkta function when the user clicks resend, no need for anything fancy here.
Add changePasswordEmailIdx to sendChangePasswordEmail.ts
This set's up the functionality to send an email with a passcode inside the sendChangePasswordEmail which is used for all things password reset related.
The changePasswordEmailIdx starts the Okta IDX flow and goes through all the steps necessary to send the user an email with a passcode. Currently it only checks for the user with Status.ACTIVE and user.credentials.password (user has a password). If a user is not in this state it will revert to the legacy password reset flow.
The steps and api calls this function makes are described above.
It will redirect the user to the email sent page with a passcode input if successful, or give an error and revert to the legacy flow.
Add idx/passcode usage metrics for changePasswordMetric
changePasswordMetric was only used for the legacy flow, this commit updates this functionality so that we can use this for passcodes also and provide a slightly different metric
Add cypress tests
As it says on the tin, adds cypress tests to test reset password with passcodes for ACTIVE users with password.
Add passcode reset for users with only email authenticator (i.e SOCIAL users)
If a user has only the email authenticator, and no password, we set a placeholder password for these users, and then rerun the changePasswordEmailIdx method to send a passcode to these users.
How to test
Deploy this PR to CODE.
Go to https://profile.code.dev-theguardian.com/reset-password?usePasscodesResetPassword=true
Go through the reset password process, depending on the user type you'll get a different journey
In this PR if you want to use passcodes you need to do reset password for an already ACTIVE user with password
The easiest way to get a user into this state is to create a new user and go through the whole flow, once you've signed in, sign out, and then do the reset password process
Tested?
[x] CODE - ACTIVE user with password, email factors - with flag - passcodes
[x] CODE - ACTIVE user with password, email factors - without flag - no passcodes
[x] CODE - ACTIVE user only email factor - with flag - passcodes
[x] CODE - ACTIVE user only email factor - without flag - no passcodes
[x] CODE - ACTIVE user only password factor - with flag - no passcodes
[x] CODE - ACTIVE user only password factor - without flag - no passcodes
[x] CODE - Other states - with flag - no passcodes
[x] CODE - Other states - without flag - no passcodes
All deployment options
- [Deploy build 9938 of `identity:identity-gateway` to CODE](https://riffraff.gutools.co.uk/deployment/deployAgain?project=identity%3Aidentity-gateway&build=9938&stage=CODE&updateStrategy=MostlyHarmless&action=deploy)
- [Deploy parts of build 9938 to CODE by previewing it first](https://riffraff.gutools.co.uk/preview/yaml?project=identity%3Aidentity-gateway&build=9938&stage=CODE&updateStrategy=MostlyHarmless)
- [What's on CODE right now?](https://riffraff.gutools.co.uk/deployment/history?projectName=identity%3Aidentity-gateway&stage=CODE)
What does this change?
This PR sets up the first bit of functionality to allow some readers in the ACTIVE state, to reset their password using a passcode sent to their email instead of a link.
If the user is ACTIVE, then they'll be in one of 3 states:
This PR adds in the ability for users in states 1 and 3 to reset their passwords using a passcode.
This functionality has been added behind the
usePasscodesResetPassword
feature flag.This allows us to merge this into production and iron out any bugs we find while developing this feature for the other user states. The flag will eventually be removed when all user states for reset password have been migrated to passcodes.
The general IDX API flow for users in these state is:
POST /oauth2/<custom_auth_server>/v1/interact
endpoint/authorize
endpoint)profile
application to do this (this is the gateway one)interaction_handle
POST /idp/idx/introspect
endpoint with theinteraction_handle
in the post bodystateHandle
key, which identifies the current authentication requestexpiresAt
key here, which initially is set to 2 hours in the futurestateHandle
in the body to get the current transaction state to see if it’s validPOST /idp/idx/identify
endpoint with theemail
,rememberMe=true
, andstateHandle
in the body/introspect
endpoint but with different thingsexpiresAt
key has now changed to 5 minsremediation
key has everything in it relating to how to resolve the current requestpassword
andemail
authenticator, then the user is in state 1password
authenticator, then the user is in state 2email
authenticator then the user is in state 3password
authenticator, and start again from step 1remediation.value[1].value
array, there should be 2 authenticators listed,Email
andPassword
Password
optionPOST /idp/idx/challenge
withstateHandle
authenticator:methodType=password,id=password_authenticator_id
POST /idp/idx/recover
withstateHandle
authenticator-verification-data
with authenticator typeEmail
POST /idp/idx/challenge
withstateHandle
authenticator:methodType=email,id=email_authenticator_id
POST /idp/idx/challenge/answer
with the following shape{“credentials”: { “passcode”: “<passcode>” }, “stateHandle”: “<state_handle>” }
as request bodyreset-authenticator
with how to set a new password, and optionally revoke sessionsPOST /idp/idx/challenge/answer
with the following shape{“credentials”: { “passcode”: “<password>” }, “stateHandle”: “<state_handle>” }
as request bodyuser
objectSet-cookie
withidx
cookie and value, set this/login/token/redirect?stateToken=${stateHandle.split('~')[0]}
idx
cookie returned in the previous call doesn’t set a global session, you have to redirect the user to this endpoint for it to be enforcedstateToken
is thestateHandle
everything before the first~
characterinteract
call at the start and completes the interaction code flowSteps 1 and 2 are provided by the
startIdxFlow
method.Steps 3 - 6 are provided by the new
changePasswordEmailIdx
method.Step 7 is
submitPasscode
inside the newPOST /reset-password/code
methodSteps 8-9 is already provided for us the registration passcode flow, with the exception we add a check for the
reset-authenticator
inside the introspect response to make sure the user is in the correct state.Here's more information about each individual commit in detail:
Update
changePassword.ts
/checkPasswordToken.ts
for password (re)set with passcodesWe update the IDX API handlers in both files to handle the case for password (re)set, which is to add a check for the query parameter (
usePasscodesResetPassword
) andvalidateIntrospectRemediation
for thereset-authenticator
name.Add
/reset-password/code
GET
andPOST
methods to handle passcodesGET /reset-password/code
- Essentially the email sent page, but for when using passcodes. Users can end up at this URL after callingPOST /reset-password/code
and the code was incorrect/there was an error.POST /reset-password/code
- Endpoint to handle passcode submitted from the email sent page. It tries to submit the passcode and redirect the user to the correct page to set or reset their password.Add
POST /reset-password/code/resend
to handle resend functionalitySimple endpoint that just reruns the
sendEmailInOkta
function when the user clicks resend, no need for anything fancy here.Add
changePasswordEmailIdx
tosendChangePasswordEmail.ts
This set's up the functionality to send an email with a passcode inside the
sendChangePasswordEmail
which is used for all things password reset related.The
changePasswordEmailIdx
starts the Okta IDX flow and goes through all the steps necessary to send the user an email with a passcode. Currently it only checks for the user withStatus.ACTIVE
anduser.credentials.password
(user has a password). If a user is not in this state it will revert to the legacy password reset flow.The steps and api calls this function makes are described above.
It will redirect the user to the email sent page with a passcode input if successful, or give an error and revert to the legacy flow.
Add idx/passcode usage metrics for
changePasswordMetric
changePasswordMetric
was only used for the legacy flow, this commit updates this functionality so that we can use this for passcodes also and provide a slightly different metricAdd cypress tests
As it says on the tin, adds cypress tests to test reset password with passcodes for ACTIVE users with password.
Add passcode reset for users with only email authenticator (i.e SOCIAL users)
If a user has only the email authenticator, and no password, we set a placeholder password for these users, and then rerun the
changePasswordEmailIdx
method to send a passcode to these users.How to test
https://profile.code.dev-theguardian.com/reset-password?usePasscodesResetPassword=true
Tested?