Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Passwordless | Sign in with passcodes #2942

Merged
merged 10 commits into from
Oct 23, 2024
Merged

Conversation

coldlink
Copy link
Member

@coldlink coldlink commented Oct 14, 2024

What does this change?

Following on from passcodes for registration and reset password, the next step including an option to sign in via passcodes too.

This functionality has been added behind the usePasscodeSignIn query parameter flag, so we're able to test this behaviour in production, and implement and UI/UX improvements before we go fully live with it to all users.

User status behaviour

This PR implements this behaviour for all user statuses described below:

Status Behaviour
ACTIVE - with email authenticator Use IDX API to send email factor challenge email.
ACTIVE - with only password authenticator Set placeholder password. Send email factor verification email.
non-ACTIVE Deactive then reactivate user and set placeholder password to force user into ACTIVE state. Then send the email factor challenge/verification email depending on which ACTIVE the user ended up in.
non-existent Don't send an email, but still show passcode input page.

IDX API calls

The general IDX API flow for users to sign in via passcodes for users in the ACTIVE state is as follows:

  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 an email authenticator, then the user can be sent a passcode to sign in
      • If there is only a password authenticator, then we have to set a placeholder password for the user, and send them an email verifcation code

At this point, depending on if the user has the email authenticator or not the steps become different

For users who have the email authenticator it's as follows.

  1. Call POST /idp/idx/challenge with stateHandle authenticator:methodType=email,id=email_authenticator_id
    • This sends the user an email with the passcode
    • The remediation step now says we need to enter a passcode
    • expiresAt has updated to 30 minutes
  2. Once the user types the passcode, call the POST /idp/idx/challenge/answer with stateHandle, credentials:passcode=<passcode> in body
    • This authenticates the code, if valid returns a 200 with the user object inside the response and no remediation
    • The user has authenticated at this point.
  3. Redirect the user to /login/token/redirect?stateToken=${stateHandle.split('~')[0]}
    • This sets the Okta global session idx cookie
    • 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, and eventually the user back to where they were going

For users who only have the password authenticator it's as follows

  1. Use the dangerouslySetPlaceholderPassword method to set a placeholder password for the user
  2. Call POST /idp/idx/challenge with stateHandle authenticator:methodType=password,id=password_authenticator_id
    • This starts the password authenticator challenge
  3. Use the placeholder password, call the POST /idp/idx/challenge/answer with stateHandle, credentials:passcode=<password> in body
    • The remediation will suggest. we need to enroll in the email authenticator
  4. Call POST /idp/idx/credential/enroll with stateHandle authenticator:methodType=email,id=email_authenticator_id
    • This sends the user a verification email with a passcode
  5. Once the user types the passcode, call the POST /idp/idx/challenge/answer with stateHandle, credentials:passcode=<passcode> in body
    • This authenticates the code, if valid returns a 200 with the user object inside the response and no remediation
    • The user has authenticated at this point.
  6. Redirect the user to /login/token/redirect?stateToken=${stateHandle.split('~')[0]}
    • This sets the Okta global session idx cookie
    • 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, and eventually the user back to where they were going

For users in a non-ACTIVE state, we use the forceUserIntoActiveState function, which first calls deactivateUser followed by activateUser to get an activation/recovery token. It then sets a placeholder password for the user using the dangerouslySetPlaceholderPasswordUsingRecoveryToken helper, which forces the user into the ACTIVE state. At this point the user will be in one of the above states, so we can call oktaIdxApiSignInPasscodeController to send the user an OTP to sign in.

Current UX

While we attempt to get support regarding improving our UX, the following behaviour is used in order to sign in with passcodes when the flag is used.

Passcode Password
https://github.com/user-attachments/assets/00092a1a-e2d6-4efe-af0d-f34c6b299f94 https://github.com/user-attachments/assets/2cb4eb3c-c352-4067-baf4-928607f457e7
Screenshot 2024-10-18 at 12-04-35 Pages _ SignIn - With Passcode Selected Default Passcode ⋅ Storybook Screenshot 2024-10-18 at 12-05-05 Pages _ SignIn - With Passcode Selected Default Passcode ⋅ Storybook

Tested

  • CODE - with the usePasscodeSignIn flag
  • CODE - no flag (existing behaviour)
  • CODE - with flag, on apps

@coldlink coldlink force-pushed the mm/passcodes-signin-active branch 4 times, most recently from 25f7b0e to 3e7bd1b Compare October 16, 2024 09:11
@coldlink coldlink changed the base branch from main to mm/passcodes-signin-active-refactor-fixes October 16, 2024 10:17
Base automatically changed from mm/passcodes-signin-active-refactor-fixes to main October 16, 2024 14:03
An error occurred while trying to automatically change base from mm/passcodes-signin-active-refactor-fixes to main October 16, 2024 14:03
@coldlink coldlink force-pushed the mm/passcodes-signin-active branch 5 times, most recently from aa24e7c to 369152c Compare October 17, 2024 15:22
@coldlink coldlink requested review from a team and removed request for a team October 17, 2024 15:23
@coldlink coldlink marked this pull request as ready for review October 18, 2024 09:26
@coldlink coldlink requested a review from a team as a code owner October 18, 2024 09:26
@coldlink coldlink requested review from a team and removed request for a team October 18, 2024 10:23
- `usePasscodeSignIn` determines if the option to sign in with passcodes is available
- `signInCurrentView` tracks whether the `password` or `password` view of the sign in page at the point of user submission, so if the user navigates back to the sign in page, it would be on the view they last selected
@coldlink coldlink requested a review from a team October 22, 2024 10:52
@coldlink coldlink requested review from a team and removed request for a team October 22, 2024 10:58
@coldlink coldlink added the passwordless PRs/Issues related to passwordless/passcode functionality label Oct 23, 2024
Copy link
Contributor

@akinsola-guardian akinsola-guardian left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

@coldlink coldlink merged commit bb8b32e into main Oct 23, 2024
21 checks passed
@coldlink coldlink deleted the mm/passcodes-signin-active branch October 23, 2024 09:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
passwordless PRs/Issues related to passwordless/passcode functionality
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants