From e4c5926848977a3e1512e76ecb0e6ca9dfdbdc53 Mon Sep 17 00:00:00 2001 From: Mahesh Makani Date: Mon, 21 Oct 2024 12:10:38 +0100 Subject: [PATCH] docs(idx): add documentation on the Interaction Code flow and IDX API --- docs/okta/idx/README.md | 104 ++- docs/okta/idx/create-account-idx.md | 67 ++ docs/okta/idx/hoppscotch.md | 51 ++ docs/okta/idx/idx-api.md | 1090 +++++++++++++++++++++++++++ docs/okta/idx/reset-password-idx.md | 94 +++ docs/okta/idx/sign-in-idx.md | 127 ++++ 6 files changed, 1495 insertions(+), 38 deletions(-) create mode 100644 docs/okta/idx/create-account-idx.md create mode 100644 docs/okta/idx/hoppscotch.md create mode 100644 docs/okta/idx/idx-api.md create mode 100644 docs/okta/idx/reset-password-idx.md create mode 100644 docs/okta/idx/sign-in-idx.md diff --git a/docs/okta/idx/README.md b/docs/okta/idx/README.md index 3ff3578f0..ed59724aa 100644 --- a/docs/okta/idx/README.md +++ b/docs/okta/idx/README.md @@ -1,57 +1,85 @@ -# Okta IDX API +# Okta IDX API and Identity Engine Status: WIP -_More information about the Okta IDX API will be added soon_ +#### To document -## Hoppscotch Collection +- [x] IDX API introduction +- [ ] Quick Start +- [ ] Why passcodes and passwordless? +- [ ] Gateway implementation +- [x] Further implementation context +- [x] [Hoppsotch](./hoppscotch.md) +- [x] [API endpoints](./idx-api.md) +- [ ] [Sign in](./sign-in-idx.md) +- [x] [Create account](./create-account-idx.md) +- [x] [Reset password](./reset-password-idx.md) -To make working with the Okta IDX API easier, we've added a [Hoppscotch](https://hoppscotch.com/) collection that can be used to make calls directly to the IDX API. +### Quick Start -### Setting up the collection + variables +_TODO: Add a quick start guide to using the IDX API_ -Hoppscotch is a tool which self describes as a took for "developers to build, test and share APIs". It is similar to other tools such as Postman. Except [open source](https://github.com/hoppscotch/hoppscotch). -The collection might also work in other tools, but it's untested. +## Identity Engine -1. Download the [collection](./okta_idx_hoppscotch_collection.json) -2. In the desktop Hoppscotch app click "Collections" -> "Import/Export" button -3. Click "Import from Hoppscotch" and select the collection file. -4. When imported, a new "Okta IDX" collection should've appeared! +In Q3 2023, we upgraded our Okta environment to [Identity Engine](https://developer.okta.com/docs/concepts/oie-intro/), which is Okta's new(ish) authentication pipeline with additional features compared to the older [Classic Engine](https://developer.okta.com/docs/guides/archive-overview/main/), which we'll refer to as "Okta Classic" or "Okta Legacy" or some variation of the two. -You'll also need an Hoppscotch "Environment" containing the variables/secrets. +Primarily for us, Identity Engine enables features that were previously impossible or difficult to implement, such as sign in without a password, or MFA (multi-factor authentication). Currently in Gateway (and the identity platform as a whole), we've been implementing "email" factor authentication and recovery, primarily referred to as "passcodes" or "OTPs (one-time passcodes)", and the generic concept of "passwordless". See the ["Why passcodes and passwordless?"](#why-passcodes-and-passwordless) section for more information on why we're betting on passcodes and passwordless. -A collection pointing to the CODE environment is available on S3. Use the following to download it. Be sure to have the `identity` Janus credentials. +### Interaction Code flow -```sh -# this downloads it to your home directory, feel free to change the path if needed e.g. to your downloads folder -$ aws s3 cp --profile identity s3://identity-private-config/DEV/identity-gateway/okta_code_environment_hoppscotch.json ~/okta_code_environment_hoppscotch.json -``` +In order to utilise some of these new features with Identity Engine, Okta have created a **proprietary** extension to the [OAuth 2.0 and Open ID Connect](../oauth.md) standard called the "[Interaction Code](https://developer.okta.com/docs/concepts/interaction-code/) grant type", which in their words "enables you to create a more customized user authentication experience", and behaves in a similar way to the [Authorization Code flow with PKCE](../oauth.md#oauth-20-flowsgrant-types). -Once downloaded, in Hoppscotch you can: +The [overview](https://developer.okta.com/docs/concepts/interaction-code/) gives a reasonable overview of how this new extension works. Essentially the flow consists a number of "interactions" between the client/user and the authorization server in order to authenticate the user. Each "interaction" is called a "remediation" step, and corresponds to a piece of user data or input required by the authorization server in order to continue to the next step, until the user is fully authenticated. -1. Select "Environments" -2. "Import/Export" button -3. Click "Import from Hoppscotch" and select the environment file. -4. When imported, a new "Okta CODE" environment will have appeared! +A bonus to the interaction code flow is that it makes it possible to both create new users and allow users to reset their password, as part of a single authentication process which wasn't previously possible using Classic flows. Previously we'd have to use multiple different Okta API endpoints to give the impression to the user that it was a single flow. Now with the Interaction Code flow, we can use this to potentially sign in, create a new user, or reset a password all as part of the same process! -Now the "Okta CODE" environment can be selected. You can do this in the top-right under the environments dropdown. +The documentation suggests that in order to use all these new features, we have to migrate our applications to use the Interaction Code flow. But since this is a **proprietary** extension, we'd rather avoid implementing this everywhere, as that would lead to potential vendor lock in, and we'd have to migrate all the user facing applications we've already migrated to use OAuth just to enable features like passwordless. This isn't ideal. -This file already prefills the non-secret and non-changeable variables, called "variables" in Hoppscotch. If these do change, make sure to update the file in s3! +Thankfully though, we have a simple solution to this, the only place that actually needs to migrate to use the Interaction Code flow is Gateway, as Gateway (and more accurately the `profile` subdomain) is the single point for all authentication activities at the Guardian. User facing applications that need an authenticated user session **_should_** continue to use the Authorization Code flow (with or without PKCE) in order to authenticate a user. This is because when the Authorization Code flow is started, it requires a browser based login, as it has to call the `/authorize` endpoint in order to check if the user is already authenticated or not. If the user isn't authenticated it uses the browser to attempt to authenticate the user. -Under the "secrets" tab, these are all things which are either secret, or change frequently. These should be filled in as needed. +This browser based sign in normally redirects to the Okta hosted login page. However thanks to our [Okta hosted login page interception](../login-page-interception.md) functionality, we perform a client side redirect from the Okta hosted page to Gateway, with all the parameters we need to authenticate the user. From there we use Gateway to authenticate the user, before redirecting back to the application once authenticated. This means the only place which we need to update to use the Interaction Code flow is Gateway! -### Using the collection +Because the Interaction Code grant type is a proprietary extension by Okta, we have to interact with it the way Okta wants us to, this means using their new authentication API where the Interaction Code flow is supported, namely the Okta IDX API. -To interface with the Okta IDX API, you always have to make 2 calls in order to begin with. +## IDX API -1. First call the "IDX /interact" endpoint. - - All the variables/parameters should be set up for this, so just click send! - - This returns an `interaction_handle` in the body. - - This will automatically be updated in the variables as `interactionHandle` -2. Next call the "IDX /introspect (with `interactionCode`)" endpoint. - - This uses the `interactionHandle` from the previous step. - - This returns a bunch of things, but it's automatically be updated in the variables as `stateHandle` - - The response includes a `remediation` property which contains an array of possible next steps, this is the same for every IDX API endpoint - - More information about the IDX API will be added soon. -3. Ready to make further calls! - - You may need to set up further secrets under environments to make these calls, the ones that you might need to set/update will be highlighted +The IDX API is built on the Okta Identity Engine and implements the Interaction Code flow grant type. In order to implement this they want clients to use their SDKs where this API is supported. The IDX API is also undocumented outside of [SDK documentation](https://github.com/okta/okta-auth-js/blob/master/docs/idx.md). Under the hood however the SDKs are making standard HTTP requests to a number of different API endpoints. + +Our initial plan was to use the SDK in order to implement the Interaction Code flow and the IDX API in Gateway, however we found that the SDKs were too limiting in terms of being able to fully customise the flow and experience for users, and it also wouldn't set a [global session](../sessions.md#idp-session) outside of the Okta hosted flow. The only option remaining was to reverse engineer the calls made to the IDX API. + +Thankfully for us this was relatively straightforward, we could see how the Okta hosted login page was using the IDX API, and document the behaviour in order of us to replicate. This documentation serves as the primary source for what we found in our investigation and implementation of the IDX API. + +The decision to reverse engineer wasn't straightforward, we had to be relatively certain that Okta wouldn't suddenly change the behaviour of the IDX API which would potentially break our reverse engineered implementation. We came to this conclusion for two reasons; 1) The Okta IDX API is versioned, it returns a `version` key (currently `1.0.0`) in the response, and through the header, `Accept` and `Content-Type` supply `application/ion+json; okta-version=1.0.0` and 2) the [SDK](https://github.com/okta/okta-auth-js/?tab=readme-ov-file#release-status) also has a "Release Status" section, showing which versions of the SDK are supported, and when they will be deprecated. This gives us some confidence that the API won't change too much in the near future. + +The IDX API is a relatively simple API, it consists of a number of different endpoints, each corresponding to a different "remediation" step in the Interaction Code flow. The API is documented in the [IDX API documentation](./idx-api.md), and the [Hoppscotch](./hoppscotch.md) documentation shows how to interact directly with the API. You can specifically see how the IDX is implemented for [sign in](./sign-in-idx.md), [create account](./create-account-idx.md), and [reset password](./reset-password-idx.md) in their respective documentation. + +## Why passcodes and passwordless? + +## Gateway implementation + +_TODO: Add a section on how the IDX API is implemented in Gateway_ + +## Further implementation context + +Here's a list of PRs/Issues that are related to Identity Engine, IDX API, and passwordless (passcodes). This may not be conclusive. + +Identity Engine Upgrade PRs: + +- Gateway + - https://github.com/guardian/gateway/pull/2422 + - https://github.com/guardian/gateway/pull/2424 + - https://github.com/guardian/gateway/pull/2433 + - https://github.com/guardian/gateway/pull/2450 + - https://github.com/guardian/gateway/pull/2463 +- [Identity Platform](https://github.com/guardian/identity-platform) + - https://github.com/guardian/identity-platform/pull/679 + - https://github.com/guardian/identity-platform/pull/682 + - https://github.com/guardian/identity-platform/pull/684 + - https://github.com/guardian/identity-platform/pull/685 + - https://github.com/guardian/identity-platform/pull/688 + +Okta IDX API Setup and Social Sign In using IDX - https://github.com/guardian/gateway/pull/2625 & https://github.com/guardian/identity-platform/pull/718 + +See the [`passwordless` label](https://github.com/guardian/gateway/issues?q=label%3Apasswordless+) for all PRs related to passcodes/passwordless, and the respective label in [`identity-platform` repo](https://github.com/guardian/identity-platform/issues?q=label%3Apasswordless) + +Specific PRs are shown in the documents describing the individual implementations/flows of the IDX API. diff --git a/docs/okta/idx/create-account-idx.md b/docs/okta/idx/create-account-idx.md new file mode 100644 index 000000000..bc26f452f --- /dev/null +++ b/docs/okta/idx/create-account-idx.md @@ -0,0 +1,67 @@ +# Create Account flow with Okta IDX API + +This document describes how the create account flow ([`/register`](https://profile.theguardian.com/register)) is implemented using the Okta IDX API. + +See the [IDX API documentation](./idx-api.md) for more information on the API, e.g. to look up the specific endpoints and body used in the create account flow. The flowcharts below only show the expected success and error paths, if there are any unexpected errors, we fall back to the classic Okta API flow. + +## User States + +Currently the IDX API for users going through the create account journey is only implemented for new users. For existing users attempting to go through the registration flow we use the legacy Okta API for the time being in order to let the user know that they already have an account and can either reset their password or sign in. This is until we have UX consultation on how to handle existing users going through the registration flow. + +| State | Description | Action | +| ---------------- | ------------------------------- | ------------------------------------------- | +| No existing user | The user does not exist in Okta | User is created using the IDX API | +| Existing user | The user exists in Okta | User sent email saying their account exists | + +## Flowchart + +```mermaid +flowchart TD + start(User visits /register) + start --> enter-email[/User enters email and submits form/] + enter-email --> interact[POST /oauth2/auth_server_id/v1/interact] + interact --> introspect[POST /idp/idx/introspect] + introspect --> enroll[POST /idp/idx/enroll] + enroll --> enroll-new[POST /idp/idx/enroll/new] + enroll-new --> enroll-new-check{Check Response} + enroll-new-check -- Success --> email-sent-passcode[/Show email sent page
with passcode input/] + enroll-new-check -- User Exists --> fallback-classic + email-sent-passcode -- Submit Code --> challenge-answer[POST /idp/idx/challenge/answer] + email-sent-passcode -- Resend Code --> email-sent-passcode-resend[POST /idp/idx/challenge/resend] + email-sent-passcode -- Change email --> start + email-sent-passcode-resend -- Success --> email-sent-passcode + challenge-answer --> challenge-answer-check{Check Response} + challenge-answer-check -- Success --> credential-enroll-password[POST /idp/idx/credential/enroll
password authenticator] + challenge-answer-check -- Invalid Passcode
Show Error --> email-sent-passcode + challenge-answer-check -- Passcode Expired

Show expired page --> start + credential-enroll-password --> password-page[/Show password page
user enters password and submits/] + password-page --> challenge-answer-password[POST /idp/idx/challenge/answer] + challenge-answer-password --> challenge-answer-password-check{Check Response} + challenge-answer-password-check -- Success --> login-redirect([303 Redirect /login/token/redirect]) + challenge-answer-password-check -- Invalid Password
e.g. short/long/breached etc.
Show Error --> password-page + login-redirect -- set global session --> finish + + finish(User finished account creation
they've redirected back to the application they were on
or the new account review page is shown) + + fallback-classic(Fallback to Classic Flow
Handle existing user) +``` + +## Implementation + +See the [`oktaIdxCreateAccount`](https://github.com/guardian/gateway/blob/bb8b32e30dd178a7ffe81ec75c64b2ce4ad93aeb/src/server/routes/register.ts#L319-L335) method for the implementation in code to send the user a passcode email for verification, this is called from the [`POST /register`](https://github.com/guardian/gateway/blob/bb8b32e30dd178a7ffe81ec75c64b2ce4ad93aeb/src/server/routes/register.ts#L108-L115) route. + +The passcode submit route is [`POST /register/code`](https://github.com/guardian/gateway/blob/bb8b32e30dd178a7ffe81ec75c64b2ce4ad93aeb/src/server/routes/register.ts#L143-L145), and the route to resend the passcode is [`POST /register/code/resend`](https://github.com/guardian/gateway/blob/5211380b6cfbe2ad5bfe4f0d1aeed7a1ff831333/src/server/routes/register.ts#L244-L246). + +### PRs + +Here is a list of pull requests/issues relating to the create account flow with the Okta IDX API, probably not an exhaustive list: + +- [#2567 - Initial (outdated) flowchart](https://github.com/guardian/gateway/issues/2567) +- [#2639 - Passcodes | Set up passcodes for registration](https://github.com/guardian/gateway/pull/2639) +- [#2671 - Passcodes | Add email template for RegistrationPasscode](https://github.com/guardian/gateway/pull/2671) + - Additional PRs for email related issues + - https://github.com/guardian/gateway/pull/2729 + - https://github.com/guardian/gateway/pull/2737 +- [#2752 - Passcodes | Remove usePasscodeRegistration query parameter and make passcode registration default](https://github.com/guardian/gateway/pull/2752) +- [#2773 - Passcodes | Fix issues after round one of testing](https://github.com/guardian/gateway/pull/2773) +- [#2786 - Passcodes | Improve passcode styling/functionality](https://github.com/guardian/gateway/pull/2786) diff --git a/docs/okta/idx/hoppscotch.md b/docs/okta/idx/hoppscotch.md new file mode 100644 index 000000000..7454e5c16 --- /dev/null +++ b/docs/okta/idx/hoppscotch.md @@ -0,0 +1,51 @@ +# Hoppscotch + +To make working with the Okta IDX API easier, we've added a [Hoppscotch](https://hoppscotch.com/) collection that can be used to make calls directly to the IDX API. + +### Setting up the collection + variables + +Hoppscotch is a tool which self describes as a took for "developers to build, test and share APIs". It is similar to other tools such as Postman. Except [open source](https://github.com/hoppscotch/hoppscotch). +The collection might also work in other tools, but it's untested. + +1. Download the [collection](./okta_idx_hoppscotch_collection.json) +2. In the desktop Hoppscotch app click "Collections" -> "Import/Export" button +3. Click "Import from Hoppscotch" and select the collection file. +4. When imported, a new "Okta IDX" collection should've appeared! + +You'll also need an Hoppscotch "Environment" containing the variables/secrets. + +A collection pointing to the CODE environment is available on S3. Use the following to download it. Be sure to have the `identity` Janus credentials. + +```sh +# this downloads it to your home directory, feel free to change the path if needed e.g. to your downloads folder +$ aws s3 cp --profile identity s3://identity-private-config/DEV/identity-gateway/okta_code_environment_hoppscotch.json ~/okta_code_environment_hoppscotch.json +``` + +Once downloaded, in Hoppscotch you can: + +1. Select "Environments" +2. "Import/Export" button +3. Click "Import from Hoppscotch" and select the environment file. +4. When imported, a new "Okta CODE" environment will have appeared! + +Now the "Okta CODE" environment can be selected. You can do this in the top-right under the environments dropdown. + +This file already prefills the non-secret and non-changeable variables, called "variables" in Hoppscotch. If these do change, make sure to update the file in s3! + +Under the "secrets" tab, these are all things which are either secret, or change frequently. These should be filled in as needed. + +### Using the collection + +To interface with the Okta IDX API, you always have to make 2 calls in order to begin with. + +1. First call the "IDX /interact" endpoint. + - All the variables/parameters should be set up for this, so just click send! + - This returns an `interaction_handle` in the body. + - This will automatically be updated in the variables as `interactionHandle` +2. Next call the "IDX /introspect (with `interactionCode`)" endpoint. + - This uses the `interactionHandle` from the previous step. + - This returns a bunch of things, but it's automatically be updated in the variables as `stateHandle` + - The response includes a `remediation` property which contains an array of possible next steps, this is the same for every IDX API endpoint + - More information about the IDX API will be added soon. +3. Ready to make further calls! + - You may need to set up further secrets under environments to make these calls, the ones that you might need to set/update will be highlighted diff --git a/docs/okta/idx/idx-api.md b/docs/okta/idx/idx-api.md new file mode 100644 index 000000000..4c9ff602b --- /dev/null +++ b/docs/okta/idx/idx-api.md @@ -0,0 +1,1090 @@ +# IDX API Documentation + +_See the [README](./README.md#idx-api) for an introduction to the IDX API_ + +This document describes the Okta IDX API endpoints that are used in Gateway to authenticate a user using the Interaction Code flow. + +See the [Hoppscotch](./hoppscotch.md) collection for a way to interact with the IDX API directly. + +See the specific flow documentation for how the IDX API is used in Gateway for [sign in](./sign-in-idx.md), [create account](./create-account-idx.md), and [reset password](./reset-password-idx.md). + +Within Gateway, everything that is directly specific for the IDX API is located in the [`src/server/lib/okta/idx`](https://github.com/guardian/gateway/blob/bb8b32e30dd178a7ffe81ec75c64b2ce4ad93aeb/src/server/lib/okta/idx) directory. We use [`zod`](https://zod.dev/) to model and validate the request and response types for the IDX API. To make it easier to call the IDX API, we have a `fetch` wrapper method specifically for the IDX API implementation in Gateway, see the [`idxFetch` method](https://github.com/guardian/gateway/blob/bb8b32e30dd178a7ffe81ec75c64b2ce4ad93aeb/src/server/lib/okta/idx/shared/idxFetch.ts#L85-L95) for information about that. This handles correctly parsing the request and response types, and also handles error handling and logging. + +## API Endpoints + +#### Endpoints used in Gateway + +These endpoints are used in Gateway to authenticate a user using the Interaction Code flow. + +- [`interact`](#interact) - `POST /oauth2/{authorizationServerId}/v1/interact` +- [`introspect`](#introspect) - `POST /idp/idx/introspect` +- [`identify`](#identify) - `POST /idp/idx/identify` +- [`enroll`](#enroll) - `POST /idp/idx/enroll` +- [`enroll/new`](#enrollnew) - `POST /idp/idx/enroll/new` +- [`challenge`](#challenge) - `POST /idp/idx/challenge` +- [`challenge/answer`](#challengeanswer) - `POST /idp/idx/challenge/answer` +- [`challenge/resend`](#challengeresend) - `POST /idp/idx/challenge/resend` +- [`recover`](#recover) - `POST /idp/idx/recover` +- [`credential/enroll`](#credentialenroll) - `POST /idp/idx/credential/enroll` + +#### Other Endpoints + +These endpoints are not used in Gateway, but are part of the IDX API, and could potentially be used or implemented in the future if needed. For now they are documented here as the [Hoppscotch](./hoppscotch.md) collection includes them. + +- [`identify/select`](#identifyselect) - `POST /idp/idx/identify/select` +- [`skip`](#skip) - `POST /idp/idx/skip` +- [`cancel`](#cancel) - `POST /idp/idx/cancel` +- [`unlock-account`](#unlock-account) - `POST /idp/idx/unlock-account` + +#### Login redirect + +Not an endpoint per say, but the URL that the user should be redirected to after authenticating with the IDX API in order to complete the Interaction Code flow and set a global session cookie. This is the last step in the flow once the user has authenticated. This will eventually redirect the user back to the application that they were initially authenticating from. + +- [`/login/token/redirect`](#logintokenredirect) - `303 See Other GET /idp/idx/login/token/redirect` + +#### Usage + +All methods are `POST` requests, and with the exception of the `interact` endpoint, all requests should have the `Content-Type` and `Accept` headers set to `application/ion+json; okta-version=1.0.0`, and include the `stateHandle` in the body of the request, along with any other required parameters. + +The responses section of the IDX API endpoints describe a subset of the full response, as the response can vary depending on the user/state, and we don't include all possible fields/values. + +For each of the endpoints for more implementation details, such as the exact request/response types and usages, see the implementation section which links to the source code in Gateway. + +In most cases we rely on the remediation name to determine the next step in the flow, and the `stateHandle` to identify the current state of the interaction code flow, rather than the full response. + +### `interact` + +#### Implementation + +[`src/server/lib/okta/idx/interact.ts`](https://github.com/guardian/gateway/blob/bb8b32e30dd178a7ffe81ec75c64b2ce4ad93aeb/src/server/lib/okta/idx/interact.ts#L30-L45) + +#### Description + +Gets an interaction handle from the IDX API, and starts the Interaction Code flow. This is the first endpoint that should be called when authenticating a user using the Interaction Code flow. + +The endpoint takes a parameters that are a subset of those used in the Authorization Code flow with PKCE, and returns an `interaction_handle` which is used to identify the current interaction. + +This `interaction_handle` is then only used in the next step when calling the `introspect` endpoint, after which it can be discarded as the `stateHandle` is used to identify the process from that point forward. + +In Gateway we only use the interaction code flow for authentication, in order to avoid using the Okta hosted sign in page. The standard authorization code flow is used for anything else. + +#### Path + +`POST /oauth2/{authorizationServerId}/v1/interact` + +| Parameter | Description | Value | +| ----------------------- | ---------------------------------- | ------------------------------------------------------------ | +| `authorizationServerId` | The ID of the authorization server | Set to the main Guardian custom authorization server in Okta | + +#### Headers + +```http +Content-Type: application/x-www-form-urlencoded +``` + +#### Body + +The body parameters is similar to that used in the Authorization Code flow, which can be seen in the [Okta `/authorize` endpoint](https://developer.okta.com/docs/api/openapi/okta-oauth/oauth/tag/CustomAS/#tag/CustomAS/operation/authorizeCustomAS). Instead of the parameters being in the query string, they are in the body of the request as form data (`application/x-www-form-urlencoded`). + +We only use a subset of the parameters available in the Authorization Code flow, as we don't need to use all of them, with the ones used shown below. + +See [usage](https://github.com/guardian/gateway/blob/bb8b32e30dd178a7ffe81ec75c64b2ce4ad93aeb/src/server/lib/okta/idx/interact.ts#L100-L116) in Gateway. + +| Parameter | Description | Value | +| ----------------------- | --------------------------------------- | --------------------------------------------------------------------------------------------------- | +| `client_id` | The Client ID of the application | Set to the Gateway (`profile`) client id in Okta | +| `redirect_uri` | The redirect URI of the application | Always set to the interaction code flow callback uri in Gateway | +| `scope` | The scopes requested by the application | Set to the scopes required for authentication | +| `state` | The state parameter | Set to a random string to prevent CSRF, should match whats stored in the Authorization State cookie | +| `code_challenge` | The code challenge | A random string used to verify the code verifier | +| `code_challenge_method` | The code challenge method | Set to `S256` for PKCE | + +#### Response + +```http +Content-Type: application/json +``` + +```json +{ + "interaction_handle": "" +} +``` + +| Parameter | Description | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `interaction_handle` | The interaction handle, used to identify the current interaction, a seemingly random string. Can be discarded after calling [`introspect`](#introspect), after which the `stateHandle` is used to identify the current process. | + +### `introspect` + +#### Implementation + +[`src/server/lib/okta/idx/introspect.ts`](https://github.com/guardian/gateway/blob/bb8b32e30dd178a7ffe81ec75c64b2ce4ad93aeb/src/server/lib/okta/idx/introspect.ts#L79-L92) + +#### Description + +IThe introspect step lets us know what kind of authentication we can perform and what the next steps are, called a "remediation". It also returns the `stateHandle` which identifies the current state of the authentication process, and should be preserved and used in any subsequent requests in the flow. + +This is the second endpoint that should be called when authenticating a user using the Interaction Code flow (using an `interaction_handle` from the [`introspect`](#introspect) endpoint), or at any point in the flow to get the current state and remediation steps (using the `stateHandle` from any `/idp/idx/` call). + +#### Path + +`POST /idp/idx/introspect` + +#### Body + +**With `interactionHandle`** + +```json +{ + "interactionHandle": "" +} +``` + +**With `stateHandle`** + +```json +{ + "stateHandle": "" +} +``` + +#### Response + +```json +{ + "version": "1.0.0", + "stateHandle": "", + "expiresAt": "", + "remediation": [ + { + "name": "", + ... + }, + ... + ] +} +``` + +| Parameter | Description | +| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `version` | The version of the IDX API, currently `1.0.0` | +| `stateHandle` | The state handle, used to identify the current state of the interaction code flow. This should be persisted, and is used in all subsequent calls to the IDX API. | +| `expiresAt` | The time at which the state handle expires, in ISO 8601 format, e.g. `2024-10-21T12:08:37.789Z`. This can change depending on the current remediation step. | +| `remediation` | An array of remediation steps, each corresponding to a different "remediation" step in the Interaction Code flow. Each value can vary depending on the current step. | +| `remediation_name` | The name of the remediation step, is used to identify what next steps can be taken | + +### `identify` + +#### Implementation + +[`src/server/lib/okta/idx/identify.ts`](https://github.com/guardian/gateway/blob/bb8b32e30dd178a7ffe81ec75c64b2ce4ad93aeb/src/server/lib/okta/idx/identify.ts#L47-L56) + +#### Description + +Use the `identify` endpoint to start the sign in process or to reset password, for an existing user. Can be called after the first time after the `introspect` step. + +#### Path + +`POST /idp/idx/identify` + +#### Body + +```json +{ + "stateHandle": "", + "identifier": "", + "rememberMe": true +} +``` + +| Parameter | Description | Value | +| ------------- | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | +| `stateHandle` | The state handle, used to identify the current state of the interaction code flow. | The `stateHandle` from the [`introspect`](#introspect) step. | +| `identifier` | User's primary identifier | The email address of the user to sign in, or reset the password for. | +| `rememberMe` | Whether to be able to set a global session after authentication | Always set to `true` as we always want a global session after authenticating a user | + +#### Response + +Users will get either the `select-authenticator-authenticate` or `challenge-authenticator` remediation step, depending on the user's current state. In most cases, the user will get the `select-authenticator-authenticate` remediation step should they be in the `ACTIVE` state. + +**`select-authenticator-authenticate`** response + +```json +{ + "version": "1.0.0", + "stateHandle": "", + "expiresAt": "", + "remediation": [ + { + "name": "select-authenticator-authenticate", + "value": [ + { + "name": "authenticator", + "options": [ + { + "label": "", + "value": { + "form": { + "value": [ + { + "name": "id", + "value": "" + ... + }, + { + "name": "methodType", + "value": "" + ... + } + ] + } + } + }, + ... + ], + ... + }, + ... + ] + ... + }, + ... + ] +} +``` + +| Parameter | Description | +| ---------------------- | ------------------------------------------------------------------------------------------------------------------ | +| `authenticator_label` | The label of the authenticator, e.g. `"Email"` or `"Password"` | +| `authenticator_id` | The ID of the authenticator, used by the `challenge` step to try and authenticate the user with that authenticator | +| `authenticator_method` | The method type of the authenticator, e.g. `"password"` or `"email"` | + +**`challenge-authenticator`** response + +```json +{ + "version": "1.0.0", + "stateHandle": "", + "expiresAt": "", + "remediation": [ + { + "name": "challenge-authenticator", + "value": [ + { + "name": "credentials", + "form": { + "value": [ + { + "name": "passcode", + ... + } + ] + }, + ... + }, + { + "name": "stateHandle", + ... + } + ] + ... + }, + ... + ] +} +``` + +| Parameter | Description | +| ---------- | ---------------------------------------------------------------------------------------------------------------------------- | +| `passcode` | The passcode to authenticate the user with the `challenge/answer` endpoint, e.g. the user's password or a one-time passcode. | + +### `enroll` + +#### Implementation + +[`src/server/lib/okta/idx/enroll.ts`](https://github.com/guardian/gateway/blob/bb8b32e30dd178a7ffe81ec75c64b2ce4ad93aeb/src/server/lib/okta/idx/enroll.ts#L38-L46) + +#### Description + +Use the `enroll` endpoint to start the sign up process for a new user. Can be called after the first time after the `introspect` step. Note that this endpoint doesn't actually create the user, it just signals to the IDX API that we're entering the sign up process. + +#### Path + +`POST /idp/idx/enroll` + +#### Body + +```json +{ + "stateHandle": "" +} +``` + +| Parameter | Description | Value | +| ------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------ | +| `stateHandle` | The state handle, used to identify the current state of the interaction code flow. | The `stateHandle` from the [`introspect`](#introspect) step. | + +#### Response + +```json +{ + "version": "1.0.0", + "stateHandle": "", + "expiresAt": "", + "remediation": [ + { + "name": "enroll-profile", + ... + }, + ... + ] +} +``` + +| Parameter | Description | +| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `remediation` | The remediation step, in this case `enroll-profile`, which is the first step in the sign up process. If this is present we can call the [`enroll/new`](#enrollnew) endpoint to attempt to create a new user | + +### `enroll/new` + +#### Implementation + +[`src/server/lib/okta/idx/enroll.ts`](https://github.com/guardian/gateway/blob/bb8b32e30dd178a7ffe81ec75c64b2ce4ad93aeb/src/server/lib/okta/idx/enroll.ts#L125-L134) + +#### Description + +Use the `enroll/new` endpoint to attempt to create a new user. This is the second step in the sign up process, and is called after the `enroll` step. This endpoint will return a `remediation` step which will contain the next steps in the sign up process. + +If a user already exists then the response will be the same as the `enroll` endpoint but also containing the `registration.error.notUniqueWithinOrg` error, and should be handled accordingly. + +#### Path + +`POST /idp/idx/enroll/new` + +#### Body + +```json +{ + "stateHandle": "", + "userProfile": { + "email": "", + "isGuardianUser": true, + "registrationLocation": "", + "registrationPlatform": "" + } +} +``` + +| Parameter | Description | Value | +| ---------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `stateHandle` | The state handle, used to identify the current state of the interaction code flow. | The `stateHandle` from a previous IDX API step. | +| `email` | The email address of the user to sign up | The email address of the user to sign up | +| `isGuardianUser` | Whether the user is a Guardian user, to add them to the `GuardianUser-All` group in Okta | Always set to `true` | +| `registrationLocation` | The optional geographic country where the user is creating the account from | e.g. "United Kingdom", "United States", "Europe", "Other", etc. See [more](https://github.com/guardian/gateway/blob/bb8b32e30dd178a7ffe81ec75c64b2ce4ad93aeb/src/server/lib/getRegistrationLocation.ts#L10-L47) | +| `registrationPlatform` | The optional application where the user is creating the account from | Corresponds to the application name in Okta based on the client ide.g. "android_live_app", "ios_feast_app", "profile" etc. See [more](https://github.com/guardian/gateway/blob/bb8b32e30dd178a7ffe81ec75c64b2ce4ad93aeb/src/server/lib/registrationPlatform.ts#L6-L34) | + +#### Response + +Users will get either the `select-authenticator-enroll` or `enroll-authenticator` remediation step, depending on the current way create account process is setup in Okta. + +Currently it will be set to `select-authenticator-authenticate` remediation step as we manually select the authenticators to use in the create account process. As first we enroll in the `email` authenticator for the user to verify their account, and then we enroll in the `password` authenticator for the user to set a password. + +In the future this might change where we make the `password` authenticator optional. + +**`select-authenticator-authenticate`** response + +```json +{ + "version": "1.0.0", + "stateHandle": "", + "expiresAt": "", + "remediation": [ + { + "name": "select-authenticator-authenticate", + "value": [ + { + "name": "authenticator", + "options": [ + { + "label": "", + "value": { + "form": { + "value": [ + { + "name": "id", + "value": "" + ... + }, + { + "name": "methodType", + "value": "" + ... + } + ] + } + } + }, + ... + ], + ... + }, + { + "name": "stateHandle", + ... + } + ] + ... + }, + ... + ] +} +``` + +| Parameter | Description | +| ---------------------- | ------------------------------------------------------------------------------------------------------------------ | +| `authenticator_label` | The label of the authenticator, e.g. `"Email"` or `"Password"` | +| `authenticator_id` | The ID of the authenticator, used by the `challenge` step to try and authenticate the user with that authenticator | +| `authenticator_method` | The method type of the authenticator, e.g. `"password"` or `"email"` | + +**`enroll-authenticator`** response + +```json +{ + "version": "1.0.0", + "stateHandle": "", + "expiresAt": "", + "remediation": [ + { + "name": "enroll-authenticator", + "value": [ + { + "name": "credentials", + "form": { + "value": [ + { + "name": "passcode", + ... + } + ] + }, + ... + }, + { + "name": "stateHandle", + ... + } + ] + ... + }, + ... + ], + "currentAuthenticator": { + "value": { + "type": "", + ... + } + } +} +``` + +| Parameter | Description | +| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `passcode` | The passcode to authenticate the user with the `challenge/answer` endpoint, e.g. the user's password or a one-time passcode. | +| `authenticator_type` | The type of the authenticator, e.g. `"password"` or `"email"`, the "currentAuthenticator" section can be used to check if it's possible to resend the authenticator email. | + +### `challenge` + +#### Implementation + +[`src/server/lib/okta/idx/challenge.ts`](https://github.com/guardian/gateway/blob/bb8b32e30dd178a7ffe81ec75c64b2ce4ad93aeb/src/server/lib/okta/idx/challenge.ts#L67-L75) + +#### Description + +Use the `challenge` endpoint to authenticate a user with a specified authenticator, currently "email" or "password". This is usually called after [`identity`](#identify) or [`enroll/new`](#enrollnew). + +#### Path + +`POST /idp/idx/challenge` + +#### Body + +```json +{ + "stateHandle": "", + "authenticator": { + "id": "", + "methodType": "" + } +} +``` + +| Parameter | Description | Value | +| -------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `stateHandle` | The state handle, used to identify the current state of the interaction code flow. | The `stateHandle` from a previous IDX API step. | +| `authenticator_id` | The ID of the authenticator, used to authenticate the user with that authenticator. | Use [`findAuthenticatorId`](https://github.com/guardian/gateway/blob/bb8b32e30dd178a7ffe81ec75c64b2ce4ad93aeb/src/server/lib/okta/idx/shared/findAuthenticatorId.ts#L17-L25) to get the id from a given response, remediation name, and expected authenticator type | +| `authenticator_type` | The method type of the authenticator, | `"password"` or `"email"`, depending on the authenticator id | + +#### Response + +```json +{ + "version": "1.0.0", + "stateHandle": "", + "expiresAt": "", + "remediation": [ + { + "name": "challenge-authenticator", + "value": [ + { + "name": "credentials", + "form": { + "value": [ + { + "name": "passcode", + ... + } + ] + }, + ... + }, + { + "name": "stateHandle", + ... + } + ] + ... + }, + { + "name": "select-authenticator-authenticate", + "value": [ + { + "name": "authenticator", + "options": [ + { + "label": "", + "value": { + "form": { + "value": [ + { + "name": "id", + "value": "" + ... + }, + { + "name": "methodType", + "value": "" + ... + } + ] + } + } + }, + ... + ], + ... + }, + { + "name": "stateHandle", + ... + } + ] + ... + }, + ... + ], + "currentAuthenticatorEnrollment": { + "value": { + "type": "", + "": { + ... + } + ... + } + } + ... +} +``` + +| Parameter | Description | +| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| `passcode` | The passcode to authenticate the user with the `challenge/answer` endpoint, e.g. the user's password or a one-time passcode. | +| `authenticator_label` | The label of the authenticator, e.g. `"Email"` or `"Password"` | +| `authenticator_id` | The ID of the authenticator, used by the `challenge` step to try and authenticate the user with that authenticator | +| `authenticator_method` | The method type of the authenticator, e.g. `"password"` or `"email"` | +| `resend` | The resend object, only when "email" authenticator method is selected. Allows us to resend an OTP email to the user | +| `recover` | The recover object, only when "password" authenticator method is selected. Allows us to recover (reset) the user's password | + +### `challenge/answer` + +#### Implementation + +[`src/server/lib/okta/idx/challenge.ts`](https://github.com/guardian/gateway/blob/bb8b32e30dd178a7ffe81ec75c64b2ce4ad93aeb/src/server/lib/okta/idx/challenge.ts#L240-L249) + +#### Description + +Use the `challenge/answer` endpoint to answer the challenge from the `challenge` endpoint, and authenticate the user with the specified authenticator. + +#### Path + +`POST /idp/idx/challenge/answer` + +#### Body + +```json +{ + "stateHandle": "", + "credentials": { + "passcode": "" + } +} +``` + +| Parameter | Description | Value | +| ---------- | ------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- | +| `passcode` | The credential to attempt authenticate the user with the `challenge/answer` endpoint | The user's password (when using "password" authenticator), or a one-time passcode (when using "email" authenticator). | + +#### Response + +If there are any errors then the response will contain a message with the error, and the `remediation` step will contain the same `challenge` step as the response, allowing the user to try again. + +On a success response, one of two things could happen: + +1. The user is fully authenticated and a `CompleteLoginResponse` is returned, with no `remediation`. + - This will contain a `user` object +2. The user hasn't fully authenticated and additional `remediation` steps are returned. + - `select-authenticator-enroll` + - Returned if the user needs to enroll in an additional authenticator after authenticating with the previous one, e.g. the user has verifying with their email and now needs to set a password. + - `reset-authenticator` + - Returned if the user needs to reset an authenticator after validating the previous one, e.g. the user has forgotten their password and needs to reset it, so after verifying with their email they need to reset their password. + - `skip` + - Returned if the user can skip the current step, e.g. the user has verified their email and doesn't want to set a password. Will potentially be used in the future when we make the password authenticator optional. + +**CompleteLoginResponse** + +```json +{ + "version": "1.0.0", + "stateHandle": "", + "expiresAt": "", + "user": { + "value": { + "id": "", + "identifier": "", + "profile": { + ... + } + }, + ... + }, + ... +} +``` + +| Parameter | Description | +| ------------ | ------------------------------------------------ | +| `user_id` | The Okta ID of the user. | +| `user_email` | The email address of the user. | +| `profile` | The user's profile, with default fields included | + +As noted since there is no `remediation` and has a `user` object the user has finished authenticating. To set a global session cookie and finish the Interaction Code flow we have to redirect the user to the [`/login/token/redirect`](#logintokenredirect) endpoint. + +**`select-authenticator-enroll`** response + +Returned when we need to enroll in an additional required authenticator if the user is not already enrolled in it. + +```json +{ + "version": "1.0.0", + "stateHandle": "", + "expiresAt": "", + "remediation": [ + { + "name": "select-authenticator-enroll", + "value": [ + { + "name": "authenticator", + "options": [ + { + "label": "", + "value": { + "form": { + "value": [ + { + "name": "id", + "value": "" + ... + }, + { + "name": "methodType", + "value": "" + ... + } + ] + } + } + }, + ... + ], + ... + }, + { + "name": "stateHandle", + ... + } + ] + ... + }, + ... + ] +} +``` + +| Parameter | Description | +| ---------------------- | -------------------------------------------------------------------- | +| `authenticator_label` | The label of the authenticator, e.g. `"Email"` or `"Password"` | +| `authenticator_id` | The ID of the authenticator that we need to enroll in | +| `authenticator_method` | The method type of the authenticator, e.g. `"password"` or `"email"` | + +**`reset-authenticator`** response + +Returned when we have to set a new password for the user after verifying their email during the [`recover`y](#recover) flow. + +```json +{ + "version": "1.0.0", + "stateHandle": "", + "expiresAt": "", + "remediation": [ + { + "name": "reset-authenticator", + "value": [ + { + "name": "credentials", + "form": { + "value": [ + { + "name": "passcode", + ... + } + ] + }, + ... + }, + { + "name": "stateHandle", + ... + } + ] + ... + }, + ... + ] +} +``` + +| Parameter | Description | +| ---------- | ---------------------------------------------------------------------------------------- | +| `passcode` | The user's new password which we have to call the `challenge/answer` endpoint again with | + +### `challenge/resend` + +#### Implementation + +[`src/server/lib/okta/idx/challenge.ts`](https://github.com/guardian/gateway/blob/bb8b32e30dd178a7ffe81ec75c64b2ce4ad93aeb/src/server/lib/okta/idx/challenge.ts#L268-L276) + +#### Description + +Use the `challenge/resend` endpoint to resend the OTP email to the user, when the user is enrolling/authenticating with the `email` authenticator. Available after the `challenge` step, and when the `challenge/answer` step returns the `resend` object inside the `currentAuthenticatorEnrollment`/`currentAuthenticatorEnrollment` object. + +#### Path + +`POST /idp/idx/challenge/resend` + +#### Body + +```json +{ + "stateHandle": "" +} +``` + +| Parameter | Description | Value | +| ------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | +| `stateHandle` | The state handle, used to identify the current state of the interaction code flow. | The `stateHandle` from a previous IDX API step, where the `resend` object was present in the `currentAuthenticatorEnrollment`. | + +#### Response + +Same as the [`challenge`](#challenge) response. + +### `recover` + +#### Implementation + +[`src/server/lib/okta/idx/recover.ts`](https://github.com/guardian/gateway/blob/bb8b32e30dd178a7ffe81ec75c64b2ce4ad93aeb/src/server/lib/okta/idx/recover.ts#L55-L62) + +#### Description + +Use the `recover` endpoint to start the password recovery process for a user. This is called after the [`identify`](#identify) step, and starting the `password` authenticator [`challenge`](#challenge) step where the `recover` object is present in the `currentAuthenticatorEnrollment` object. + +#### Path + +`POST /idp/idx/recover` + +#### Body + +```json +{ + "stateHandle": "" +} +``` + +| Parameter | Description | Value | +| ------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | +| `stateHandle` | The state handle, used to identify the current state of the interaction code flow. | The `stateHandle` from a previous IDX API step, where the `recover` object was present in the `currentAuthenticatorEnrollment`. | + +#### Response + +```json +{ + "version": "1.0.0", + "stateHandle": "", + "expiresAt": "", + "remediation": [ + { + "name": "authenticator-verification-data", + "value": [ + { + "name": "authenticator", + "label": "", + "form": { + "value": [ + { + "name": "id", + "value": "" + ... + }, + { + "name": "methodType", + "value": "" + ... + } + ] + }, + ... + }, + { + "name": "stateHandle", + ... + } + ] + ... + }, + ... + ] +} +``` + +| Parameter | Description | +| ---------------------- | ----------------------------------------------------------------------------------------------------------------- | +| `authenticator_label` | The label of the authenticator, currently this will just be "Email" | +| `authenticator_id` | The ID of the authenticator, used to authenticate the user with it, currently the id of the "email" authenticator | +| `authenticator_method` | The method type of the authenticator, currently this will just be "email" | + +### `credential/enroll` + +#### Implementation + +[`src/server/lib/okta/idx/credential.ts`](https://github.com/guardian/gateway/blob/bb8b32e30dd178a7ffe81ec75c64b2ce4ad93aeb/src/server/lib/okta/idx/credential.ts#L32-L40) + +#### Description + +Use the `credential/enroll` endpoint to enroll in a new credential, e.g. a password, after authenticating with the `email` authenticator. This usually after `challenge/answer` returns a remediation requesting this. + +#### Path + +`POST /idp/idx/credential/enroll` + +#### Body + +```json +{ + "stateHandle": "", + "authenticator": { + "id": "", + "methodType": "" + } +} +``` + +| Parameter | Description | Value | +| -------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `stateHandle` | The state handle, used to identify the current state of the interaction code flow. | The `stateHandle` from a previous IDX API step. | +| `authenticator_id` | The ID of the authenticator, used to authenticate the user with that authenticator. | Use [`findAuthenticatorId`](https://github.com/guardian/gateway/blob/bb8b32e30dd178a7ffe81ec75c64b2ce4ad93aeb/src/server/lib/okta/idx/shared/findAuthenticatorId.ts#L17-L25) to get the id from a given response, remediation name, and expected authenticator type | +| `authenticator_type` | The method type of the authenticator, | `"password"` or `"email"`, depending on the authenticator id | + +#### Response + +```json +{ + "version": "1.0.0", + "stateHandle": "", + "expiresAt": "", + "remediation": [ + { + "name": "enroll-authenticator", + "value": [ + { + "name": "credentials", + "form": { + "value": [ + { + "name": "passcode", + ... + } + ] + }, + ... + }, + { + "name": "stateHandle", + ... + } + ] + ... + }, + ... + ], + "currentAuthenticator": { + "value": { + "type": "", + "resend": { + ... + } + ... + } + } + ... +} +``` + +| Parameter | Description | +| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `passcode` | The passcode to authenticate the user with the `challenge/answer` endpoint, e.g. the user's password or a one-time passcode. | +| `authenticator_method` | The type of the authenticator, e.g. `"password"` or `"email"`, the "currentAuthenticator" section can be used to check if it's possible to resend the authenticator email, which is only available when enrolling in the "email" factor | + +### `identify/select` + +#### Description + +Used to navigate back to the starting point from the `enroll` or `enroll/new` steps, to allow the user to sign in instead of signing up. + +#### Path + +`POST /idp/idx/identify/select` + +#### Body + +```json +{ + "stateHandle": "" +} +``` + +| Parameter | Description | Value | +| ------------- | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | +| `stateHandle` | The state handle, used to identify the current state of the interaction code flow. | The `stateHandle` from a previous IDX API step, where the user was in the `enroll` or `enroll/new` step and wants to sign in. | + +#### Response + +Same as the initial [`introspect`](#introspect) response, allowing the user to sign in instead of signing up. + +### `skip` + +#### Description + +Allows the user to skip enrollment for an optional authenticator, e.g. the user has verified their email and doesn't want to set a password. May potentially be used in the future when we make the password authenticator optional. + +#### Path + +`POST /idp/idx/skip` + +#### Body + +```json +{ + "stateHandle": "" +} +``` + +| Parameter | Description | Value | +| ------------- | ---------------------------------------------------------------------------------- | ----------------------------------------------- | +| `stateHandle` | The state handle, used to identify the current state of the interaction code flow. | The `stateHandle` from a previous IDX API step. | + +#### Response + +Same as the [`challenge/answer`](#challengeanswer) response, allowing the user to continue with the Interaction Code flow, or will be authenticated. + +### `cancel` + +#### Description + +Allows the user to cancel the current interaction code flow, and return to the starting point. Not used in Gateway, if we want to restart, we just call [`interact`](#interact) again for a new `interaction_handle` followed by [`introspect`](#introspect). + +#### Path + +`POST /idp/idx/cancel` + +#### Body + +```json +{ + "stateHandle": "" +} +``` + +| Parameter | Description | Value | +| ------------- | ---------------------------------------------------------------------------------- | ----------------------------------------------- | +| `stateHandle` | The state handle, used to identify the current state of the interaction code flow. | The `stateHandle` from a previous IDX API step. | + +### `unlock-account` + +#### Description + +Allows the user to unlock their account, if they have been locked out due to too many failed login attempts. Not used in Gateway, as we don't have this feature currently. + +#### Path + +`POST /idp/idx/unlock-account` + +#### Body + +```json +{ + "stateHandle": "" +} +``` + +| Parameter | Description | Value | +| ------------- | ---------------------------------------------------------------------------------- | ----------------------------------------------- | +| `stateHandle` | The state handle, used to identify the current state of the interaction code flow. | The `stateHandle` from a previous IDX API step. | + +### `/login/token/redirect` + +#### Implementation + +[`src/server/lib/okta/idx/shared/idxFetch.ts`](https://github.com/guardian/gateway/blob/bb8b32e30dd178a7ffe81ec75c64b2ce4ad93aeb/src/server/lib/okta/idx/shared/idxFetch.ts#L120-L128) + +#### Description + +The URL that the user should be redirected to after authenticating with the IDX API in order to complete the Interaction Code flow and set a global session cookie (the `idx` cookie). + +This is the last step in the flow once the user has authenticated. This will eventually redirect the user back to the application that they were initially authenticating from. + +#### Path + +`303 See Other GET /idp/idx/login/token/redirect?stateToken={stateToken}` + +| Parameter | Description | Value | +| ------------ | --------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | +| `stateToken` | The state token, used to identify the current state of the interaction code flow. | Derived from the `stateHandle`, and is the `stateHandle` only everything before the first tilde (`stateHandle.split('~')[0]`) | diff --git a/docs/okta/idx/reset-password-idx.md b/docs/okta/idx/reset-password-idx.md new file mode 100644 index 000000000..ce1e725d7 --- /dev/null +++ b/docs/okta/idx/reset-password-idx.md @@ -0,0 +1,94 @@ +# Reset password flow with the Okta IDX API + +This document describes how the reset (forgot) password flow ([`/reset-password`](https://profile.theguardian.com/reset-password)) is implemented using the Okta IDX API. + +See the [IDX API documentation](./idx-api.md) for more information on the API, e.g. to look up the specific endpoints and body used in the create account flow. The flowcharts below only show the expected success and error paths, if there are any unexpected errors, we fall back to the classic Okta API flow where appropriate or show an error. + +## User states + +| Number | Internal Name | State | Description | Action | +| ------ | ----------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 1 | `ACTIVE_EMAIL_PASSWORD` | ACTIVE - "email" + "password" authenticators | Existing users, who have both the "email" and "password" authenticators when calling [`/identify`](./idx-api.md#identify) | Use IDX [`/recover`](./idx-api.md#recover) flow to send passcode via email and set password via the IDX API | +| 2 | `ACTIVE_EMAIL_ONLY` | ACTIVE - "email" authenticator only | Existing users, only "email" authenticator when calling [`/identify`](./idx-api.md#identify), so users who only sign in via a social provider, or don't have a password set (passwordless user) | Set placeholder password for user to force them into above state, and then use IDX [`/recover`](./idx-api.md#recover) flow to send passcode via email and set password via the IDX API | +| 3 | `ACTIVE_PASSWORD_ONLY` | ACTIVE - "password" authenticator only | Existing users, with only the "password" authenticator when calling [`/identify`](./idx-api.md#identify). User managed to set password (through the Okta Classic API flow) without verifying account with passcode (which would have set the "email" authenticator) | We need to set the "email" authenticator first before we allow them to change their password. See [`sendVerifyEmailAuthenticatorIdx` method](https://github.com/guardian/gateway/blob/bb8b32e30dd178a7ffe81ec75c64b2ce4ad93aeb/src/server/controllers/oktaIdxShared.ts#L24-L59) for full details and context. | +| 4 | `NOT_ACTIVE` | non-ACTIVE | Existing users in any other state, e.g. STAGED/PROVISIONED etc. | Force the user into an active state, easiest way to do this would be deactivating, then reactivating a user and setting a placeholder password. See [`forceUserIntoActiveState` method](https://github.com/guardian/gateway/blob/bb8b32e30dd178a7ffe81ec75c64b2ce4ad93aeb/src/server/controllers/oktaIdxShared.ts#L162-L184) for full details and context. | +| 5 | `NON_EXISTENT` | No existing user | The user does not exist in Okta | We show the passcode input email sent page when a user without account attempts to reset password, but send no email. Behaviour on passcode input page is the same as other cases, except submitting in passcode always results in "incorrect code" error | + +## Flowchart + +```mermaid +flowchart TD + start(User visits /reset-password) + start --> enter-email[/User enters email and submits form/] + enter-email --> get-user[GET /api/v1/users/:email] + get-user --> get-user-check{Does user exist?} + get-user-check -- No

Save NON_EXISTENT to state --> email-sent-passcode[/Show email sent page
with passcode input/] + get-user-check -- Yes --> interact[POST /oauth2/auth_server_id/v1/interact] + interact --> introspect[POST /idp/idx/introspect] + introspect --> check-user-state{Check user state} + check-user-state -- ACTIVE --> active-state-identify[POST /idp/idx/identify] + check-user-state -- Non-ACTIVE --> non-active-state[Force user into active state
and get updated user] + non-active-state --> interact + active-state-identify --> active-state-identify-check{Check Response For Authenticators} + active-state-identify-check -- email and password
authenticators

Save ACTIVE_EMAIL_PASSWORD to state --> active-email-password + active-state-identify-check -- email only
authenticator --> active-email-only + active-state-identify-check -- password only
authenticator

Save ACTIVE_PASSWORD_ONLY to state --> active-password-only + active-email-password[POST /idp/idx/challenge
password authenticator] --> recover[POST /idp/idx/recover] + recover --> recover-email-challenge[POST /idp/idx/challenge
email authenticator] + recover-email-challenge --> email-sent-passcode + active-email-only[Set placeholder password using dangerouslySetPlaceholderPassword
This puts user in ACTIVE_EMAIL_PASSWORD state] --> interact + active-password-only[Use sendVerifyEmailAuthenticatorIdx to send
email authenticator verification email
see method for steps/details] --> email-sent-passcode + email-sent-passcode -- Resend Code --> email-sent-passcode-resend[POST /idp/idx/challenge/resend] + email-sent-passcode -- Change email --> start + email-sent-passcode-resend --> email-sent-passcode + email-sent-passcode -- Submit Code --> passcode-check{Check user status
from state} + passcode-check -- NON_EXISTENT
Show incorrect code error --> email-sent-passcode + passcode-check -- ACTIVE_EMAIL_PASSWORD or ACTIVE_PASSWORD_ONLY --> challenge-answer-passcode[POST /idp/idx/challenge/answer] + challenge-answer-passcode --> challenge-answer-passcode-check{Check response} + challenge-answer-passcode-check -- CompleteLoginResponse
Only users in ACTIVE_PASSWORD_ONLY will get this response --> classic-reset + challenge-answer-passcode-check -- Set password response --> password-page[/Show password page
user enters password and submits/] + classic-reset[Generate reset password token
using Okta Classic API] --> password-page + password-page --> password-token-check{Check if using recover token} + password-token-check -- Yes
This is for ACTIVE_PASSWORD_ONLY
users --> classic-reset-password + password-token-check -- No
This is for ACTIVE_EMAIL_PASSWORD
users --> challenge-answer-password[POST /idp/idx/challenge/answer] + challenge-answer-password --> challenge-answer-password-check{Check response} + challenge-answer-password-check -- Success --> login-redirect([303 Redirect /login/token/redirect]) + challenge-answer-password-check -- Invalid Password
e.g. short/long/breached etc.
Show Error --> password-page + login-redirect -- set global session --> finish + classic-reset-password[User Okta Classic API to set password
using reset token] --> classic-reset-password-check{Check response} + classic-reset-password-check -- Invalid Password
e.g. short/long/breached etc.
Show Error --> password-page + classic-reset-password-check -- Success
Set session using existing flow --> finish + finish(User finished resetting
they've redirected back to the application they were on
or password set page is shown) +``` + +## Implementation + +See the [`changePasswordEmailIdx`](https://github.com/guardian/gateway/blob/bb8b32e30dd178a7ffe81ec75c64b2ce4ad93aeb/src/server/controllers/sendChangePasswordEmail.ts#L87-L98) method for the implementation in code to send the user a passcode email for reset password verification, this is called from the [`POST /reset-password`](https://github.com/guardian/gateway/blob/bb8b32e30dd178a7ffe81ec75c64b2ce4ad93aeb/src/server/routes/resetPassword.ts#L44-L46) and [`POST /reset-password/code/resend`](https://github.com/guardian/gateway/blob/bb8b32e30dd178a7ffe81ec75c64b2ce4ad93aeb/src/server/routes/resetPassword.ts#L303-L304) routes. + +The passcode submit route is [`POST /reset-password/code`](https://github.com/guardian/gateway/blob/bb8b32e30dd178a7ffe81ec75c64b2ce4ad93aeb/src/server/routes/resetPassword.ts#L140-L142), and this will redirect to the password page if the user is in the correct state to set a password. + +If the user is in the `ACTIVE_PASSWORD_ONLY` state the password page will be handled by the Okta Classic APIs, specifically [`checkTokenInOkta`](https://github.com/guardian/gateway/blob/bb8b32e30dd178a7ffe81ec75c64b2ce4ad93aeb/src/server/controllers/checkPasswordToken.ts#L197) method when loading the page and [`setPasswordController`](https://github.com/guardian/gateway/blob/bb8b32e30dd178a7ffe81ec75c64b2ce4ad93aeb/src/server/controllers/changePassword.ts#L128) when submitting the password. + +If the user is in the `ACTIVE_EMAIL_PASSWORD` state, which would be for all other users, the password page will be handled by the IDX API, specifically [`oktaIdxApiCheckHandler`](https://github.com/guardian/gateway/blob/bb8b32e30dd178a7ffe81ec75c64b2ce4ad93aeb/src/server/controllers/checkPasswordToken.ts#L109-L126) method when loading the page and [`oktaIdxApiPasswordHandler`](https://github.com/guardian/gateway/blob/b6fd253d1ca0861186b9460d4b8de9362917fd84/src/server/controllers/changePassword.ts#L43-L60) when submitting the password. + +Either way the user will automatically get signed in once the password is set, and redirected back to where they were before the reset password flow. + +### PRs + +Here is a list of pull requests/issues relating to the reset password flow with the Okta IDX API, probably not an exhaustive list: + +- [#2833 - Passwordless | Add additional IDX API endpoints required](https://github.com/guardian/gateway/pull/2833) +- [#2835 - Passwordless | Add `EmailChallengePasscode` email and rendered route](https://github.com/guardian/gateway/pull/2835) +- [#2850 - Passwordless | Query parameter setup for Reset Password with passcodes](https://github.com/guardian/gateway/pull/2850) +- [#2851 - Passwordless | Passcode email sent page changes](https://github.com/guardian/gateway/pull/2851) +- [#2852 - Passwordless | Passcodes for reset password - ACTIVE users](https://github.com/guardian/gateway/pull/2852) +- [#2853 - Passwordless | Refactor and fix some Okta IDX API endpoints](https://github.com/guardian/gateway/pull/2853) +- [#2854 - Passwordless | Refactoring multiple Okta IDX API related things](https://github.com/guardian/gateway/pull/2854) +- [#2865 - Passwordless | Refactoring check/change password handlers](https://github.com/guardian/gateway/pull/2865) +- [#2866 - Passwordless | Update `PasswordUsed` page component + add routes](https://github.com/guardian/gateway/pull/2866) +- [#2881 - Passwordless | `ResetPasswordEmailSentPage` and API refactors](https://github.com/guardian/gateway/pull/2881) +- [#2889 - Passwordless | Passcodes for reset password - ACTIVE users with only "password" authenticator](https://github.com/guardian/gateway/pull/2889) +- [#2891 - Passwordless | YMIAR (Yet more IDX API Refactors)](https://github.com/guardian/gateway/pull/2891) +- [#2902 - Passwordless | Passcodes for reset password - non-ACTIVE users](https://github.com/guardian/gateway/pull/2902) +- [#2915 - Passwordless | Reset password with passcodes handle case for non-existent users](https://github.com/guardian/gateway/pull/2915) +- [#2916 - Passwordless | Make passcodes default for reset password flows](https://github.com/guardian/gateway/pull/2916) diff --git a/docs/okta/idx/sign-in-idx.md b/docs/okta/idx/sign-in-idx.md new file mode 100644 index 000000000..f210351e8 --- /dev/null +++ b/docs/okta/idx/sign-in-idx.md @@ -0,0 +1,127 @@ +# Sign in flow with the Okta IDX API + +This document describes how the sign in flow ([`/signin`](https://profile.theguardian.com/signin)) is implemented using the Okta IDX API. + +This will be split into two parts, one for [sign in with a password](#sign-in-with-password), and the other for [sign in with a one-time passcode](#sign-in-with-one-time-passcode). + +See the [IDX API documentation](./idx-api.md) for more information on the API, e.g. to look up the specific endpoints and body used in the sign in flow. The flowcharts below only show the expected success and error paths, if there are any unexpected errors, we fall back to the classic Okta API flow where appropriate or show an error. + +## Sign in with password + +Previously the sign in with password functionality used the classic [Okta Authentication API](https://developer.okta.com/docs/api/resources/authn) to implement this, specifically the [Primary authentication with public application](https://developer.okta.com/docs/reference/api/authn/#primary-authentication-with-public-applications) operation. + +To match the new Interaction Code flow, we now use the Okta IDX API to implement the sign in with password functionality. From a user's perspective, the sign in flow is the same as before, they enter their email and password, and are redirected back to the application once authenticated, but the backend implementation is different. + +### User States + +| Number | Internal Name | State | Description | Action | +| ------ | ----------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 1 | `ACTIVE_EMAIL_PASSWORD` | ACTIVE - "email" + "password" authenticators | Existing users, who have both the "email" and "password" authenticators when calling [`/identify`](./idx-api.md#identify) | Able to sign in with password using the IDX API. If the user isn't in the `GuardianUser-EmailValidated` group, then we effectively use the [reset password](./reset-password-idx.md#user-states) to validate their account and set a password securely | +| 2 | `ACTIVE_EMAIL_ONLY` | ACTIVE - "email" authenticator only | Existing users, only "email" authenticator when calling [`/identify`](./idx-api.md#identify), so users who only sign in via a social provider, or don't have a password set (passwordless user) | Since these users don't have a password, we just show the "Email and password don’t match" error | +| 3 | `ACTIVE_PASSWORD_ONLY` | ACTIVE - "password" authenticator only | Existing users, with only the "password" authenticator when calling [`/identify`](./idx-api.md#identify). User managed to set password (through the Okta Classic API flow) without verifying account with passcode (which would have set the "email" authenticator) | We authenticate using the IDX API, but before they can finish authentication we have to enroll the user in the "email" authenticator using an OTP, and have set a new password for these users, using how it works for these users in the [reset password](./reset-password-idx.md#user-states) flow to validate their account and set a password securely | +| 4 | `NOT_ACTIVE` | non-ACTIVE | Existing users in any other state, e.g. STAGED/PROVISIONED etc. | Since these users don't have a password, we just show the "Email and password don't match" error | +| 5 | `NON_EXISTENT` | No existing user | The user does not exist in Okta | Since these users don't have an account, we just show the "Email and password don't match" error | + +### Flowchart + +```mermaid +flowchart TD + start(User visits /signin) + start --> enter-email[/User enters email and password
and submits form/] + enter-email --> get-user[GET /api/v1/users/:email] + get-user --> get-user-check{Does user exist and in ACTIVE state?} + get-user-check -- No --> show-error + get-user-check -- Yes --> interact[POST /oauth2/auth_server_id/v1/interact] + interact --> introspect[POST /idp/idx/introspect] + introspect --> identify[POST /idp/idx/identify] + identify --> identify-check{Does user have password authenticator?} + identify-check -- No --> show-error + identify-check -- Yes --> challenge[POST /idp/idx/challenge
password authenticator] + challenge --> challenge-answer[POST /idp/idx/challenge/answer] + challenge-answer --> challenge-answer-check{Check response} + challenge-answer-check -- Authentication Error --> show-error + challenge-answer-check -- Email authenticator
enrollment required --> credential-enroll-email + challenge-answer-check -- Success --> get-user-groups + + credential-enroll-email[POST /idp/idx/credential/enroll
email authenticator] --> security-email-sent + + get-user-groups[GET /api/v1/users/:id/groups] --> get-user-groups-check{Check user groups
GuardianUser-EmailValidated} + get-user-groups-check -- No --> password-reset + get-user-groups-check -- Yes --> login-redirect + + show-error{{Show error:
Email and password don't match}} --> start + + password-reset(Uses password reset flow
to send user a passcode to verify account
and set password) --> security-email-sent + security-email-sent[/Show security email sent page
with passcode input/] --> password-reset-check + password-reset-check(Finish setting password using password reset flow
with a passcode) --> login-redirect + + login-redirect([303 Redirect /login/token/redirect]) -- set global session --> finish + + finish(User finished authenticating
they've redirected back to the application they were on) +``` + +Note: If the user is not in the `GuardianUser-EmailValidated` group, or if they only have the "password" authenticator and we need to enroll them in the "email" authenticator, then we need to validate their email and set a password securely. This functionality effectively uses the [reset password](./reset-password-idx.md) flow to do this. This could be changed in the future to only require the user to validate their email without needing to set a password, specifically when passwordless is enabled and users don't need to have a password set when using a non social provider flow. + +### Implementation + +See the [`oktaIdxApiSignInController` method](https://github.com/guardian/gateway/blob/bb8b32e30dd178a7ffe81ec75c64b2ce4ad93aeb/src/server/controllers/signInControllers.ts#L368-L382) when the user submits the sign in form using the [`POST /signin` route](https://github.com/guardian/gateway/blob/bb8b32e30dd178a7ffe81ec75c64b2ce4ad93aeb/src/server/routes/signIn.ts#L254-L255), but not using passcodes (determined by the `passcode` body parameter not being present/defined). + +### PRs + +Here's a list of PRs/Issues that are related to the sign in with password flow with the Okta IDX API. This may not be conclusive: + +- [#2926 - Passwordless | Use IDX API for Sign In With Password](https://github.com/guardian/gateway/pull/2926) +- [#2931 - Passwordless | Make IDX API default for sign in with password flows!](https://github.com/guardian/gateway/pull/2931) + +## Sign in with one-time passcodes + +Using the IDX API and the Interaction Code flow, we can implement sign in with one-time passcodes (OTPs) for all users to attempt to sign in without a password. This exists alongside the sign in with password flow, and the user is given the option to sign in with a password or a one-time passcode. + +### User States + +| Number | Internal Name | State | Description | Action | +| ------ | ----------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 1 | `ACTIVE_EMAIL_PASSWORD` | ACTIVE - "email" + "password" authenticators | Existing users, who have both the "email" and "password" authenticators when calling [`/identify`](./idx-api.md#identify) | Able to send the user an OTP to sign in with using the IDX API as they have the "email" authenticator | +| 2 | `ACTIVE_EMAIL_ONLY` | ACTIVE - "email" authenticator only | Existing users, only "email" authenticator when calling [`/identify`](./idx-api.md#identify) | Able to send the user an OTP to sign in with using the IDX API as they have the "email" authenticator | +| 3 | `ACTIVE_PASSWORD_ONLY` | ACTIVE - "password" authenticator only | Existing users, with only the "password" authenticator when calling [`/identify`](./idx-api.md#identify) | Can't send OTP straight away to sign in user. We have to enroll the user into the "email" authenticator. We do this by setting a placeholder password, and then sending the user a "email" authenticator verification email with a OTP. When the user uses the passcode we sign them in (no need to set a new password). See [`sendVerifyEmailAuthenticatorIdx` method](https://github.com/guardian/gateway/blob/bb8b32e30dd178a7ffe81ec75c64b2ce4ad93aeb/src/server/controllers/oktaIdxShared.ts#L24-L59) for full details and context. | +| 4 | `NOT_ACTIVE` | non-ACTIVE | Existing users in any other state, e.g. STAGED/PROVISIONED etc. | Force the user into an active state, easiest way to do this would be deactivating, then reactivating a user and setting a placeholder password. See [`forceUserIntoActiveState` method](https://github.com/guardian/gateway/blob/bb8b32e30dd178a7ffe81ec75c64b2ce4ad93aeb/src/server/controllers/oktaIdxShared.ts#L162-L184) for full details and context. Once in `ACTIVE` state, then take once of the `ACTIVE` actions. | +| 5 | `NON_EXISTENT` | No existing user | The user does not exist in Okta | We show the passcode input email sent page when a user without account attempts to sign in with a passcode, but send no email. Behaviour on passcode input page is the same as other cases, except submitting in passcode always results in "incorrect code" error | + +### Flowchart + +```mermaid +flowchart TD + start(User visits /signin) + start --> enter-email[/User enters email
and submits form/] + enter-email --> get-user[GET /api/v1/users/:email] + get-user --> get-user-check{Does user exist?} + get-user-check -- No

Save NON_EXISTENT to state --> email-sent-passcode[/Show email sent page
with passcode input/] + get-user-check -- Yes --> get-user-check-active{Is user in ACTIVE state?} + get-user-check-active -- No --> non-active-state[Force user into active state] + non-active-state --> get-user + get-user-check-active -- Yes --> interact[POST /oauth2/auth_server_id/v1/interact] + interact --> introspect[POST /idp/idx/introspect] + introspect --> identify[POST /idp/idx/identify] + identify --> identify-check{Check user authenticators} + identify-check -- Has email authenticator

Save ACTIVE_EMAIL_PASSWORD or
ACTIVE_EMAIL_ONLY to state --> challenge-email[POST /idp/idx/challenge
email authenticator] + identify-check -- Only password authenticator

Save ACTIVE_PASSWORD_ONLY to state --> active-password-only + active-password-only[Use sendVerifyEmailAuthenticatorIdx to send
email authenticator verification email
see method for steps/details] --> email-sent-passcode + challenge-email --> email-sent-passcode + email-sent-passcode -- Resend code
POST /signin/code/resend --> get-user + email-sent-passcode -- Change email --> start + email-sent-passcode -- Submit code --> passcode-check{Check user status
from state} + passcode-check -- NON_EXISTENT
Show incorrect code error --> email-sent-passcode + passcode-check -- ACTIVE --> challenge-answer[POST /idp/idx/challenge/answer] + challenge-answer --> challenge-answer-check{Check response} + challenge-answer-check -- Authentication Error
Show incorrect code error --> email-sent-passcode + challenge-answer-check -- Success --> get-user-groups + get-user-groups[GET /api/v1/users/:id/groups] --> get-user-groups-check{Check user groups
GuardianUser-EmailValidated} + get-user-groups-check -- No --> set-email-validated-group([Use Okta API to update user
to GuardianUser-EmailValidated group]) --> login-redirect + get-user-groups-check -- Yes --> login-redirect + login-redirect([303 Redirect /login/token/redirect]) -- set global session --> finish + finish(User finished authenticating
they've redirected back to the application they were on) +``` + +### Implementation + +### PRs