From 24a01457caaf454ff872ee0f513491f72bf9eb19 Mon Sep 17 00:00:00 2001 From: Mahesh Makani Date: Wed, 30 Oct 2024 09:50:53 +0000 Subject: [PATCH] chore(docs): bring documentation up to date --- README.md | 2 +- docs/architecture.md | 4 +- docs/dependency-upgrades.md | 20 ++- docs/development.md | 59 +++++- docs/gateway-flows.md | 12 +- docs/okta/login-page-interception.md | 4 +- docs/okta/native-apps-integration-guide.md | 198 +++++---------------- docs/okta/oauth.md | 134 +++++++++++++- docs/okta/okta-authentication-tokens.md | 25 +++ docs/okta/password-reset.md | 2 + docs/okta/signin.md | 2 + docs/okta/tokens.md | 155 ---------------- docs/okta/web-apps-integration-guide.md | 59 +++--- src/server/lib/encryptedStateCookie.ts | 33 ++++ src/server/routes/signIn.ts | 3 +- 15 files changed, 357 insertions(+), 355 deletions(-) create mode 100644 docs/okta/okta-authentication-tokens.md delete mode 100644 docs/okta/tokens.md diff --git a/README.md b/README.md index c4f6bdd53..2bf33f993 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ profile (dot) theguardian (dot) com Gateway is the frontend to sign-in and registration at the Guardian at [profile.theguardian.com](https://profile.theguardian.com). -Need help? Contact the Identity team on [Digital/Identity](https://chat.google.com/room/AAAAFdv9gK8). +Need help? Contact the Identity & Trust team on [P&E/Identity & Trust](https://chat.google.com/room/AAAAFdv9gK8). ## Architecture/Overview diff --git a/docs/architecture.md b/docs/architecture.md index 5c123a85c..38c5846e6 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -18,11 +18,13 @@ Many other client side applications at the Guardian now are React app based, whi Gateway is primarily a [TypeScript](https://www.typescriptlang.org/), [React](https://reactjs.org/), and [Express.js](https://expressjs.com/) application, utilising [the Guardian Source Design System](https://theguardian.design/) components, and [Emotion](https://emotion.sh) CSS-in-JS library for UI/design. We use [Jest](https://jestjs.io/) for unit testing, and [Cypress](https://www.cypress.io/) for integration tests and E2E tests. +We also heavily integrate with [Okta Customer Identity Solution](https://okta.com), who is our Identity backend and provider, and their APIs. For documentation on how we interact with Okta, see the [Okta Documentation](./okta) folder, and specific documentation on the new(er) [Okta IDX API](./okta/idx/README.md). + ## Browser Support **Our core line in the sand is that core functionality and features must NOT require any client-side JavaScript.** To this end Gateway is set up as a completely SSR (Server-side Rendered) React application. -This has the added benefit that since we're only serving HTML, we can target a wider range of browsers compared to doing client side hydration, pretty much any browser thats supports [TLS 1.2](https://caniuse.com/#feat=tls1-2) will get functional support. As far as styling/css go, we target the recommended browsers from [guardian/dotcom-rendering](https://github.com/guardian/dotcom-rendering/blob/master/docs/principles/browser-support.md#recommended-browsers). +This has the added benefit that since we're only serving HTML, we can target a wider range of browsers compared to doing client side hydration, pretty much any browser thats supports [TLS 1.2](https://caniuse.com/#feat=tls1-2) will get functional support. As far as styling/css go, we target the recommended browsers from [guardian/dotcom-rendering](https://github.com/guardian/dotcom-rendering/blob/main/dotcom-rendering/docs/principles/browser-support.md#browser-support-principles). However there will be some JavaScript code that needs to run client side, such as for analytics, consent etc. And hydration can be used to enhance functionality for the user. diff --git a/docs/dependency-upgrades.md b/docs/dependency-upgrades.md index ec8866ec0..cdcb19a02 100644 --- a/docs/dependency-upgrades.md +++ b/docs/dependency-upgrades.md @@ -13,7 +13,25 @@ > Antoine de Saint-Exupéry, _The Little Prince_ We should, if possible, update our NPM packages weekly, for they too will decay -and reveal security vulnerabilities. Here is a general workflow for so doing: +and reveal security vulnerabilities. + +## Automatic dependency upgrades + +Every week on Monday at 08:30 UTC, Dependabot will create a number of PRs for +updating our dependencies. + +These will need to be reviewed and merged. + +In some cases dependabot will not be able to upgrade a package due to tests +failing. In this case, you'll will have to manually review, and potentially +upgrade the dependencies manually. The instructions for this are below. + +## Manual dependency upgrades + +We usually have to do manual dependency upgrades when performing major version +upgrades, or when dependabot is unable to upgrade a package due to failing tests. + +Here is a general workflow for so doing: 1. Dependabot will add some PRs for necessary dependency upgrades to our PRs list. This is your call to action, your batsignal! diff --git a/docs/development.md b/docs/development.md index 3afcecdb3..badb7aa36 100644 --- a/docs/development.md +++ b/docs/development.md @@ -236,6 +236,16 @@ You can export multiple stories from each file, for example to show how the comp ## State Management +Within Gateway state will take one of two forms. + +1. Managing some data/data between the server and client on a per request basis. + - See [Request State Locals and Client State](#request-state-locals-and-client-state) for more information. +2. Managing some user data/data between requests. + - Use query parameters for simple data that needs to persist between requests. + - See [Query Params](#query-params) for more information. + - Use the encrypted state cookie for more complex data that needs to persist between requests. + - See [Encrypted State Cookie](#encrypted-state-cookie) for more information. + ### Request State Locals and Client State Sometimes data is needed by the client to render a specific component, e.g. an error. Using SSR with additional client side hydration we @@ -393,6 +403,10 @@ export const getPersistableQueryParams = (params: QueryParams): PersistableQuery This file also exposes an `addQueryParamsToPath` method which can be used to append query parameters to a given path/string with the correct divider (`?`|`&`). By default it filters out parameters that do not persist from the `QueryParams` object and then turns it into a query string. If you want to include an parameter that doesn't persist, you can manually opt into providing a value as the 3rd argument to the method. +The `parseExpressQueryParams` method in [`src/server/lib/queryParams.ts`](../src/server/lib/queryParams.ts) will parse and validate the query parameters from a request. This will also need to be updated when adding new query parameters, to make sure we only allow the expected parameters are available to use. + +The query params for a given request will be available on the `RequestState` (`res.locals.queryParams`) on the [server](#server), and the `ClientState` on the [client](#client). + #### Server You can access this server side on the `ResponseWithRequestState` object as `res.locals.queryParams`. For example you could get the `returnUrl` using: @@ -467,24 +481,55 @@ const TestComponent = ({ queryString, clientId, error }: Props) => { ``` +### Encrypted State Cookie + +In some cases we need to preserve user data, or user state data between requests in order to be able to modify behaviour of a given request/page. This is done using the encrypted state cookie. + +The type is defined in [`EncryptedState`](../src/shared/model/EncryptedState.ts) interface. This is used to determine what data is stored in the cookie. The `EncryptedState` interface should only include properties that need to persist between requests, and should not include any properties that are only needed for a single request. The data should also be as small as possible, as the cookie has a maximum size limit. + +The data is encrypted and the cookie signed in order to prevent tampering with the data, and prevent it being readable by an actor. The cookie is also set to be HttpOnly, so it cannot be accessed by JavaScript, and Secure, so it can only be sent over HTTPS. + +To set/update the cookie, use the methods in [`src/server/lib/encryptedStateCookie.ts`](../src/server/lib/encryptedStateCookie.ts). The `setEncryptedStateCookie` method is used to set the cookie, and overwrite any existing cookie. The `updateEncryptedStateCookie` method is used to update the cookie, and merge the new data with the existing data. The `clearEncryptedStateCookie` method is used to clear the cookie. Use `readEncryptedStateCookie` to read the cookie and get the data. + +When using the cookie, make sure to remove data from the cookie when it is no longer needed, to avoid the cookie growing too large. + +Example of usage: + +```ts +router.get('/some-route', (req: Request, res: Response) => { + const encryptedState = readEncryptedStateCookie(req); + + // do something with the encrypted state + const email = encryptedState.email; + + // update/set the encrypted state to remove the email, and add a new value + updateEncryptedStateCookie(res, { + email: undefined, + passcodeUsed: true, + }); + + ... +}); +``` + ## Styling Styling is done in JS (or TSX in our case) using the [Emotion](https://emotion.sh) CSS-in-JS library, which allows for the definitions of styles at the component level, which means once rendered, the html sent to the client only contains the CSS required for that page. -It's also used as [the Guardian Source Design System](https://theguardian.design/) components are built using Emotion too, allowing the use for those components in our project. +It's also used as [the Guardian Source Design System](https://theguardian.design/) components are built using Emotion too, allowing the use for those components in our project, through the [`@guardian/source`](https://github.com/guardian/csnx/tree/main/libs/@guardian/source) and [`@guardian/source-development-kitchen`](https://github.com/guardian/csnx/tree/main/libs/%40guardian/source-development-kitchen) packages. Example of styling and adding it to a `p` tag using Emotion and Source: ```tsx import React from 'react'; import { css } from '@emotion/react'; -import { textSans, neutral } from '@guardian/source/foundations'; +import { textSans15 } from '@guardian/source/foundations'; // style the tag using the css string literal const p = css` - color: ${neutral[100]}; + ${textSans15}; + color: var(--color-text); margin: 0; - ${textSans.small()}; `; // example component with the css attribute to add the styling @@ -495,6 +540,12 @@ Try to keep the styling as close to the component as possible to the component b Shared styles used by multiple components can be added to and imported from the [src/client/styles/Shared.ts](../src/client/styles/Shared.ts) file. +Gateway also supports theming, specifically a light and dark mode, which is done through the [`@media (prefers-color-scheme: dark)`](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme) media query. + +The theme is set in the [src/client/styles/Theme.tsx](../src/client/styles/Theme.tsx), and uses CSS variables to define the colours for each theme. The theme is then applied using the [Global Styles](https://emotion.sh/docs/globals) from Emotion, to the [`MinimalLayout`](../src/client/layouts/MinimalLayout.tsx) component, that is used as the base layout for all pages. + +Therefore rather than defining colours directly, it is recommended to use the CSS variables defined in the theme, as shown in the example above. + ## Environment Variables As mentioned in the setup guide, some environment variables are required to start the application. However this section focuses on adding or removing an environment variable. diff --git a/docs/gateway-flows.md b/docs/gateway-flows.md index d5fa7fbbf..106891b6f 100644 --- a/docs/gateway-flows.md +++ b/docs/gateway-flows.md @@ -1,16 +1,20 @@ # Flow diagrams for common Gateway routes -These flow diagrams are a WIP, mostly used to help visualise some complex functions in Gateway for the purposes of mocking Okta flows in testing. +**_Note: These diagrams are outdated and describe behaviour for the older Okta Classic API. For more up to date flows using the newer Okta IDX API, see the [IDX Documentation](./okta/idx/README.md)_** ## Sign in +_Note: See [IDX Sign In](./okta/idx/sign-in-idx.md) for the most up to date flow_ + ```mermaid flowchart TD A[GET /signin] --> B[POST /signin] --> oktaSignInController --> authenticateWithOkta --> getUserGroups --> performAuthorizationCodeFlow ``` -## Register +## Register / Create Account + +_Note: See [IDX Create Account](./okta/idx/create-account-idx.md) for the most up to date flow for new users_ ```mermaid flowchart TD @@ -30,7 +34,7 @@ flowchart TD redirectToEmailSent[302 /register/email-sent] ``` -## Resend registration email +### Resend registration email ```mermaid flowchart TD @@ -41,6 +45,8 @@ flowchart TD ## Reset password +_Note: See [IDX Reset Password](./okta/idx/reset-password-idx.md) for the most up to date flow_ + ```mermaid flowchart TD get[GET /reset-password] --> post[POST /reset-password] --> sendChangePasswordEmailController --> sendEmailInOkta --> getUser --> userFound{User found?} -- Yes --> userStatus{User status?} diff --git a/docs/okta/login-page-interception.md b/docs/okta/login-page-interception.md index ecec0b40c..2ab2f6016 100644 --- a/docs/okta/login-page-interception.md +++ b/docs/okta/login-page-interception.md @@ -4,7 +4,7 @@ When performing the OAuth Authorization Code flow with Okta, while navigating to If they do, then the user will be redirected back to the client app with the `authorization_code` parameter, and the SDK will then exchange this for the OAuth tokens. -If they don't then the user will be prompted to sign in/authenticate. However this is done by Okta showing their own hosted login page, which has a lack of customisation options, and it doesn't provide us full control over the user experience, or doesn't allow us to do specific things during the process. +If they don't, or the `prompt=login` parameter is included in the request, then the user will be prompted to sign in/authenticate. However this is done by Okta showing their own hosted login page, which has a lack of customisation options, and it doesn't provide us full control over the user experience, or doesn't allow us to do specific things during the process. Examples of things we currently do that we can't do with the Okta hosted login page: @@ -52,7 +52,7 @@ opt no existing session - interception happens here note over Browser: Load HTML, execute JS,
redirect to Gateway Browser->>Gateway: Request /signin?fromUri={fromUri}&clientId={clientId}&... Gateway->>Browser: Load /signin?fromUri={fromUri}&clientId={clientId}&... - note over Browser: User sign in with
email+password/social/set password
session set in browser
redirect to fromURI + note over Browser: User authenticates
i.e sign in, create account,
or reset password

Okta session set in browser

Perform redirect to fromURI Browser->>Okta: Request fromUri end Okta->>Browser: Redirect request to app with the `auth_code` parameter
oauth redirect_uri diff --git a/docs/okta/native-apps-integration-guide.md b/docs/okta/native-apps-integration-guide.md index 3fe85cc50..1692bc39b 100644 --- a/docs/okta/native-apps-integration-guide.md +++ b/docs/okta/native-apps-integration-guide.md @@ -6,59 +6,40 @@ To hand control back to the App Native layer from the In-App Browser Tab we reco However the below should be enough to implement the flows. -## Authentication (Sign in, Registration, Reset Password, Set Password) +## Auth(entication/orization) Flow -### Sign In +### Introduction -The approach for both sign in and registration in native apps uses the Okta SDK methods to launch the authentication flow and handle the OAuth2/OIDC flow. From the Identity side, the call to the Okta own login page is intercepted and we redirect to our own login page with the parameters provided from the Okta hosted sign in page. +For authorizing users in native apps, the best current practice is to perform the OAuth authorization request in an external user-agent (typically the browser) rather than an embedded user-agent (such as one implemented with web-views). -The SDKs we're using are the [Okta OIDC iOS SDK](https://github.com/okta/okta-oidc-ios) and [Okta OIDC Android SDK](https://github.com/okta/okta-oidc-android). +Previously, it was common for native apps to use embedded user-agents (commonly implemented with web-views) for OAuth authorization requests. That approach has many drawbacks, including the host app being able to copy user credentials and cookies as well as the user needing to authenticate from scratch in each app. See [Section 8.12 of the RFC 8252](https://datatracker.ietf.org/doc/html/rfc8252#section-8.12) for a deeper analysis of the drawbacks of using embedded user-agents for OAuth. -To initiate login or registration the app will use the SDK to call the relevant method. In Android this is done by calling `.signIn(...)` and in iOS by calling `.signInWithBrowser(...)`, without any additional parameters. The SDK uses this method to launch the Authorization Code Flow with PKCE. It will launch an in-app browser tab (iOS: `ASWebAuthenticationSession`, Android: `CustomTabsService` (Custom Tab)) to check if a user session exists in Okta. In most cases you will want to pass the `prompt=login` parameter to the SDK to ensure that the user is shown the sign in page regardless of if they have an existing session or not. +Native app authorization requests that use the browser are more secure and can take advantage of the user's authentication state. Being able to use the existing authentication session in the browser enables single sign-on, as users don't need to authenticate to the authorization server each time they use a new app (unless required by the authorization server policy). -If a user session exists, Okta will redirect back to the app with an authorization code. The app will then use the SDK to exchange the authorization code for any tokens, usually the `id_token`, `access_token` and `refresh_token`. These tokens are stored in the SDK and can be used to authenticate the user, and authorize the app to access other resources. Once the tokens are received, checking the validity of these tokens is enough to check if the use is signed in, instead of having to perform the full login flow again. +### Implementation -If no user session exists, Okta will attempt to show it's own login page. When the browser attempts to load this page, we perform a JavaScript redirect to our own login page instead with any parameters that are required to complete the Authorization Code flow in the Native App. At this point the user can navigate between sign in and registration. - -If the user signs in successfully, a session cookie will be set in the browser, and we complete sign in by redirecting the user back to the `fromURI` parameter. This will redirect the user to Okta, which will then redirect the user back to the app with the authorization code, and as above this is exchanged for tokens. - -### Registration / Reset Password / Set Password - -Registration works a bit differently. You may not need to implement this in your application as this can all be handled from within a browser, however the user would be asked to sign in again after registering if this approach was taken. +The approach for authentication in native apps uses the Okta SDK methods to launch the authentication flow and handle the OAuth2/OIDC flow. From the Identity side, the call to the Okta own login page is intercepted and we redirect to our own login page with the parameters provided from the Okta hosted sign in page. -After launching the `.signIn(...)`/`.signInWithBrowser(...)` method the user will navigate to the registration page, enter their email, and be sent a registration email. The user will then have to navigate to their inbox, and click the link in the email. The app should intercept this link using the [claimed "https" scheme URIs](https://datatracker.ietf.org/doc/html/rfc8252#section-7.2) (known as "Universal Links"). The token should be extracted from this link, and passed to the SDK `.signIn(...)`/`.signInWithBrowser(...)` method as an additional parameter called `activation_token`. This will again launch the in-app browser to the Okta login page, on that page we check for this `activation_token` parameter and then redirect to our own set password/welcome page. The user will then set their password, get a session set, and we complete sign in by redirecting the user back to the `fromURI` parameter. Again, This will redirect the user to Okta, which will then redirect the user back to the app with the authorization code, and as above this is exchanged for tokens. +We recommend using an SDK to implement the authentication flow: -The link we send to the user will have a format like this: - -```text -https://profile.theguardian.com/welcome/_ - -e.g. for the iOS live app (prefix is `il_`) a token could look like this: - -https://profile.theguardian.com/welcome/il_nF2_qsKfDdPQlGFsEbYn - -where everything after the prefix `il_` is the token. -``` +- Android + - [Okta Mobile SDK for Kotlin](https://github.com/okta/okta-mobile-kotlin) + - _legacy: [Okta OIDC Android SDK](https://github.com/okta/okta-oidc-android)_ +- iOS + - [Okta Mobile SDK for Swift](https://github.com/okta/okta-mobile-swift) + - _legacy: [Okta OIDC iOS SDK](https://github.com/okta/okta-oidc-ios)_ -A similar flow to registration is also required for reset password and set password flows. After launching the `.signIn(...)`/`.signInWithBrowser(...)` method the user will navigate to the reset password page, enter their email, and be sent a reset password or set password email depending on the state of their account. The difference to registration would be the link and the parameter required. The link we send to the user will have a format like this: +To initiate authentication the app will use the SDK to call the relevant method to start web authentication using OpenID Connect redirect. In Kotlin SDK see [here](https://github.com/okta/okta-mobile-kotlin?tab=readme-ov-file#web-authentication-using-oidc-redirect) and in Swift SDK see [here](https://github.com/okta/okta-mobile-swift?tab=readme-ov-file#web-authentication-using-oidc) for information on web authentication. The SDK uses the functionality to launch the Authorization Code Flow with PKCE in a web browser. -```text -# Reset password -https://profile.theguardian.com/reset-password/_ +It will launch an in-app browser tab (iOS: `ASWebAuthenticationSession`, Android: `CustomTabsService` (Custom Tab)) to check if a user session exists in Okta. In **_all_** cases you will want to pass the `prompt=login` parameter to the SDK to ensure that the user is shown the sign in page regardless of if they have an existing session or not. -# Set password -https://profile.theguardian.com/set-password/_ +If a user session exists, Okta will redirect back to the app with an authorization code. The app will then use the SDK to exchange the authorization code for any tokens, usually the `id_token`, `access_token` and `refresh_token`. These tokens are stored in the SDK and can be used to authenticate the user, and authorize the app to access other resources. Once the tokens are received, checking the validity of these tokens is enough to check if the use is signed in, instead of having to perform the full login flow again. See the [token](./oauth.md#oauthoidc-tokens-claims-and-scopes) documentation for more information on the differences between the tokens. -e.g. for the android live app (prefix is `al_`) a token could look like this: - -https://profile.theguardian.com/reset-password/al_nF2_qsKfDdPQlGFsEbYn - -https://profile.theguardian.com/set-password/al_nF2_qsKfDdPQlGFsEbYn +If no user session exists, Okta will attempt to show it's own login page. When the browser attempts to load this page, we perform a JavaScript redirect to our own login page instead with any parameters that are required to complete the Authorization Code flow in the Native App. At this point the user can navigate between sign in and registration. -where everything after the prefix `al_` is the token. -``` +If the user signs in successfully, a session cookie will be set in the browser, and we complete sign in by redirecting the user back to the `fromURI` parameter. This will redirect the user to Okta, which will then redirect the user back to the app with the authorization code, and as above this is exchanged for tokens. -After intercepting these endpoints, like the registration method, the token should be extracted from this link and passed to the SDK `.signIn(...)`/`.signInWithBrowser(...)` method as an additional parameter called `reset_password_token` for reset password and `set_password_token` for set password. This will again launch the in-app browser to the Okta login page, on that page we check for this `reset_password_token`/`set_password_token` parameter and then redirect to our own reset password/set password page. The user will then set their password, get a session set, and we complete sign in by redirecting the user back to the `fromURI` parameter. Again, This will redirect the user to Okta, which will then redirect the user back to the app with the authorization code, and as above this is exchanged for tokens. +The flow now supports sign in, create account, and reset password in a single browser flow thanks to the work done to implement passcodes for create account and reset password so the user doesn't have to rely on clicking on a link. ## Setup @@ -72,6 +53,7 @@ To setup a native app, we will need to register the application as an client wit - This is used by the Okta SDK to redirect back to after calling the logout method in the SDK. - Similar to above we suggest a custom URL scheme for this URI, which we can identify (`your-app:/logout/callback`). - e.g. `com.theguardian.app.oauth:/logout/callback` + - Optional No 1. will be handled by the Okta SDK. @@ -81,143 +63,45 @@ We will need a set for the PROD, CODE, and possibly DEV environments. Once the app is set up within Okta and this project. The Identity team will give you the following information to configure the Okta SDK: -| Name | Key | Explanation | Example | -| ------------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | -| Client ID | `client_id`/`clientId` | The client ID from the app integration that was created | `0ux3rutxocxFX9xyz3t9` | -| Issuer | `discovery_uri`/`issuer` | Domain of the Okta app, followed by the OAuth authorization server. We use a custom authorization server rather than the default one as it lets us customise lifetimes of tokens | `https://profile.theguardian.com/oauth2/aus2qtyn7pS1YsVLs0x7` | -| Logout Redirect URI | `end_session_redirect_uri`/`logoutRedirectUri` | The post-logout redirect URI from the app integration that was created | `com.theguardian.app.oauth:/logout/callback` | -| Redirect URI | `redirect_uri`/`redirectUri` | The Redirect URI from the app integration that was created | `com.theguardian.app.oauth:/authorization/callback` | -| Scopes | `scopes` | Default permissions for the OAuth tokens, you'll want `openid profile offline_access` at the minimum. Information about the scopes can be seen [here](https://developer.okta.com/docs/reference/api/oidc/#scopes) | `openid profile offline_access` or json `["openid", "profile", "offline_access"]` | +| Name | Key | Required | Explanation | Example | +| ------------------- | ---------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | +| Client ID | `client_id`/`clientId` | Yes | The client ID from the app integration that was created | `0ux3rutxocxFX9xyz3t9` | +| Issuer | `discovery_uri`/`issuer` | Yes | Domain of the Okta app, followed by the OAuth authorization server. We use a custom authorization server rather than the default one as it lets us customise lifetimes of tokens | `https://profile.theguardian.com/oauth2/aus2qtyn7pS1YsVLs0x7` | +| Redirect URI | `redirect_uri`/`redirectUri` | Yes | The Redirect URI from the app integration that was created | `com.theguardian.app.oauth:/authorization/callback` | +| Scopes | `scopes` | Yes | Default permissions for the OAuth tokens, you'll want `openid profile offline_access` at the minimum. Information about the scopes can be seen [here](https://developer.okta.com/docs/reference/api/oidc/#scopes) | `openid profile offline_access` or json `["openid", "profile", "offline_access"]` | +| Logout Redirect URI | `end_session_redirect_uri`/`logoutRedirectUri` | Optional | The post-logout redirect URI from the app integration that was created | `com.theguardian.app.oauth:/logout/callback` | The relevant `.well-known` files to be able to handle these redirects within the app. -See [apple-app-site-association](../../src/client/.well-known/apple-app-site-association) for iOS and [assetlinks.json](../../src/client/.well-known/assetlinks.json) for Android to be able to do this. - ## Diagrams -### Sign In - -Much of the following is adopted from [signin.md](signin.md), with changes to fit the native apps implementation. Apps should only need to be aware of what's happening in the native layer, but the full flow is useful for understanding the interaction between the native layer and the identity system. User interaction is implied. +The following shows the behaviour in perspective from the app (NativeLayer), for more specific details on how the [sign in](./idx/sign-in-idx.md), [create account](./idx/create-account-idx.md), or [reset password](./idx/reset-password-idx.md) flows work see the relevant documentation. -To initiate sign-in from the app, you can call this via the Okta SDK using `.signInWithBrowser(from: self)` in iOS or `.signIn(this, null)` in Android. +The app only needs to pay attention to what's happening on the NativeLayer. ```mermaid sequenceDiagram -autonumber participant NativeLayer participant InAppBrowserTab participant Gateway participant Okta -NativeLayer ->> InAppBrowserTab: Call `signin`/`signInWithBrowser`
to perform
Authorization Code Flow with PKCE +NativeLayer->>InAppBrowserTab: Start Authorization Code Flow (with PKCE)
Using Web Authentication using OIDC
functionality from SDK note over InAppBrowserTab: SDK will launch another in-app browser
tab to handle the session check,
and redirect back to the app with auth code -InAppBrowserTab->>Okta: call OAuth /authorize -note over Okta: Existing session check -opt no existing session - Okta->>InAppBrowserTab: Redirect request to /login/login.html - InAppBrowserTab->>Okta: Request /login/login.html - Okta->>InAppBrowserTab: Return /login/login.html - note over InAppBrowserTab: Run JS script
This redirect to /signin or /welcome/:token
with fromURI and clientId params - InAppBrowserTab->>Gateway: Request /signin - Gateway->>InAppBrowserTab: Return /signin - InAppBrowserTab ->> Gateway: Login form POST /signin (email + password) - Gateway ->> Okta: Authenticate with Okta
/authn with email + pw) - note over Okta: validate email + password - Okta ->> Gateway: response with sessionToken
+ email/okta_id - note over Gateway: Perform auth code flow to create
Okta session - Gateway ->> InAppBrowserTab: Redirect request to OAuth auth code flow
/authorize?prompt=none&sessionToken={sessionToken}...
see notes for other parameters/implementation - InAppBrowserTab ->> Okta: Request /authorize - note over Okta: Use sessionToken to create an okta session on
auth subdomain as cookie - Okta ->> InAppBrowserTab: Redirect request to gateway
/oauth/authorization-code/callback?code={auth_code}...
see notes for other parameters/implementation - InAppBrowserTab ->> Gateway: Request to gateway
/oauth/authorization-code/callback?code={auth_code}... - Gateway ->> InAppBrowserTab: Redirect to fromURI parameter
e.g. `/authorize?okta_key={okta_key}` - InAppBrowserTab ->> Okta: Request /authorize?okta_key={okta_key} - note over Okta: Session check
at this point session exists
in all scenarios -end -Okta ->> InAppBrowserTab: Redirect request to app with the `auth_code` parameter
e.g. `com.theguardian.app.oauth:/authorization/callback?auth_code={auth_code}` -InAppBrowserTab ->> NativeLayer: Handle redirect request
in-app browser tab to app
e.g.`com.theguardian.app.oauth:/authorization/callback?auth_code={auth_code}` -NativeLayer ->> Okta: SDK uses auth_code to call OAuth /token to get OAuth tokens -Okta ->> NativeLayer: Return tokens to SDK -note over NativeLayer: the SDK manages the tokens, which can now be used to
authenticate requests, and for checking the user's
session -``` - -### Registration (and (Re)set Password) - -Similar to sign in, but with changes around registration to fit the native apps implementation. Apps should only need to be aware of what's happening in the native layer, but the full flow is useful for understanding the interaction between the native layer and the identity system. User interaction is implied. This flow is very similar for reset and set password. - -On certain applications you might not need to implement the Registration and Reset Password flows, especially as this adds development overhead. It's always possible to register the user on an in-app browser tab and then have the user sign in again. However this is an extra step for the user. - -We also support the scenario where we want to log the user in once they have exchanged the activation_token from their email inbox and set their password, this is the 2nd step of the registration flow. - -To initiate 2nd step of the registration flow from the app, you can call this via the Okta SDK using `.signInWithBrowser(additionalParameters: [activation_token: {activationToken}])` or `.signIn(this, payload)` in Android, where `payload` can be seen [here](https://github.com/okta/okta-oidc-android#sign-in-with-a-browser), with an `.addParameter("activation_token", activationToken)`. - -The start of this flow is very similar to the sign in journey. - -```mermaid -sequenceDiagram -autonumber - -participant NativeLayer -participant InAppBrowserTab -participant Gateway -participant Okta -participant EmailInbox - -NativeLayer ->> InAppBrowserTab: Call `signin`/`signInWithBrowser`
to perform
Authorization Code Flow with PKCE -note over InAppBrowserTab: SDK will launch another in-app browser
tab to handle the session check -InAppBrowserTab ->> Okta: call OAuth /authorize -note over Okta: Existing session check -alt - Okta ->> InAppBrowserTab: Redirect request to /login/login.html - InAppBrowserTab ->> Okta: Request /login/login.html - Okta ->> InAppBrowserTab: Return /login/login.html - note over InAppBrowserTab: Run JS script
This redirect to /signin
with fromURI and clientId params - InAppBrowserTab ->> Gateway: Request /signin - Gateway ->> InAppBrowserTab: Return /signin - InAppBrowserTab ->> Gateway: Request /register - Gateway ->> InAppBrowserTab: Return /register - InAppBrowserTab ->> Gateway: Login form POST /register (email) - Gateway ->> Okta: register with Okta (no password) POST /api/v1/users?activate=false - Okta ->> Gateway: user response object status: STAGED - par Okta setup account - Gateway ->> Okta: activate user POST /api/v1/users/${userId}/lifecycle/activate?sendEmail=true - Okta ->> Gateway: activation token status: PROVISIONED - and - Okta ->> EmailInbox: Activation email - end - Gateway ->> InAppBrowserTab: Redirect to /register/email-sent - InAppBrowserTab ->> Gateway: Request /register/email-sent - Gateway ->> InAppBrowserTab: Return /register/email-sent - note over InAppBrowserTab: At this point the user
has to navigate to the email inbox
and click the activation
link to activate their account.
The app should intercept this link
and handle the activation
process. - EmailInbox ->> InAppBrowserTab: Click link in email /welcome/{prefix}_{activationToken} - InAppBrowserTab ->> NativeLayer: Intercept activation link
in-app browser tab to app - note over NativeLayer: Extract token from link
and use SDK methods - NativeLayer ->> InAppBrowserTab: Call `signin`/`signInWithBrowser`
to perform
Authorization Code Flow with PKCE
pass `activation_token` as additional parameter
or `reset_password_token` for reset password
or `set_password_token` for set password - note over InAppBrowserTab: SDK will launch another in-app browser
tab to handle the session check - InAppBrowserTab ->> Okta: call OAuth /authorize - note over Okta: Existing session check - Okta ->> InAppBrowserTab: Redirect request to /login/login.html - InAppBrowserTab ->> Okta: Request /login/login.html - Okta ->> InAppBrowserTab: Return /login/login.html - note over InAppBrowserTab: Run JS script redirect
/welcome/:token
with fromURI and clientId params - InAppBrowserTab ->> Gateway: Request /welcome/:token - Gateway ->> InAppBrowserTab: Return /welcome/:token - InAppBrowserTab->>Gateway: POST /welcome/{activationToken} with password - Gateway ->> Okta: Set password return session token - note over Gateway: Perform auth code flow to create
Okta session - Gateway ->> InAppBrowserTab: Redirect request to OAuth auth code flow
/authorize?prompt=none&sessionToken={sessionToken}...
see notes for other parameters/implementation - InAppBrowserTab ->> Okta: Request /authorize - note over Okta: Use sessionToken to create an okta session on
auth subdomain as cookie - Okta ->> InAppBrowserTab: Redirect request to gateway
/oauth/authorization-code/callback?code={auth_code}...
see notes for other parameters/implementation - InAppBrowserTab ->> Gateway: Request to gateway
/oauth/authorization-code/callback?code={auth_code}... - Gateway ->> InAppBrowserTab: Redirect to fromURI parameter
e.g. `/authorize?okta_key={okta_key}` - InAppBrowserTab ->> Okta: Request /authorize?okta_key={okta_key} - note over Okta: Session check
at this point session exists
in all scenarios +InAppBrowserTab->>Okta: Call OAuth /authorize?... +note over Okta: Okta global session check +opt no existing session or prompt=login parameter included
this is all handled within Gateway + Okta->>InAppBrowserTab: Return Okta Hosted Login Page + note over InAppBrowserTab: Load HTML, execute JS,
redirect to Gateway + InAppBrowserTab->>Gateway: Request /signin?fromUri={fromUri}&clientId={clientId}&... + Gateway->>InAppBrowserTab: Load /signin?fromUri={fromUri}&clientId={clientId}&... + note over InAppBrowserTab,Gateway: User authenticates i.e sign in, create account,
or reset password

Okta session created + InAppBrowserTab->>Okta: Request fromUri to complete
Authorization Code Flow end Okta ->> InAppBrowserTab: Redirect request to app with the `auth_code` parameter
e.g. `com.theguardian.app.oauth:/authorization/callback?auth_code={auth_code}` InAppBrowserTab ->> NativeLayer: Handle redirect request
in-app browser tab to app
e.g.`com.theguardian.app.oauth:/authorization/callback?auth_code={auth_code}` NativeLayer ->> Okta: SDK uses auth_code to call OAuth /token to get OAuth tokens Okta ->> NativeLayer: Return tokens to SDK -note over NativeLayer: the SDK manages the tokens, which can now be used to
authenticate requests, and for checking the user's
session +note over NativeLayer: the SDK manages the tokens, which can now be used to
authenticate requests, and for checking the user's
session and user data ``` diff --git a/docs/okta/oauth.md b/docs/okta/oauth.md index 095c723e2..a8e13e47a 100644 --- a/docs/okta/oauth.md +++ b/docs/okta/oauth.md @@ -96,11 +96,13 @@ OAuth 2.0 defines a number of flows to get an access token. These flows are call Deciding which one is suited for your case depends mostly on your application type. In most cases the first three are the most suitable. +Okta have also created a **proprietary** extension to the OAuth 2.0 and Open ID Connect 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 grant type. This is not a standard OAuth 2.0 flow, but is specific to Okta. See the [Okta Interaction Code documentation](./idx/README.md#interaction-code-flow) for more information. + ## Gateway Usage Within Gateway we use OAuth 2.0 and OpenID Connect to interact with Okta on a number of user flows. -We have implemented the Authorization Code Flow within this application. This is used in a number of places, including: [sign in](./signin.md), [sign out](./signout.md), change password, etc. +We have implemented the Authorization Code Flow (and the related Interaction Code Flow) within this application. This is used in a number of places, including: [sign in](./idx/sign-in-idx.md), [sign out](./signout.md), change password, etc. We use the Authorization Code Flow to both authorise a user and get an access token, as well as check if the user has an Okta session. @@ -117,3 +119,133 @@ The redirect endpoint (`/oauth/authorization-code/callback`) is defined in [`src This method is also used to retrieve Identity cookies from Identity API as part of the dual running of systems. To do this we get an access token from the authorization server, send this to the Identity API (the "resource server" in this case), which validates the token, and returns signed Identity cookies if so. Once all the checks are complete it will redirect the browser to an appropriate location. + +## OAuth/OIDC Tokens, Claims, and Scopes + +https://developer.okta.com/docs/reference/api/oidc/ + +There are 3 types of tokens that are used by OAuth 2.0 and OpenID Connect through Okta: + +- [Access Token](#access-token) +- [ID Token](#id-token) +- [Refresh Token](#refresh-token) + +The access token and id token are both [JWT](https://jwt.io) tokens, and the refresh token is an opaque string which is an optional token. As a JWT, the access and id tokens contain a header, a body, and a signature. The header contains the algorithm used to sign the token, and the body contains the claims as a key-value pair. The signature is used to verify that the token is valid. + +### Claims + +https://developer.okta.com/docs/reference/api/oidc/#claims + +Tokens issued by Okta contain claims that are statements about a subject (user). For example, the claim can be about a name, identity, key, group, or privilege. The claims in a given token are dependent upon the type of token, the type of credential used to authenticate the user, and the application configuration. + +The claims is generally provided in the body of the access and id tokens, which are key-value pairs. + +The default claims can be seen in the Okta documentation, but it is also possible to add custom claims to the tokens. For example a custom claim could be added to the id token to indicate if the user has a validated email. Custom claims are dependant on the scopes that are requested by the application, and can be customised by the [Identity team in the Okta configuration](https://github.com/guardian/identity-platform/blob/main/okta/terraform/modules/okta/auth_server_claims.tf). + +### Scopes + +https://developer.okta.com/docs/reference/api/oidc/#scopes + +Scopes are used to define what claims are returned in the token. The scopes that are returned in the token are defined by the `scopes` parameter in the request. The `scopes` parameter is a space-delimited list of scopes. The `openid` scope is required to get an id token. The `offline_access` scope is also required to get a refresh token. +Scopes are also used by access tokens to define what actions the user can perform using that particular scope. The list of scopes that are available for a particular user can be found in the `scp` claim of the access token. This way an API can check the `scp` claim to see if the user has the correct permissions to perform the action. + +We are able to limit which scopes a given application has access to through the Okta configuration. + +The default scopes that are available are: + +- `openid` - Identifies the request as an OpenID Connect request. This is the only required scope. +- `profile` - Requests access to the end user's default profile claims. +- `email` - Requests access to the `email` claim. +- `offline_access` - Requests a refresh token used to obtain more access tokens without re-prompting the user for authentication. + +Some scopes that are not currently used by us are: + +- `address` - Requests access to the `address` claim. +- `phone` - Requests access to the `phone_number` claim (not currently used by us). +- `groups` - Requests access to the `groups` claim. +- `device_sso` - Requests a device secret used to obtain a new set of tokens without re-prompting the user for authentication. + +We also define custom scopes which are used to define what actions the application can perform on behalf of the user. These scopes are defined in the [Okta configuration by the identity team](https://github.com/guardian/identity-platform/blob/main/okta/terraform/modules/okta/guardian_auth_server_scopes.tf). We define them in two types: `API Scopes` and `Client Scopes`. + +#### API Scopes + +API scopes are used to control operations and access to user data during communication between a user client and an API which holds the data. The API subdomain refers to the API providing or processing the data. + +The general naming convention for API scopes is: + +`guardian.[....]..[.secure]` + +e.g. + +`guardian.members-data-api.complete.read.self.secure` + +Where: + +- `guardian` - The prefix for all API scopes. +- `apiSubdomain` - The subdomain of API eg `discussion-api` except where there the subdomain isn't particularly clear, eg. `identity-api` instead of `idapi` or where the protected endpoints are logically separate from the rest of the API, eg. `save-for-later` +- `resourceSubtype` - Part of the API that needs more specific access, optional. +- `read|create|update|delete` - The operation that is being performed on the resource. +- `self|all` - Whether the resource is for data belonging to the user or for general access +- `secure` - The API endpoint requires the access token to be additionally verified on the Okta authorization server, usually for more sensitive operations, e.g. updating a users profile, posting a new comment, optional. + +#### Client Scopes + +Client scopes are used to control read access to data consumed by a given client/app, irrespective of the data's origin. The data is returned in the ID Token. For example to return additional claims about the user in the ID Token, e.g. the user's name. + +The general naming convention for client scopes is: + +`id_token..` + +e.g. + +`id_token.profile.ios_live_app` + +Where: + +- `id_token` - The prefix for all client scopes, indicating this should only be consumed in an ID token. +- `dataDomain` - The category/domain of data being returned in the ID token, e.g. 'profile'. +- `clientName` - The name/identifier of the client/app using this scope, e.g. 'ios_live_app'. + +### Access Token + +https://developer.okta.com/docs/reference/api/oidc/#access-token + +The access token is used to authenticate requests to an API, e.g. discussion API, members data api. It is a JWT token that is signed by Okta. The token is valid for 1 hour by default, but can be configured to be longer or shorter, from 5 mins to 24 hours. + +The body of the access token contains claims about the token, as well as some additional ones about the user which the API uses. The default claims can be seen in the documentation, the main one being `scp` which is the list of scopes that the token has access to on behalf of the user. We also provide some custom claims which are helpful in most API calls, namely `legacy_identity_id`, `email`, and `email_validated` claims. + +### ID Token + +https://developer.okta.com/docs/reference/api/oidc/#id-token + +The OpenId Connect standard introduces an additional token to OAuth 2.0, the ID token. The ID token is a JWT token that is signed by Okta. The token is valid for 1 hour by default, and cannot be changed. The ID tokens contains information about an authentication event and claims about the authenticated user. + +The ID token is what applications should use to read information about a given user. We're able to customise the claims that the ID token returns through the Okta configuration and which scopes were used to generate the token. This is dependant per application, but may include things like the user's name, legacy identity id, email, email validation status, and groups. Pretty much anything that is available in the user's profile could be added to the ID token. + +### Refresh Token + +https://developer.okta.com/docs/guides/refresh-tokens/main/ + +Refresh tokens are an opaque string that can be used to obtain new access tokens. This is an optional token which is available through the `offline_access` scope. + +Typically, a user needs a new access token when they attempt to access a resource for the first time or after the previous access token that was granted to them expires. A refresh token is a special token that is used to obtain additional access tokens. This allows you to have short-lived access tokens without having to collect credentials every time one expires, or having to redirect a user through an OAuth flow again. You request a refresh token alongside the access and/or ID tokens as part of a user's initial authentication and authorization flow. Applications must then securely store refresh tokens since they allow users to remain authenticated. + +However, public clients such as browser-based applications have a much higher risk of a refresh token being compromised when a persistent refresh token is used. With clients such as single-page applications (SPAs), long-lived refresh tokens aren't suitable, because there isn't a way to safely store a persistent refresh token in a browser and assure access by only the intended app. These threats are reduced by rotating refresh tokens. Refresh token rotation helps a public client to securely rotate refresh tokens after each use. With refresh token rotation behaviors, a new refresh token is returned each time the client makes a request to exchange a refresh token for a new access token. Refresh token rotation works with SPAs, native apps, and web apps in Okta. + +Refresh tokens are valid for 90 days by default, but can be configured to be longer or shorter, from 5 mins to unlimited, we also recommend that you rotate the refresh token after each use. + +Refresh tokens provide an additional "session" layer on top of the in browser `idx` cookie (which is set after login in browser). Meaning these two sessions are independent of each other, and can be used to manage the user's session in different ways. You can see the [Sessions](sessions.md) documentation for more information on how we use these sessions. + +### Token management + +Managing tokens is a complex topic, and there are many different ways to do it depending on the application. We have a few different approaches depending on the type of application. + +#### Web applications + +In general for web applications we only recommend using access and ID tokens. We don't recommend refresh tokens due to the complexity that arises in being able to manage the refresh token session, as well as difficulty in storing the refresh token securely. + +Depending on the type of web application the storage of tokens will happen in different places. For application which are mostly client side based (e.g. using JS to do user actions) we recommend storing the tokens in local storage. For applications which are mostly server side based (e.g. using a server to do user actions) we recommend storing the tokens in a cookie. + +#### Native applications + +In general for native applications we recommend using all access, ID, and refresh tokens. We recommend using an SDK to manage the tokens, as well as refreshing any tokens that we need to, as the SDK will store the tokens securely in the recommended way for the platform. diff --git a/docs/okta/okta-authentication-tokens.md b/docs/okta/okta-authentication-tokens.md new file mode 100644 index 000000000..8cedc47ea --- /dev/null +++ b/docs/okta/okta-authentication-tokens.md @@ -0,0 +1,25 @@ +# Okta Authentication API Tokens + +Okta's tokens fall into two groups: the standard OAuth and OIDC (Open ID Connect) tokens and the custom tokens that form +part of its Authentication API. These are unrelated to each other and serve different purposes. + +This documentation is on the Okta Authentication API tokens. See [OAuth 2.0 and OpenID connect documentation](./oauth.md#oauthoidc-tokens-claims-and-scopes) for more information on those tokens. + +### Recovery / Activation Tokens + +Single use tokens for performing an account recovery action. They should be distributed out-of-band to a user such as +via email. We currently use these tokens when a user requests to reset their password, or when a new user registers and +is sent an activation email. The tokens are exchanged for state tokens to complete the recovery operation. + +### State token + +An ephemeral token encoding the current state of the recovery operation. To use a concrete example, when a user resets +their password they are sent a recovery token by email. When they click on the link their recovery token is exchanged +for a state token. The state token is then sent on the request to reset the user's password. + +### Session token + +A single-use token which can be exchanged for a login session via the [OIDC & OAuth API](https://developer.okta.com/docs/reference/api/oidc/) +or the [Sessions API](https://developer.okta.com/docs/reference/api/sessions/) + +Documentation on Okta tokens can be found here: https://developer.okta.com/docs/reference/api/authn/#tokens diff --git a/docs/okta/password-reset.md b/docs/okta/password-reset.md index a66336711..a0578773b 100644 --- a/docs/okta/password-reset.md +++ b/docs/okta/password-reset.md @@ -1,5 +1,7 @@ # Resetting passwords and generating password reset tokens in Okta +**_Note: These diagrams are outdated and describe behaviour for the older Okta Classic API. For more up to date flows using the newer Okta IDX API, see the [IDX Documentation](./okta/idx/README.md) and the [IDX Reset Password](./idx/reset-password-idx.md) documentation specifically_** + These are some notes which might be helpful, giving an overview of the ways we reset user passwords in Okta. All reset password operations are asynchronous and two-stage. First, we generate a reset password token within Okta. This is a short-lived token (60 minute expiry), and is emailed to the user. Then, the user posts their new password to the `/reset_password/:token` route, which will check if the token is valid and use the `resetPassword` function, which calls `/api/v1/authn/credentials/reset_password`, to complete the reset password operation. diff --git a/docs/okta/signin.md b/docs/okta/signin.md index 28add6912..bdd2d6f7f 100644 --- a/docs/okta/signin.md +++ b/docs/okta/signin.md @@ -1,5 +1,7 @@ # Sign In with Okta +**_Note: These diagrams are outdated and describe behaviour for the older Okta Classic API. For more up to date flows using the newer Okta IDX API, see the [IDX Documentation](./okta/idx/README.md) and the [IDX Sign In](./idx/sign-in-idx.md) documentation specifically_** + This document describes how we've implemented the sign in flow with Okta in Gateway. There are two parts to this, sign in with email + password, and sign in with social. ## Email + Password diff --git a/docs/okta/tokens.md b/docs/okta/tokens.md deleted file mode 100644 index 115a95943..000000000 --- a/docs/okta/tokens.md +++ /dev/null @@ -1,155 +0,0 @@ -# Tokens - -Okta's tokens fall into two groups: the standard OAuth and OIDC (Open ID Connect) tokens and the custom tokens that form -part of its Authentication API - -## Okta Authentication API Tokens - -### Recovery / Activation Tokens - -Single use tokens for performing an account recovery action. They should be distributed out-of-band to a user such as -via email. We currently use these tokens when a user requests to reset their password, or when a new user registers and -is sent an activation email. The tokens are exchanged for state tokens to complete the recovery operation. - -### State token - -An ephemeral token encoding the current state of the recovery operation. To use a concrete example, when a user resets -their password they are sent a recovery token by email. When they click on the link their recovery token is exchanged -for a state token. The state token is then sent on the request to reset the user's password. - -### Session token - -A single-use token which can be exchanged for a login session via the [OIDC & OAuth API](https://developer.okta.com/docs/reference/api/oidc/) -or the [Sessions API](https://developer.okta.com/docs/reference/api/sessions/) - -Documentation on Okta tokens can be found here: https://developer.okta.com/docs/reference/api/authn/#tokens - -## OAuth/OIDC Tokens, Claims, and Scopes - -https://developer.okta.com/docs/reference/api/oidc/ - -There are 3 types of tokens that are used by OAuth and returned by the Okta SDK. These are: - -- Access Token -- ID Token -- Refresh Token - -The access token and id token are both JWT tokens, and the refresh token is an opaque string which is an optional token. As a JWT, the access and id tokens contain a header, a body, and a signature. The header contains the algorithm used to sign the token, and the body contains the claims as a key-value pair. The signature is used to verify that the token is valid. - -### Claims - -https://developer.okta.com/docs/reference/api/oidc/#claims - -Tokens issued by Okta contain claims that are statements about a subject (user). For example, the claim can be about a name, identity, key, group, or privilege. The claims in a given token are dependent upon the type of token, the type of credential used to authenticate the user, and the application configuration. - -The claims is generally provided in the body of the access and id tokens, which are key-value pairs. - -The default claims can be seen in the Okta documentation, but it is also possible to add custom claims to the tokens. For example a custom claim could be added to the id token to indicate if the user has a validated email. Custom claims are dependant on the scopes that are requested by the application, and can be customised by the [Identity team in the Okta configuration](https://github.com/guardian/identity-platform/blob/main/okta/terraform/modules/okta/auth_server_claims.tf). - -### Scopes - -https://developer.okta.com/docs/reference/api/oidc/#scopes - -Scopes are used to define what claims are returned in the token. The scopes that are returned in the token are defined by the `scopes` parameter in the request. The `scopes` parameter is a space-delimited list of scopes. The `openid` scope is required to get an id token. The `offline_access` scope is also required to get a refresh token. -Scopes are also used by access tokens to define what actions the user can perform using that particular scope. The list of scopes that are available for a particular user can be found in the `scp` claim of the access token. This way an API can check the `scp` claim to see if the user has the correct permissions to perform the action. - -We are able to limit which scopes a given application has access to through the Okta configuration. - -The default scopes that are available are: - -- `openid` - Identifies the request as an OpenID Connect request. This is the only required scope. -- `profile` - Requests access to the end user's default profile claims. -- `email` - Requests access to the `email` claim. -- `offline_access` - Requests a refresh token used to obtain more access tokens without re-prompting the user for authentication. - -Some scopes that are not currently used by us are: - -- `address` - Requests access to the `address` claim. -- `phone` - Requests access to the `phone_number` claim (not currently used by us). -- `groups` - Requests access to the `groups` claim. -- `device_sso` - Requests a device secret used to obtain a new set of tokens without re-prompting the user for authentication. - -We also define custom scopes which are used to define what actions the application can perform on behalf of the user. These scopes are defined in the [Okta configuration by the identity team](https://github.com/guardian/identity-platform/blob/main/okta/terraform/modules/okta/guardian_auth_server_scopes.tf). We define them in two types: `API Scopes` and `Client Scopes`. - -#### API Scopes - -API scopes are used to control operations and access to user data during communication between a user client and an API which holds the data. The API subdomain refers to the API providing or processing the data. - -The general naming convention for API scopes is: - -`guardian.[....]..[.secure]` - -e.g. - -`guardian.members-data-api.complete.read.self.secure` - -Where: - -- `guardian` - The prefix for all API scopes. -- `apiSubdomain` - The subdomain of API eg `discussion-api` except where there the subdomain isn't particularly clear, eg. `identity-api` instead of `idapi` or where the protected endpoints are logically separate from the rest of the API, eg. `save-for-later` -- `resourceSubtype` - Part of the API that needs more specific access, optional. -- `read|create|update|delete` - The operation that is being performed on the resource. -- `self|all` - Whether the resource is for data belonging to the user or for general access -- `secure` - The API endpoint requires the access token to be additionally verified on the Okta authorization server, usually for more sensitive operations, e.g. updating a users profile, posting a new comment, optional. - -#### Client Scopes - -Client scopes are used to control read access to data consumed by a given client/app, irrespective of the data's origin. The data is returned in the ID Token. For example to return additional claims about the user in the ID Token, e.g. the user's name. - -The general naming convention for client scopes is: - -`id_token..` - -e.g. - -`id_token.profile.ios_live_app` - -Where: - -- `id_token` - The prefix for all client scopes, indicating this should only be consumed in an ID token. -- `dataDomain` - The category/domain of data being returned in the ID token, e.g. 'profile'. -- `clientName` - The name/identifier of the client/app using this scope, e.g. 'ios_live_app'. - -### Access Token - -https://developer.okta.com/docs/reference/api/oidc/#access-token - -The access token is used to authenticate requests to an API, e.g. discussion API, members data api. It is a JWT token that is signed by Okta. The token is valid for 1 hour by default, but can be configured to be longer or shorter, from 5 mins to 24 hours. - -The body of the access token contains claims about the token, as well as some additional ones about the user which the API uses. The default claims can be seen in the documentation, the main one being `scp` which is the list of scopes that the token has access to on behalf of the user. We also provide some custom claims which are helpful in most API calls, namely `legacy_identity_id`, `email`, and `email_validated` claims. - -### ID Token - -https://developer.okta.com/docs/reference/api/oidc/#id-token - -The OpenId Connect standard introduces an additional token to OAuth 2.0, the ID token. The ID token is a JWT token that is signed by Okta. The token is valid for 1 hour by default, and cannot be changed. The ID tokens contains information about an authentication event and claims about the authenticated user. - -The ID token is what applications should use to read information about a given user. We're able to customise the claims that the ID token returns through the Okta configuration and which scopes were used to generate the token. This is dependant per application, but may include things like the user's name, legacy identity id, email, email validation status, and groups. Pretty much anything that is available in the user's profile could be added to the ID token. - -### Refresh Token - -https://developer.okta.com/docs/guides/refresh-tokens/main/ - -Refresh tokens are an opaque string that can be used to obtain new access tokens. This is an optional token which is available through the `offline_access` scope. - -Typically, a user needs a new access token when they attempt to access a resource for the first time or after the previous access token that was granted to them expires. A refresh token is a special token that is used to obtain additional access tokens. This allows you to have short-lived access tokens without having to collect credentials every time one expires, or having to redirect a user through an OAuth flow again. You request a refresh token alongside the access and/or ID tokens as part of a user's initial authentication and authorization flow. Applications must then securely store refresh tokens since they allow users to remain authenticated. - -However, public clients such as browser-based applications have a much higher risk of a refresh token being compromised when a persistent refresh token is used. With clients such as single-page applications (SPAs), long-lived refresh tokens aren't suitable, because there isn't a way to safely store a persistent refresh token in a browser and assure access by only the intended app. These threats are reduced by rotating refresh tokens. Refresh token rotation helps a public client to securely rotate refresh tokens after each use. With refresh token rotation behaviors, a new refresh token is returned each time the client makes a request to exchange a refresh token for a new access token. Refresh token rotation works with SPAs, native apps, and web apps in Okta. - -Refresh tokens are valid for 90 days by default, but can be configured to be longer or shorter, from 5 mins to unlimited, we also recommend that you rotate the refresh token after each use. - -Refresh tokens provide an additional "session" layer on top of the in browser `idx` cookie (which is set after login in browser). Meaning these two sessions are independent of each other, and can be used to manage the user's session in different ways. You can see the [Sessions](sessions.md) documentation for more information on how we use these sessions. - -### Token management - -Managing tokens is a complex topic, and there are many different ways to do it depending on the application. We have a few different approaches depending on the type of application. - -#### Web applications - -In general for web applications we only recommend using access and ID tokens. We don't recommend refresh tokens due to the complexity that arises in being able to manage the refresh token session, as well as difficulty in storing the refresh token securely. - -Depending on the type of web application the storage of tokens will happen in different places. For application which are mostly client side based (e.g. using JS to do user actions) we recommend storing the tokens in local storage. For applications which are mostly server side based (e.g. using a server to do user actions) we recommend storing the tokens in a cookie. - -#### Native applications - -In general for native applications we recommend using all access, ID, and refresh tokens. We recommend using an SDK to manage the tokens, as well as refreshing any tokens that we need to, as the SDK will store the tokens securely in the recommended way for the platform. diff --git a/docs/okta/web-apps-integration-guide.md b/docs/okta/web-apps-integration-guide.md index 01c340f1a..2339de5aa 100644 --- a/docs/okta/web-apps-integration-guide.md +++ b/docs/okta/web-apps-integration-guide.md @@ -1,7 +1,5 @@ # Web apps integration with Okta -This is all subject to change following the outcomes of the spike work in web application migration. - ## Context We're moving to use the [OAuth 2.0 Authorization Framework](https://www.rfc-editor.org/rfc/rfc6749) and the [OpenID Connect](https://openid.net/specs/openid-connect-core-1_0.html) (OIDC) protocol for authentication and authorisation. This is a standardised way of authenticating readers and authorising them to access resources. It is used by many other companies, including Google, Facebook, Twitter, Microsoft, and Amazon. @@ -27,14 +25,15 @@ In the integration guide we go over how to perform authentication in both cases OAuth has multiple different ways of authenticating a reader, and for web applications we will only use the [Authorization Code Flow](https://www.rfc-editor.org/rfc/rfc6749#section-4.1), ideally with the [Proof Key for Code Exchange](https://tools.ietf.org/html/rfc7636) (PKCE) extension. This is the most secure way of authenticating a reader, and is the recommended way of authenticating a reader in a web application. -Once the OAuth flow is completed, the application will get two tokens, an access token and an id token. The access token is used to authenticate the reader to an API, and the id token is used to retrieve the reader's profile information within the app. More information about tokens can be found in the [Tokens](tokens.md) document. But in general the following rules apply for each type: +Once the OAuth flow is completed, the application will get two tokens, an access token and an id token. The access token is used to authenticate the reader to an API, and the id token is used to retrieve the reader's profile information within the app. More information about tokens can be found in the [tokens](./oauth.md#oauthoidc-tokens-claims-and-scopes) documentation. But in general the following rules apply for each type: - Access Token - Should only be used to authenticate the reader to an API, and should not be used to read reader profile information directly from the token. - An example of this would be to use the access token to call members-data-api to retrieve the reader's subscription status, or discussion-api to post a comment, etc. -- Id Token +- ID Token + - Should only be used to read a reader profile information, and should not be used to authenticate the reader to an API. - An example of this would be to read the access token to read the readers email, name, etc. to display in the app. @@ -72,18 +71,6 @@ Once the app is set up within Okta and this project. The Identity team will give #### Library Options -~~Can’t use the Okta SDK directly as its super huge even when gzipped (72.7 kB minified + gzipped)~~ - -~~https://bundlephobia.com/package/@okta/okta-auth-js@7.2.0~~ - -~~We’ll have to use a smaller OAuth library, or implement our own with a subset of the functionality required. Also investigate if tree shaking is possible with the Okta SDK.~~ - -~~https://bundlephobia.com/package/oidc-client-ts@2.2.1 (17 kB minified + gzipped) -Open ID compliant, ESM + CommonJS + Browser. Also included is support for reader session and access token management.~~ - -~~https://bundlephobia.com/package/oauth4webapi@2.0.6 (8.1 kB minified + gzipped) -Open ID compliant, ESM only, uses Web APIs. No support for reader session/token management.~~ - In general a library should provide the following features: - “Sign in” the reader to the app @@ -96,9 +83,9 @@ In general a library should provide the following features: - “isSignedIn” method - Reading user claims from ID token -~~A compelling option is to create our own library, which would be a subset of the Okta SDK. This would be a good option if we want to minimise the size of the library, and also have more control over the OAuth flow. We could also make a wrapper around the Okta SDK, which would be a good option if we want to minimise the amount of work required to implement the OAuth flow, and only make available the features we need.~~ +We've created our own library which implements a sub-set of the features that the Okta library provides. It is available in [`@guardian/csnx`](https://github.com/guardian/csnx) repo, specifically the [`@guardian/identity-auth`](https://github.com/guardian/csnx/tree/main/libs/%40guardian/identity-auth) library ([npm](https://www.npmjs.com/package/@guardian/identity-auth)). -We've now created our own library which implements a sub-set of the features that the Okta library provides. It is available in [`@guardian/csnx`](https://github.com/guardian/csnx) repo, specifically the [`@guardian/identity-auth`](https://github.com/guardian/csnx/tree/main/libs/%40guardian/identity-auth) library ([npm](https://www.npmjs.com/package/@guardian/identity-auth)). Our library only uses some of the same classes and methods as the official SDK, but the implementation of them is our own. Currently it only supports the `okta_post_message` `response_mode` parameter, which means that it only gets Access and ID tokens through performing the Authorization Code Flow from within an iframe on the page, this avoids any redirects. +`@guardian/identity-auth` only uses some of the same classes and methods as the official SDK, but the implementation is our own. Currently it only supports the proprietary `okta_post_message` `response_mode` parameter, which means that it only gets Access and ID tokens through performing the Authorization Code Flow from within an iframe on the page, this avoids any redirects. #### OAuth Considerations @@ -126,12 +113,26 @@ Expiry should be set to the minimum required, and refreshed when required, ideal On the server side we have options for multiple libraries and standards for authentication. Since we don’t have to worry about bundle sizes we can use any OAuth/OpenID Connect compliant library. -In this project (Gateway) we already use https://github.com/panva/node-openid-client to generate oauth tokens, which are only used to authenticate with identity API to create the existing cookies for the readers. +There are other options available https://openid.net/developers/certified/ and https://developer.okta.com/code/ if the following libraries don't suit your needs. + +##### Javascript/TypeScript -There are other options available https://openid.net/developers/certified/ and https://developer.okta.com/code/ +In this project (Gateway), and [MMA (manage-frontend)](https://github.com/guardian/manage-frontend/blob/main/server/oauth.ts) we use https://github.com/panva/node-openid-client to perform the Authorization Code Flow to authroise the reader and get the access and id tokens. We also use [`@okta/jwt-verifier`](https://github.com/okta/okta-jwt-verifier-js) in order to verify the JWT access and id tokens on the server side. +##### Scala + +We currently don't have a Scala library to perform the Authorization Code Flow, but an implementation is available in [support-frontend](https://github.com/guardian/support-frontend/blob/main/support-frontend/app/controllers/AuthCodeFlowController.scala#L30). + +The [`identity-auth-core`](https://github.com/guardian/identity/blob/main/identity-auth-core/src/main/scala/com/gu/identity/auth/OktaAuthService.scala#L24C7-L24C22) or [`identity-auth-play`](https://github.com/guardian/identity/blob/main/identity-auth-play/src/main/scala/com/gu/identity/play/OktaPlayAuthService.scala) libraries can be used to verify the JWT access and id tokens on the server side. + +```sbt +"com.gu.identity" %% "identity-auth-core" % "4.12", +// or +"com.gu.identity" %% "identity-auth-play" % "4.12", +``` + #### OAuth Considerations We’d want to use the Authorization Code Flow (optionally with PKCE, but don’t have to as secrets can be stored securely server side). @@ -160,9 +161,9 @@ Refresh should be attempted by going through the Authorization Code flow again, flowchart TD A[Has access/id token and valid?] -A -- Yes --> B[Has `GU_SO` cookie?] -A -- No --> C[has `GU_U` cookie?] -B -- Yes --> D[is `GU_SO` value > `iat` claim on access/id token?] +A -- Yes --> B[Has GU_SO cookie?] +A -- No --> C[has GU_U cookie?] +B -- Yes --> D[is GU_SO value > iat claim on access/id token?] B -- No --> SI[isLoggedIn/maybeLoggedIn] C -- Yes --> SI C -- No --> SO[isSignedOut] @@ -186,6 +187,8 @@ In general the following rules apply: By using this `isLoggedIn`/`maybeLoggedIn` and `isSignedOut` states, we can avoid making unnecessary API calls to Okta to check if the reader is signed in, and not lower our security by making the global session cookie available to all Guardian domains. +This logic has already been implemented within the [`@guardian/identity-auth`](https://github.com/guardian/csnx/tree/main/libs/%40guardian/identity-auth) library, but may have to be re-implemented in other applications. + We already set a `GU_U` cookie which is valid across all Guardian domains, but is not used to take any actions on behalf of the reader (this is performed by the secure, httpOnly, `SC_GU_U` cookie). We can keep using this cookie to determine if the reader is `maybeLoggedIn`. The `GU_U` cookie and the `idx` cookie will lead to the following scenarios: @@ -239,9 +242,7 @@ This gives us two options on how to do "sign in": - Or a link which generates and redirects to the `/authorize` url - This is what's currently happening in some other Guardian system, namely the [Jobs site](https://jobs.theguardian.com) and the [Native apps](native-apps-integration-guide.md). And is usually the approach taken by most things using OAuth. -Our recommendation is to ideally implement both possibilities to cover all cases of sign in. This is because the 2nd option is most "OAuthy" and used by other systems and minimizes complication and redirects, but the 1st option will be required anyway in order to refresh tokens. - -This could be handled by an SDK. +We allow both forms in order to authenticate. This is because the 2nd option is most "OAuthy" and used by other systems and minimizes complication and redirects, but the 1st option will be required anyway to match legacy behaviour. #### Option 1 @@ -307,13 +308,13 @@ note over SDK: the SDK manages the tokens, which can now be used to
authenti ### How would I get a user’s details to display? -The Id and access tokens hold "claims" about the user, essentially a key-value pair of fields which the token "claims" to have about the user, and is only validated through verifying the signature of the token. +The ID and Access tokens hold "claims" about the user, essentially a key-value pair of fields which the token "claims" to have about the user, and is only validated through verifying the signature of the token. Once validated and decoded, the "claims" can be used within the application to read user information. The ID token is what a client should read from in order to read and display user information. The access token is used to make API calls to other services, and should not be used to read user information. -Claims are customisable depending on the scopes and application. We want to keep the number of claims to a minimum so that we don’t give away any additional information about the user outside of what is required by the application. Which is unlike what is possible at the moment, where if a user has an `SC_GU_U` cookie, the application/api can read any/all information about the user. +Claims are customisable depending on the scopes and application. We want to keep the number of claims to a minimum so that we don’t give away any additional information about the user outside of what is required by the application. Which is unlike what is possible at the moment, where if a user has the legacy `SC_GU_U` cookie, the application/api can read any/all information about the user. ### Where should we store the tokens? @@ -321,7 +322,7 @@ As mentioned, hopefully an SDK should handle this, but the following applies. Client side applications: Local storage. -Server side applications: HTTPOnly, Secure cookie, at least `SameSite=Lax`, you could also encrypt the cookie for extra security. +Server side applications: `HTTPOnly`, `Secure` cookie, at least `SameSite=Lax`, you could also encrypt the cookie for extra security. ### How will sign out work? diff --git a/src/server/lib/encryptedStateCookie.ts b/src/server/lib/encryptedStateCookie.ts index 31a15549b..9b9ff846b 100644 --- a/src/server/lib/encryptedStateCookie.ts +++ b/src/server/lib/encryptedStateCookie.ts @@ -17,6 +17,13 @@ const encryptedStateCookieOptions: CookieOptions = { sameSite: 'lax', }; +/** + * @name setEncryptedStateCookie + * @description Set the encrypted state cookie, overwriting any existing data in the cookie should it exist + * @param {Response} res - The express response object + * @param {EncryptedState} state - The state to encrypt and set in the cookie + * @returns {Response} The express response object with the cookie set, usually not needed + */ export const setEncryptedStateCookie = ( res: Response, state: EncryptedState, @@ -50,6 +57,12 @@ export const setEncryptedStateCookie = ( ); }; +/** + * @name getEncryptedStateCookie + * @description Get the encrypted state cookie from the express request by checking the correct cookie source + * @param {Request} req - The express request object + * @returns {string | undefined} The encrypted state cookie or undefined if it doesn't exist + */ const getEncryptedStateCookie = (req: Request): string | undefined => { // eslint-disable-next-line functional/no-let let cookieSource: CookieSource; @@ -68,6 +81,12 @@ const getEncryptedStateCookie = (req: Request): string | undefined => { return req?.[cookieSource]?.[encryptedStateCookieName]; }; +/** + * @name readEncryptedStateCookie + * @description Read the encrypted state cookie from the express request, decrypt it and parse it as JSON, and return it + * @param {Request} req - The express request object + * @returns {EncryptedState | undefined} The decrypted and parsed state or undefined if it doesn't exist + */ export const readEncryptedStateCookie = ( req: Request, ): EncryptedState | undefined => { @@ -89,6 +108,14 @@ export const readEncryptedStateCookie = ( } }; +/** + * @name updateEncryptedStateCookie + * @description Update the encrypted state cookie with the provided state, merging it with the existing state + * @param {Request} req - The express request object + * @param {Response} res - The express response object + * @param {EncryptedState} state - The state to merge with the existing state in the cookie + * @returns {void} Nothing, the cookie is set directly on the response + */ export const updateEncryptedStateCookie = ( req: Request, res: Response, @@ -101,6 +128,12 @@ export const updateEncryptedStateCookie = ( }); }; +/** + * @name clearEncryptedStateCookie + * @description Clear the encrypted state cookie from the express response + * @param {Response} res - The express response object + * @returns {void} Nothing, the cookie is cleared directly on the response + */ export const clearEncryptedStateCookie = (res: Response) => { // Web browsers and other compliant clients will only clear the cookie // if the given options is identical to those given to res.cookie() diff --git a/src/server/routes/signIn.ts b/src/server/routes/signIn.ts index 6f67d2eeb..963ad57ff 100644 --- a/src/server/routes/signIn.ts +++ b/src/server/routes/signIn.ts @@ -82,7 +82,8 @@ export const getErrorMessageFromQueryParams = ( return SignInErrors.PASSCODE_EXPIRED; } - // TODO: we're propagating a generic error message for now until we know what we're doing with the error_description parameter + // We propagate a generic error message when we don't know what the exact error is + // This error will also include a request id, so users can contact us with this information if (error_description) { return SignInErrors.GENERIC; }