guardian / gateway

🕵️🆔👤The platform for authentication at (profile.)theguardian.com
https://profile.theguardian.com
12 stars 1 forks source link

Passwordless | Passcodes for reset password - ACTIVE users #2852

Closed coldlink closed 1 month ago

coldlink commented 2 months ago

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:

  1. ACTIVE users - has email + password authenticator (okta idx email verified)
  2. ACTIVE users - has only password authenticator (okta idx email not verified)
  3. 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:

  1. 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
  2. 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
  3. 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
  4. Call POST /idp/idx/challenge with stateHandle authenticator:methodType=password,id=password_authenticator_id
    • Normally a form to submit a password, however we want to call the recover option
  5. Call POST /idp/idx/recover with stateHandle
    • Remediation should have authenticator-verification-data with authenticator type Email
  6. Call POST /idp/idx/challenge with stateHandle authenticator:methodType=email,id=email_authenticator_id
    • User should be sent a passcode, recover email
  7. 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
  8. 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
  9. 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/code GET 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

Tested?

github-actions[bot] commented 2 months ago

Deploy build 9938 of identity:identity-gateway to CODE

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)

From guardian/actions-riff-raff.