diff --git a/aws/cognito/outputs.tf b/aws/cognito/outputs.tf index db9e9c0d6..e8cdd8cfe 100644 --- a/aws/cognito/outputs.tf +++ b/aws/cognito/outputs.tf @@ -2,17 +2,21 @@ output "COGNITO_USER_POOL_ID" { value = aws_cognito_user_pool.nextstrain_dot_org.id } -output "COGNITO_BASE_URL" { - value = format("https://%s", coalesce( - one(aws_cognito_user_pool_domain.custom[*].domain), - "${aws_cognito_user_pool_domain.cognito.domain}.auth.${split("_", aws_cognito_user_pool.nextstrain_dot_org.id)[0]}.amazoncognito.com", - )) +output "OIDC_IDP_URL" { + value = format("https://%s", aws_cognito_user_pool.nextstrain_dot_org.endpoint) } -output "COGNITO_CLIENT_ID" { +output "OAUTH2_CLIENT_ID" { value = aws_cognito_user_pool_client.nextstrain_dot_org.id } -output "COGNITO_CLI_CLIENT_ID" { +output "OAUTH2_CLI_CLIENT_ID" { value = aws_cognito_user_pool_client.nextstrain-cli.id } + +output "OAUTH2_LOGOUT_URL" { + value = format("https://%s/logout", coalesce( + one(aws_cognito_user_pool_domain.custom[*].domain), + "${aws_cognito_user_pool_domain.cognito.domain}.auth.${split("_", aws_cognito_user_pool.nextstrain_dot_org.id)[0]}.amazoncognito.com", + )) +} diff --git a/docs/infrastructure.md b/docs/infrastructure.md index e7fa6b5f1..b995ee248 100644 --- a/docs/infrastructure.md +++ b/docs/infrastructure.md @@ -72,13 +72,23 @@ Several variables are required but obtain defaults from a config file (e.g. `env - `COGNITO_USER_POOL_ID` must be set to the id of the Cognito user pool to use for authentication. - - `COGNITO_BASE_URL` must be set to the URL of the Cognito user pool's hosted UI. - In production, this is `https://login.nextstrain.org`. - In development and testing, this would be something like `https://nextstrain-testing.auth.us-east-1.amazoncognito.com`. + - `OIDC_IDP_URL` must be set to the URL of the Cognito user pool's IdP endpoint. + This is something like `https://cognito-idp.{REGION}.amazonaws.com/{REGION}_{ID}`. - - `COGNITO_CLIENT_ID` must be set to the OAuth2 client id for the nextstrain.org client registered with the Cognito user pool. + - `OAUTH2_CLIENT_ID` must be set to the OAuth2 client id for the nextstrain.org client registered with the Cognito user pool. - - `COGNITO_CLI_CLIENT_ID` must be set to the OAuth2 client id for the Nextstrain CLI client registered with the Cognito user pool. + - `OAUTH2_CLI_CLIENT_ID` must be set to the OAuth2 client id for the Nextstrain CLI client registered with the Cognito user pool. + + - `OAUTH2_LOGOUT_URL` overrides any value discovered via IdP metadata. + For Cognito, which doesn't provide a value via metadata, this must be set to the logout URL of the Cognito user pool's hosted UI. + In production, this is `https://login.nextstrain.org/logout`. + In development and testing, this would be something like `https://nextstrain-testing.auth.us-east-1.amazoncognito.com/logout`. + + - `OIDC_USERNAME_CLAIM` must be set to the field in the id token claims which contains the username for a user. + For Cognito, this is `cognito:username`. + + - `OIDC_GROUPS_CLAIM` must be set to the field in the id token claims which contains the list of group names for a user. + For Cognito, this is `cognito:groups`. Variables in the environment override defaults from the config file. diff --git a/docs/terraform.rst b/docs/terraform.rst index 312d332ee..a7810df86 100644 --- a/docs/terraform.rst +++ b/docs/terraform.rst @@ -262,10 +262,13 @@ Each configuration provides outputs of key-value pairs corresponding to environment (or config) variables required by the nextstrain.org server:: $ terraform output - COGNITO_BASE_URL=https://login.nextstrain.org - COGNITO_CLIENT_ID=rki99ml8g2jb9sm1qcq9oi5n - COGNITO_CLI_CLIENT_ID=2vmc93kj4fiul8uv40uqge93m5 COGNITO_USER_POOL_ID=us-east-1_Cg5rcTged + OIDC_IDP_URL=https://cognito-idp.us-east-1.amazonaws.com/us-east-1_Cg5rcTged + OAUTH2_CLIENT_ID=rki99ml8g2jb9sm1qcq9oi5n + OAUTH2_CLI_CLIENT_ID=2vmc93kj4fiul8uv40uqge93m5 + OAUTH2_LOGOUT_URL=https://login.nextstrain.org/logout + OIDC_USERNAME_CLAIM=cognito:username + OIDC_GROUPS_CLAIM=cognito:groups Outputs are stored and tracked in the remote state and may be updated when applying configuration changes. We cache non-sensitive outputs in JSON config diff --git a/env/outputs.tf b/env/outputs.tf index 9d50c1a08..b71fa2435 100644 --- a/env/outputs.tf +++ b/env/outputs.tf @@ -6,14 +6,26 @@ output "COGNITO_USER_POOL_ID" { value = module.cognito.COGNITO_USER_POOL_ID } -output "COGNITO_BASE_URL" { - value = module.cognito.COGNITO_BASE_URL +output "OIDC_IDP_URL" { + value = module.cognito.OIDC_IDP_URL } -output "COGNITO_CLIENT_ID" { - value = module.cognito.COGNITO_CLIENT_ID +output "OAUTH2_CLIENT_ID" { + value = module.cognito.OAUTH2_CLIENT_ID } -output "COGNITO_CLI_CLIENT_ID" { - value = module.cognito.COGNITO_CLI_CLIENT_ID +output "OAUTH2_CLI_CLIENT_ID" { + value = module.cognito.OAUTH2_CLI_CLIENT_ID +} + +output "OAUTH2_LOGOUT_URL" { + value = module.cognito.OAUTH2_LOGOUT_URL +} + +output "OIDC_USERNAME_CLAIM" { + value = "cognito:username" +} + +output "OIDC_GROUPS_CLAIM" { + value = "cognito:groups" } diff --git a/env/production/config.json b/env/production/config.json index d4da4ce9c..b571c6348 100644 --- a/env/production/config.json +++ b/env/production/config.json @@ -1,6 +1,9 @@ { - "COGNITO_BASE_URL": "https://login.nextstrain.org", - "COGNITO_CLIENT_ID": "rki99ml8g2jb9sm1qcq9oi5n", - "COGNITO_CLI_CLIENT_ID": "2vmc93kj4fiul8uv40uqge93m5", - "COGNITO_USER_POOL_ID": "us-east-1_Cg5rcTged" + "OIDC_IDP_URL": "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_Cg5rcTged", + "OAUTH2_CLIENT_ID": "rki99ml8g2jb9sm1qcq9oi5n", + "OAUTH2_CLI_CLIENT_ID": "2vmc93kj4fiul8uv40uqge93m5", + "OAUTH2_LOGOUT_URL": "https://login.nextstrain.org/logout", + "COGNITO_USER_POOL_ID": "us-east-1_Cg5rcTged", + "OIDC_USERNAME_CLAIM": "cognito:username", + "OIDC_GROUPS_CLAIM": "cognito:groups" } diff --git a/env/testing/config.json b/env/testing/config.json index 6ce4d2a8d..08572fe20 100644 --- a/env/testing/config.json +++ b/env/testing/config.json @@ -1,6 +1,9 @@ { - "COGNITO_BASE_URL": "https://nextstrain-testing.auth.us-east-1.amazoncognito.com", - "COGNITO_CLIENT_ID": "6qiojrhr8tibt0f6hphnm1osp1", - "COGNITO_CLI_CLIENT_ID": "9opa27o74f4jsq8g4a34e1mqr", - "COGNITO_USER_POOL_ID": "us-east-1_zqpCrjM7I" + "OIDC_IDP_URL": "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_zqpCrjM7I", + "OAUTH2_CLIENT_ID": "6qiojrhr8tibt0f6hphnm1osp1", + "OAUTH2_CLI_CLIENT_ID": "9opa27o74f4jsq8g4a34e1mqr", + "OAUTH2_LOGOUT_URL": "https://nextstrain-testing.auth.us-east-1.amazoncognito.com/logout", + "COGNITO_USER_POOL_ID": "us-east-1_zqpCrjM7I", + "OIDC_USERNAME_CLAIM": "cognito:username", + "OIDC_GROUPS_CLAIM": "cognito:groups" } diff --git a/src/authn/index.js b/src/authn/index.js index 3f040ae35..175b0c11a 100644 --- a/src/authn/index.js +++ b/src/authn/index.js @@ -19,7 +19,7 @@ import { JOSEError, JWTClaimValidationFailed, JWTExpired } from 'jose/util/error import partition from 'lodash.partition'; import BearerStrategy from './bearer.js'; import { getTokens, setTokens, deleteTokens } from './session.js'; -import { PRODUCTION, COGNITO_USER_POOL_ID, COGNITO_BASE_URL, COGNITO_CLIENT_ID, COGNITO_CLI_CLIENT_ID } from '../config.js'; +import { PRODUCTION, OIDC_ISSUER_URL, OIDC_JWKS_URL, OAUTH2_AUTHORIZATION_URL, OAUTH2_TOKEN_URL, OAUTH2_LOGOUT_URL, OAUTH2_SCOPES_SUPPORTED, OIDC_USERNAME_CLAIM, OIDC_GROUPS_CLAIM, OIDC_IAT_BACKDATED_BY, OAUTH2_CLIENT_ID, OAUTH2_CLIENT_SECRET, OAUTH2_CLI_CLIENT_ID } from '../config.js'; import { AuthnRefreshTokenInvalid, AuthnTokenTooOld } from '../exceptions.js'; import { fetch } from '../fetch.js'; import { copyCookie } from '../middleware.js'; @@ -56,11 +56,22 @@ const SESSION_SECRET = PRODUCTION const SESSION_MAX_AGE = 30 * 24 * 60 * 60; // 30d in seconds -const COGNITO_REGION = COGNITO_USER_POOL_ID.split("_")[0]; +const OIDC_JWKS = createRemoteJWKSet(new URL(OIDC_JWKS_URL)); -const COGNITO_USER_POOL_URL = `https://cognito-idp.${COGNITO_REGION}.amazonaws.com/${COGNITO_USER_POOL_ID}`; +// These are all scopes defined by OpenID Connect. +const requiredScopes = ["openid", "profile"]; +const optionalScopes = ["email", "offline_access"]; + +const missingScopes = requiredScopes.filter(s => !OAUTH2_SCOPES_SUPPORTED.has(s)); +if (missingScopes.length) { + throw new Error(`OAuth2 IdP does not advertise support for the required scopes: ${Array.from(missingScopes).join(" ")}`); +} + +const OAUTH2_SCOPES = [ + ...requiredScopes, + ...optionalScopes.filter(s => OAUTH2_SCOPES_SUPPORTED.has(s)), +]; -const COGNITO_JWKS = createRemoteJWKSet(new URL(`${COGNITO_USER_POOL_URL}/.well-known/jwks.json`)); /* Registered clients to accept for Bearer tokens. * @@ -68,8 +79,8 @@ const COGNITO_JWKS = createRemoteJWKSet(new URL(`${COGNITO_USER_POOL_URL}/.well- * server start and might want to if we start having third-party clients, but * avoid a start-time dep for now. */ -const BEARER_COGNITO_CLIENT_IDS = [ - COGNITO_CLI_CLIENT_ID, // Nextstrain CLI +const BEARER_OAUTH2_CLIENT_IDS = [ + OAUTH2_CLI_CLIENT_ID, // Nextstrain CLI ]; /* Arbitrary ids for the various strategies for Passport. Makes explicit the @@ -91,10 +102,12 @@ function setup(app) { STRATEGY_OAUTH2, new OAuth2Strategy( { - authorizationURL: `${COGNITO_BASE_URL}/oauth2/authorize`, - tokenURL: `${COGNITO_BASE_URL}/oauth2/token`, - clientID: COGNITO_CLIENT_ID, + authorizationURL: OAUTH2_AUTHORIZATION_URL, + tokenURL: OAUTH2_TOKEN_URL, + clientID: OAUTH2_CLIENT_ID, callbackURL: "/logged-in", + scope: OAUTH2_SCOPES, + clientSecret: OAUTH2_CLIENT_SECRET, pkce: true, state: true, passReqToCallback: true, @@ -110,9 +123,7 @@ function setup(app) { // All users are ok, as we control the entire user pool. return done(null, user); } catch (e) { - return e instanceof JOSEError - ? done(null, false, "Error verifying token") - : done(e); + return done(e); } } ) @@ -127,7 +138,7 @@ function setup(app) { }, async (idToken, done) => { try { - const user = await userFromIdToken(idToken, BEARER_COGNITO_CLIENT_IDS); + const user = await userFromIdToken(idToken, BEARER_OAUTH2_CLIENT_IDS); return done(null, user); } catch (e) { if (e instanceof JOSEError) { @@ -433,10 +444,10 @@ function setup(app) { app.route("/logout").get((req, res) => { req.session.destroy(() => { const params = { - client_id: COGNITO_CLIENT_ID, + client_id: OAUTH2_CLIENT_ID, logout_uri: req.context.origin }; - res.redirect(`${COGNITO_BASE_URL}/logout?${querystring.stringify(params)}`); + res.redirect(`${OAUTH2_LOGOUT_URL}?${querystring.stringify(params)}`); }); }); } @@ -504,8 +515,8 @@ function authnWithSession(req, res, next) { /** - * Creates a user record from the given `idToken` after verifying it and the - * associated `accessToken`. + * Creates a user record from the given `idToken` after verifying it and + * ensuring the associated `accessToken` and `refreshToken` exist. * * Use this function instead of {@link userFromIdToken} if you have all three * tokens (e.g. after initial OAuth2 login or session token renewal), as it @@ -514,7 +525,7 @@ function authnWithSession(req, res, next) { * @param {String} idToken * @param {String} accessToken * @param {String} refreshToken - * @param {String|String[]} client. Optional. Passed to `verifyToken()`. + * @param {String|String[]} client. Optional. Passed to `verifyIdToken()`. * @returns {User} User record with e.g. `username` and `groups` keys. */ async function userFromTokens({idToken, accessToken, refreshToken}, client = undefined) { @@ -522,11 +533,35 @@ async function userFromTokens({idToken, accessToken, refreshToken}, client = und if (!accessToken) throw new Error("missing accessToken"); if (!refreshToken) throw new Error("missing refreshToken"); - /* Verify access token for good measure, but pull user information from the - * identity token (which is its intended purpose). Note that refresh tokens - * are opaque blobs not subject to verification. + /* Pull user information from the OIDC identity token, which is its intended + * purpose. + * + * The OAuth2 access token¹ is for accessing other resources protected by the + * IdP, such as the OIDC "user info" endpoint², but we don't currently use + * it. It is sometimes verifiable, depending on the IdP and authentication + * flow, but not necessary (nor always possible) to do so in our use.³ We + * treat it thus as an opaque blob, as suggested by the OAuth2 spec.¹ + * + * Note that OAuth2 refresh tokens are opaque blobs never subject to + * verification. + * -trs, 12 Oct 2023 + * + * ¹ + * + * ² + * + * + * ³ AWS Cognito documents its access tokens as JWTs and provides details for + * verifying them. Azure AD's are also JWTs, but various details make + * generalized verification difficult (e.g. issuer is not the IdP we + * authenticate with). Neither provides the OIDC "at_hash" claims in the + * id token that would let us treat the paired access token as an opaque + * but verifiable blob, as we're using the authorization code flow⁴ which + * does not require it.⁵ + * + * ⁴ + * ⁵ */ - await verifyToken(accessToken, "access", client); // Verifies idToken return await userFromIdToken(idToken, client); @@ -537,16 +572,16 @@ async function userFromTokens({idToken, accessToken, refreshToken}, client = und * Creates a user record from the given `idToken` after verifying it. * * @param {String} idToken - * @param {String|String[]} client. Optional. Passed to `verifyToken()`. + * @param {String|String[]} client. Optional. Passed to `verifyIdToken()`. * @returns {User} User record with e.g. `username` and `groups` keys. */ async function userFromIdToken(idToken, client = undefined) { - const idClaims = await verifyToken(idToken, "id", client); + const idClaims = await verifyIdToken(idToken, client); - const {groups, authzRoles, flags, cognitoGroups} = parseCognitoGroups(idClaims["cognito:groups"] || []); + const {groups, authzRoles, flags, cognitoGroups} = parseCognitoGroups(idClaims[OIDC_GROUPS_CLAIM] || []); const user = { - username: idClaims["cognito:username"], + username: idClaims[OIDC_USERNAME_CLAIM], groups, authzRoles, flags, @@ -629,37 +664,47 @@ function splitGroupRole(cognitoGroup) { /** - * Verifies all aspects of the given `token` (a signed JWT from our AWS Cognito - * user pool) which is expected to be used for the given `use`. + * Verifies all aspects of the given OIDC `idToken` (a signed JWT from our AWS + * Cognito user pool). * * Assertions about expected algorithms, audience, issuer, and token use follow * guidelines from * . * - * @param {String} token - * @param {String} use + * @param {String} idToken * @param {String} client. Optional `client_id` or list of `client_id`s - * expected for the token. Only relevant when `use` is not - * `access`. Defaults to this server's client id. + * expected for the token. Defaults to this server's client id. * @returns {Object} Verified claims from the token's payload */ -async function verifyToken(token, use, client = COGNITO_CLIENT_ID) { - const {payload: claims} = await jwtVerify(token, COGNITO_JWKS, { +async function verifyIdToken(idToken, client = OAUTH2_CLIENT_ID) { + const {payload: claims} = await jwtVerify(idToken, OIDC_JWKS, { algorithms: ["RS256"], - issuer: COGNITO_USER_POOL_URL, - audience: use !== "access" ? client : null, + issuer: OIDC_ISSUER_URL, + audience: client, }); + /* AWS Cognito includes the kind of token, id or access, in the claims for + * each token itself, e.g. so if you're expecting id tokens you can verify + * someone handed you an id token and not an access token. This presumably + * helps block token misuse attacks, e.g. when code that's expecting an id + * token is given an access token that looks close enough in terms of claims + * but ends up breaking unasserted expections of the code and allowing an + * authz bypass or privilege escalation. + * + * There is not, AFAICT, a standard claim for this information in OIDC, and + * other IdPs don't provide it, so we only check this claim if it exists. + * -trs, 11 Oct 2023 + */ const claimedUse = claims["token_use"]; - if (claimedUse !== use) { + if (claimedUse !== undefined && claimedUse !== "id") { throw new JWTClaimValidationFailed(`unexpected "token_use" claim value: ${claimedUse}`, "token_use", "check_failed"); } /* Verify the token was issued at (iat) a time more recent than our staleness * horizon for the user. */ - const username = claims[{id: "cognito:username", access: "username"}[claimedUse]]; + const username = claims[OIDC_USERNAME_CLAIM]; if (!username) { throw new JWTClaimValidationFailed("missing username"); @@ -671,8 +716,8 @@ async function verifyToken(token, use, client = COGNITO_CLIENT_ID) { if (typeof claims.iat !== "number") { throw new JWTClaimValidationFailed(`"iat" claim must be a number`, "iat", "invalid"); } - if (claims.iat < staleBefore) { - throw new AuthnTokenTooOld(`"iat" claim less than user's staleBefore: ${claims.iat} < ${staleBefore}`); + if (claims.iat + OIDC_IAT_BACKDATED_BY < staleBefore) { + throw new AuthnTokenTooOld(`"iat" claim (plus any backdating) less than user's staleBefore: ${claims.iat} + ${OIDC_IAT_BACKDATED_BY} < ${staleBefore}`); } } @@ -693,11 +738,16 @@ async function verifyToken(token, use, client = COGNITO_CLIENT_ID) { * except by initiating a new login. */ async function renewTokens(refreshToken) { - const response = await fetch(`${COGNITO_BASE_URL}/oauth2/token`, { + const response = await fetch(OAUTH2_TOKEN_URL, { method: "POST", body: new URLSearchParams([ ["grant_type", "refresh_token"], - ["client_id", COGNITO_CLIENT_ID], + ["client_id", OAUTH2_CLIENT_ID], + ...( + OAUTH2_CLIENT_SECRET + ? [["client_secret", OAUTH2_CLIENT_SECRET]] + : [] + ), ["refresh_token", refreshToken], ]), }); diff --git a/src/config.js b/src/config.js index a5272fcb3..f17284b2e 100644 --- a/src/config.js +++ b/src/config.js @@ -10,6 +10,7 @@ * @module config */ import { readFile } from 'fs/promises'; +import fetch from 'node-fetch'; import path, { dirname } from 'path'; import process from 'process'; import { fileURLToPath } from 'url'; @@ -54,13 +55,16 @@ const configFile = CONFIG_FILE /** * Obtain a configuration variable from the environment or the config file. * + * Values obtained from the environment will be deserialized from JSON if + * possible. + * * @param {string} name - Variable name, e.g. "COGNITO_USER_POOL_ID" * @param {any} default - Final fallback value * @throws {Error} if no value is found and default is undefined */ const fromEnvOrConfig = (name, default_) => { const value = - process.env[name] + maybeJSON(process.env[name]) || configFile?.[name]; if (!value && default_ === undefined) { @@ -70,6 +74,23 @@ const fromEnvOrConfig = (name, default_) => { }; +/** + * Deserialize a value that might be JSON, passing it thru if it isn't. + * + * @param {any} x - Value which might be JSON + */ +function maybeJSON(x) { + if (typeof x === "string") { + try { + return JSON.parse(x); + } catch (e) { + // no worries + } + } + return x; +} + + /** * Id of our Cognito user pool. * @@ -81,31 +102,115 @@ export const COGNITO_USER_POOL_ID = fromEnvOrConfig("COGNITO_USER_POOL_ID"); /** - * Base URL (i.e. origin) of our Cognito user pool's hosted UI and OAuth2 - * endpoints. + * URL of the OIDC IdP for our user directory (e.g. our Cognito user pool's + * hosted UI and OAuth2 endpoints). * * @type {string} */ -export const COGNITO_BASE_URL = fromEnvOrConfig("COGNITO_BASE_URL"); +export const OIDC_IDP_URL = fromEnvOrConfig("OIDC_IDP_URL"); + + +/** + * OpenID Connect (OIDC) identity provider configuration document. + * + * Typically this is unspecified in the environment or config file and instead + * populated by fetching it from the OIDC IdP. + * + * Refer to the spec for the + * {@link https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata core metadata fields} + * and the + * {@link https://www.iana.org/assignments/oauth-parameters/oauth-parameters.xhtml#authorization-server-metadata IANA metadata field registry} + * for references to other fields. + * + * @type {object} + */ +export const OIDC_CONFIGURATION = fromEnvOrConfig("OIDC_CONFIGURATION", await oidcConfiguration(OIDC_IDP_URL)); + +async function oidcConfiguration(idpUrl) { + const url = `${idpUrl}/.well-known/openid-configuration`; + const response = await fetch(url); + + if (!response.ok) { + console.warn(`Failed to fetch ${url}: ${response.status} ${response.statusText}: ${await response.text()}`); + return; + } + return await response.json(); +} + +/** "issuer" metadata field in */ +export const OIDC_ISSUER_URL = fromEnvOrConfig("OIDC_ISSUER_URL", OIDC_CONFIGURATION.issuer); + +/** "jwks_uri" metadata field in OIDC configuration */ +export const OIDC_JWKS_URL = fromEnvOrConfig("OIDC_JWKS_URL", OIDC_CONFIGURATION.jwks_uri); + +/** "authorization_endpoint" metadata field in OIDC configuration */ +export const OAUTH2_AUTHORIZATION_URL = fromEnvOrConfig("OAUTH2_AUTHORIZATION_URL", OIDC_CONFIGURATION.authorization_endpoint); + +/** "token_endpoint" metadata field in OIDC configuration */ +export const OAUTH2_TOKEN_URL = fromEnvOrConfig("OAUTH2_TOKEN_URL", OIDC_CONFIGURATION.token_endpoint); + +/** "end_session_endpoint" metadata field in OIDC configuration with RP-initiated logout support {@link https://openid.net/specs/openid-connect-rpinitiated-1_0.html#OPMetadata} */ +export const OAUTH2_LOGOUT_URL = fromEnvOrConfig("OAUTH2_LOGOUT_URL", OIDC_CONFIGURATION.end_session_endpoint); + +/** "scopes_supported" metadata field in OIDC configuration */ +export const OAUTH2_SCOPES_SUPPORTED = new Set(fromEnvOrConfig("OAUTH2_SCOPES_SUPPORTED", OIDC_CONFIGURATION.scopes_supported)); /** - * OAuth2 client id of nextstrain.org server as registered with our Cognito - * user pool. + * OAuth2 client id of nextstrain.org server as registered with our IdP (e.g. + * our Cognito user pool). * * @type {string} */ -export const COGNITO_CLIENT_ID = fromEnvOrConfig("COGNITO_CLIENT_ID"); +export const OAUTH2_CLIENT_ID = fromEnvOrConfig("OAUTH2_CLIENT_ID"); /** - * OAuth2 client id of Nextstrain CLI as registered with our Cognito user pool. + * Optional OAuth2 client secret corresponding to the {@link OAUTH2_CLIENT_ID}. + * + * @type {string} + */ +export const OAUTH2_CLIENT_SECRET = fromEnvOrConfig("OAUTH2_CLIENT_SECRET", null); + + +/** + * OAuth2 client id of Nextstrain CLI as registered with our IdP (e.g. Cognito + * user pool). * * Used to identify its tokens provided via Bearer auth. * * @type {string} */ -export const COGNITO_CLI_CLIENT_ID = fromEnvOrConfig("COGNITO_CLI_CLIENT_ID"); +export const OAUTH2_CLI_CLIENT_ID = fromEnvOrConfig("OAUTH2_CLI_CLIENT_ID"); + + +/** + * ID token claim field containing the username for a user. + * + * @type {string} + */ +export const OIDC_USERNAME_CLAIM = fromEnvOrConfig("OIDC_USERNAME_CLAIM"); + + +/** + * ID token claim field containing the list of role group names for a user. + * + * @type {string} + */ +export const OIDC_GROUPS_CLAIM = fromEnvOrConfig("OIDC_GROUPS_CLAIM"); + + +/** + * Fixed time (in seconds) by which the IdP backdates the "iat" claim of the ID + * token. + * + * @type {number} + */ +export const OIDC_IAT_BACKDATED_BY = fromEnvOrConfig("OIDC_IAT_BACKDATED_BY", 0); + +if (typeof OIDC_IAT_BACKDATED_BY !== 'number') { + throw new Error(`OIDC_IAT_BACKDATED_BY value is not a number; got "${OIDC_IAT_BACKDATED_BY}"`); +} /**