From 895d3088ab883ec4f3e4fdd94f06a9608c832d6e Mon Sep 17 00:00:00 2001 From: Thomas Sibley Date: Sun, 19 Nov 2023 23:42:40 -0800 Subject: [PATCH] Support for Nextstrain CLI's new means of authentication with IdPs Nextstrain CLI will start using OIDC/OAuth2's authorization code flow to interact with not just AWS Cognito but other IdPs as well (i.e. as used in other deployments of nextstrain.org). To support this without hardcoding or onerous user-side configuration, Nextstrain CLI will start using the standard OIDC configuration endpoint, /.well-known/openid-configuration, to auto-discover necessary configuration about both the IdP to talk to and the client it should be. Terraform changes are deployed to both production and testing as they're additive and will not impact current CLI auth flow. Related-to: --- aws/cognito/clients.tf | 43 +++++++++++- aws/cognito/outputs.tf | 4 ++ docs/production.rst | 34 ++++++++-- env/outputs.tf | 4 ++ env/production/.terraform.lock.hcl | 19 ++++++ env/production/config.json | 101 +++++++++++++++++++++++++++++ env/testing/.terraform.lock.hcl | 19 ++++++ env/testing/config.json | 100 ++++++++++++++++++++++++++++ src/app.js | 9 +++ src/config.js | 35 ++++++++++ src/endpoints/index.js | 2 + src/endpoints/openid.js | 68 +++++++++++++++++++ 12 files changed, 431 insertions(+), 7 deletions(-) create mode 100644 src/endpoints/openid.js diff --git a/aws/cognito/clients.tf b/aws/cognito/clients.tf index ff710d133..2a5324433 100644 --- a/aws/cognito/clients.tf +++ b/aws/cognito/clients.tf @@ -80,7 +80,15 @@ resource "aws_cognito_user_pool_client" "nextstrain-cli" { name = "nextstrain-cli" - # Allow Secure Remote Password (SRP) auth, plus refresh token auth (required). + # Allow client to use OAuth (with the authorization code grant type only) + # against the user pool, plus Secure Remote Password (SRP) auth and refresh + # token auth (required). + allowed_oauth_flows_user_pool_client = true + allowed_oauth_flows = ["code"] + allowed_oauth_scopes = ["email", "openid", "phone", "profile"] + + supported_identity_providers = ["COGNITO"] + explicit_auth_flows = [ "ALLOW_USER_SRP_AUTH", "ALLOW_REFRESH_TOKEN_AUTH", @@ -96,6 +104,39 @@ resource "aws_cognito_user_pool_client" "nextstrain-cli" { refresh_token = "days" } + # Allowed redirection destinations to complete authentication. + # + # We'd prefer to use 127.0.0.1 instead of localhost to avoid name resolution + # issues on end user systems, issues which are known to occur. The "OAuth + # 2.0 for native apps" best current practice (RFC 8252) suggests as much¹, but + # alas, Cognito's https-requirement exception for localhost is not applied to + # 127.0.0.1. + # + # Similarly, we'd prefer to register without an explicit port and rely on the + # same RFC's stipulation of relaxed port matching for localhost², but alack, + # Cognito doesn't follow that either and requires strict port matching. + # + # Since the CLI may not always be able to listen on a specific port given + # other services that might be running, and there's also value in random + # choice making interception harder, register a slew of ports for use and let + # Nextstrain CLI draw from the list. + # -trs, 19 Nov 2023 + # + # ¹ + # ² + callback_urls = formatlist("http://localhost:%d/", random_integer.nextstrain_cli_callback_port[*].result) + read_attributes = local.user_attributes write_attributes = setsubtract(local.user_attributes, ["email_verified", "phone_number_verified"]) } + +resource "random_integer" "nextstrain_cli_callback_port" { + # AWS Cognito supports 100 callback URLs per client + # + count = 99 + + # IANA-defined port range for dynamic use. + # + min = 49152 + max = 65535 +} diff --git a/aws/cognito/outputs.tf b/aws/cognito/outputs.tf index e8cdd8cfe..edc3878f0 100644 --- a/aws/cognito/outputs.tf +++ b/aws/cognito/outputs.tf @@ -14,6 +14,10 @@ output "OAUTH2_CLI_CLIENT_ID" { value = aws_cognito_user_pool_client.nextstrain-cli.id } +output "OAUTH2_CLI_CLIENT_REDIRECT_URIS" { + value = aws_cognito_user_pool_client.nextstrain-cli.callback_urls +} + output "OAUTH2_LOGOUT_URL" { value = format("https://%s/logout", coalesce( one(aws_cognito_user_pool_domain.custom[*].domain), diff --git a/docs/production.rst b/docs/production.rst index 02d62a77a..c26e6fd35 100644 --- a/docs/production.rst +++ b/docs/production.rst @@ -239,6 +239,7 @@ file are:: OAUTH2_CLIENT_ID OAUTH2_CLIENT_SECRET OAUTH2_CLI_CLIENT_ID + OAUTH2_CLI_CLIENT_REDIRECT_URIS OIDC_USERNAME_CLAIM OIDC_GROUPS_CLAIM @@ -282,13 +283,34 @@ CLI client A `public, native application client `__ is required for use by the :doc:`Nextstrain CLI ` and is permitted by the app server to make `Bearer`-authenticated requests. Its id is configured by -`OAUTH2_CLI_CLIENT_ID`. +`OAUTH2_CLI_CLIENT_ID`. The client registration must allow: -.. note:: - Currently Nextstrain CLI is tightly bound to AWS Cognito and requires - its Secure Remote Password authentication flow implemented outside of - the standard OAuth 2.0 flows. We anticipate changing this in the - future. + - the authorization code flow, ideally with PKCE_ support + + - issuance of refresh tokens, either by default or by requesting the + `offline_access` scope + + - at least one authentication redirection (sometimes "callback") URL of + `http://127.0.0.1:/` or `http://localhost:/` + +The CLI auto-discovers its OpenID client configuration (and the IdP +configuration) from the app server. The app server must be configured to know +the CLI client's redirect URIs with `OAUTH2_CLI_CLIENT_REDIRECT_URIS` so the +URLs can be included in the discovery response. + +If the IdP allows for `http://` redirect URIs for loopback IPs (e.g. +`127.0.0.1`), then the loopback IP should be preferred over using `localhost`, +as per best current practice described in `RFC 8252 § 8.3`_. + +If the IdP allows relaxed port matching for loopback IP/localhost redirect +URIs, as per best current practice described in `RFC 8252 § 7.3`_, then only a +single redirect URI needs to be registered with the IdP. Otherwise, multiple +redirect URIs with varying ports should be registered to allow the CLI +alternatives to choose from in case it can't bind a given port on a user's +computer. + +.. _RFC 8252 § 7.3: https://datatracker.ietf.org/doc/html/rfc8252#section-7.3 +.. _RFC 8252 § 8.3: https://datatracker.ietf.org/doc/html/rfc8252#section-8.3 Token lifetimes diff --git a/env/outputs.tf b/env/outputs.tf index f52d611d5..595107bf6 100644 --- a/env/outputs.tf +++ b/env/outputs.tf @@ -23,6 +23,10 @@ output "OAUTH2_CLI_CLIENT_ID" { value = module.cognito.OAUTH2_CLI_CLIENT_ID } +output "OAUTH2_CLI_CLIENT_REDIRECT_URIS" { + value = module.cognito.OAUTH2_CLI_CLIENT_REDIRECT_URIS +} + output "OAUTH2_LOGOUT_URL" { value = module.cognito.OAUTH2_LOGOUT_URL } diff --git a/env/production/.terraform.lock.hcl b/env/production/.terraform.lock.hcl index 4681d02a9..c114af0a0 100644 --- a/env/production/.terraform.lock.hcl +++ b/env/production/.terraform.lock.hcl @@ -20,3 +20,22 @@ provider "registry.terraform.io/hashicorp/aws" { "zh:f4b86e7df4e846a38774e8e648b41c5ebaddcefa913cfa1864568086b7735575", ] } + +provider "registry.terraform.io/hashicorp/random" { + version = "3.5.1" + hashes = [ + "h1:VSnd9ZIPyfKHOObuQCaKfnjIHRtR7qTw19Rz8tJxm+k=", + "zh:04e3fbd610cb52c1017d282531364b9c53ef72b6bc533acb2a90671957324a64", + "zh:119197103301ebaf7efb91df8f0b6e0dd31e6ff943d231af35ee1831c599188d", + "zh:4d2b219d09abf3b1bb4df93d399ed156cadd61f44ad3baf5cf2954df2fba0831", + "zh:6130bdde527587bbe2dcaa7150363e96dbc5250ea20154176d82bc69df5d4ce3", + "zh:6cc326cd4000f724d3086ee05587e7710f032f94fc9af35e96a386a1c6f2214f", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:b6d88e1d28cf2dfa24e9fdcc3efc77adcdc1c3c3b5c7ce503a423efbdd6de57b", + "zh:ba74c592622ecbcef9dc2a4d81ed321c4e44cddf7da799faa324da9bf52a22b2", + "zh:c7c5cde98fe4ef1143bd1b3ec5dc04baf0d4cc3ca2c5c7d40d17c0e9b2076865", + "zh:dac4bad52c940cd0dfc27893507c1e92393846b024c5a9db159a93c534a3da03", + "zh:de8febe2a2acd9ac454b844a4106ed295ae9520ef54dc8ed2faf29f12716b602", + "zh:eab0d0495e7e711cca367f7d4df6e322e6c562fc52151ec931176115b83ed014", + ] +} diff --git a/env/production/config.json b/env/production/config.json index d27393b06..c5e6a168d 100644 --- a/env/production/config.json +++ b/env/production/config.json @@ -3,6 +3,107 @@ "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_CLI_CLIENT_REDIRECT_URIS": [ + "http://localhost:49154/", + "http://localhost:49208/", + "http://localhost:49233/", + "http://localhost:49278/", + "http://localhost:49852/", + "http://localhost:50049/", + "http://localhost:50146/", + "http://localhost:50208/", + "http://localhost:50290/", + "http://localhost:50552/", + "http://localhost:50555/", + "http://localhost:50560/", + "http://localhost:50978/", + "http://localhost:51122/", + "http://localhost:51182/", + "http://localhost:51357/", + "http://localhost:51494/", + "http://localhost:51716/", + "http://localhost:51838/", + "http://localhost:51841/", + "http://localhost:51861/", + "http://localhost:51924/", + "http://localhost:52109/", + "http://localhost:52176/", + "http://localhost:52191/", + "http://localhost:52258/", + "http://localhost:52560/", + "http://localhost:52629/", + "http://localhost:53113/", + "http://localhost:53369/", + "http://localhost:53995/", + "http://localhost:54137/", + "http://localhost:54211/", + "http://localhost:54378/", + "http://localhost:54568/", + "http://localhost:54971/", + "http://localhost:55027/", + "http://localhost:55341/", + "http://localhost:55396/", + "http://localhost:55535/", + "http://localhost:55536/", + "http://localhost:55555/", + "http://localhost:55610/", + "http://localhost:55825/", + "http://localhost:56014/", + "http://localhost:56361/", + "http://localhost:56691/", + "http://localhost:56846/", + "http://localhost:56978/", + "http://localhost:57264/", + "http://localhost:57282/", + "http://localhost:57578/", + "http://localhost:57856/", + "http://localhost:57875/", + "http://localhost:58039/", + "http://localhost:58199/", + "http://localhost:58638/", + "http://localhost:59095/", + "http://localhost:59462/", + "http://localhost:59507/", + "http://localhost:59628/", + "http://localhost:59804/", + "http://localhost:59906/", + "http://localhost:59942/", + "http://localhost:60139/", + "http://localhost:60257/", + "http://localhost:60377/", + "http://localhost:60564/", + "http://localhost:60579/", + "http://localhost:60705/", + "http://localhost:60775/", + "http://localhost:61015/", + "http://localhost:61309/", + "http://localhost:61376/", + "http://localhost:61384/", + "http://localhost:61399/", + "http://localhost:61588/", + "http://localhost:61915/", + "http://localhost:62350/", + "http://localhost:62478/", + "http://localhost:62752/", + "http://localhost:62947/", + "http://localhost:63087/", + "http://localhost:63124/", + "http://localhost:63230/", + "http://localhost:63257/", + "http://localhost:63514/", + "http://localhost:63519/", + "http://localhost:63638/", + "http://localhost:63692/", + "http://localhost:63838/", + "http://localhost:64029/", + "http://localhost:64098/", + "http://localhost:64294/", + "http://localhost:64873/", + "http://localhost:65081/", + "http://localhost:65266/", + "http://localhost:65271/", + "http://localhost:65311/" + ], "OAUTH2_LOGOUT_URL": "https://login.nextstrain.org/logout", "COGNITO_USER_POOL_ID": "us-east-1_Cg5rcTged", "OIDC_USERNAME_CLAIM": "cognito:username", diff --git a/env/testing/.terraform.lock.hcl b/env/testing/.terraform.lock.hcl index 4681d02a9..c114af0a0 100644 --- a/env/testing/.terraform.lock.hcl +++ b/env/testing/.terraform.lock.hcl @@ -20,3 +20,22 @@ provider "registry.terraform.io/hashicorp/aws" { "zh:f4b86e7df4e846a38774e8e648b41c5ebaddcefa913cfa1864568086b7735575", ] } + +provider "registry.terraform.io/hashicorp/random" { + version = "3.5.1" + hashes = [ + "h1:VSnd9ZIPyfKHOObuQCaKfnjIHRtR7qTw19Rz8tJxm+k=", + "zh:04e3fbd610cb52c1017d282531364b9c53ef72b6bc533acb2a90671957324a64", + "zh:119197103301ebaf7efb91df8f0b6e0dd31e6ff943d231af35ee1831c599188d", + "zh:4d2b219d09abf3b1bb4df93d399ed156cadd61f44ad3baf5cf2954df2fba0831", + "zh:6130bdde527587bbe2dcaa7150363e96dbc5250ea20154176d82bc69df5d4ce3", + "zh:6cc326cd4000f724d3086ee05587e7710f032f94fc9af35e96a386a1c6f2214f", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:b6d88e1d28cf2dfa24e9fdcc3efc77adcdc1c3c3b5c7ce503a423efbdd6de57b", + "zh:ba74c592622ecbcef9dc2a4d81ed321c4e44cddf7da799faa324da9bf52a22b2", + "zh:c7c5cde98fe4ef1143bd1b3ec5dc04baf0d4cc3ca2c5c7d40d17c0e9b2076865", + "zh:dac4bad52c940cd0dfc27893507c1e92393846b024c5a9db159a93c534a3da03", + "zh:de8febe2a2acd9ac454b844a4106ed295ae9520ef54dc8ed2faf29f12716b602", + "zh:eab0d0495e7e711cca367f7d4df6e322e6c562fc52151ec931176115b83ed014", + ] +} diff --git a/env/testing/config.json b/env/testing/config.json index 29e05b6f6..68af5aafe 100644 --- a/env/testing/config.json +++ b/env/testing/config.json @@ -3,6 +3,106 @@ "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_CLI_CLIENT_REDIRECT_URIS": [ + "http://localhost:49161/", + "http://localhost:49334/", + "http://localhost:49359/", + "http://localhost:49398/", + "http://localhost:49603/", + "http://localhost:50044/", + "http://localhost:50110/", + "http://localhost:50132/", + "http://localhost:50467/", + "http://localhost:50667/", + "http://localhost:50712/", + "http://localhost:51264/", + "http://localhost:51333/", + "http://localhost:51413/", + "http://localhost:51467/", + "http://localhost:51596/", + "http://localhost:51664/", + "http://localhost:51953/", + "http://localhost:51974/", + "http://localhost:51977/", + "http://localhost:52272/", + "http://localhost:52342/", + "http://localhost:52361/", + "http://localhost:52564/", + "http://localhost:52621/", + "http://localhost:52673/", + "http://localhost:53216/", + "http://localhost:53267/", + "http://localhost:53375/", + "http://localhost:53624/", + "http://localhost:53644/", + "http://localhost:54071/", + "http://localhost:55078/", + "http://localhost:55286/", + "http://localhost:55296/", + "http://localhost:55357/", + "http://localhost:55419/", + "http://localhost:55462/", + "http://localhost:55724/", + "http://localhost:56972/", + "http://localhost:57135/", + "http://localhost:57194/", + "http://localhost:57255/", + "http://localhost:57321/", + "http://localhost:57411/", + "http://localhost:57564/", + "http://localhost:57591/", + "http://localhost:57649/", + "http://localhost:57654/", + "http://localhost:57883/", + "http://localhost:58259/", + "http://localhost:58549/", + "http://localhost:58826/", + "http://localhost:59071/", + "http://localhost:59359/", + "http://localhost:59688/", + "http://localhost:60055/", + "http://localhost:60107/", + "http://localhost:60205/", + "http://localhost:60590/", + "http://localhost:60790/", + "http://localhost:60883/", + "http://localhost:60897/", + "http://localhost:60911/", + "http://localhost:61155/", + "http://localhost:61325/", + "http://localhost:61369/", + "http://localhost:61400/", + "http://localhost:61406/", + "http://localhost:61553/", + "http://localhost:62190/", + "http://localhost:62405/", + "http://localhost:62439/", + "http://localhost:62467/", + "http://localhost:62638/", + "http://localhost:62726/", + "http://localhost:63016/", + "http://localhost:63103/", + "http://localhost:63309/", + "http://localhost:63318/", + "http://localhost:63387/", + "http://localhost:63480/", + "http://localhost:63526/", + "http://localhost:63704/", + "http://localhost:63743/", + "http://localhost:63775/", + "http://localhost:64008/", + "http://localhost:64373/", + "http://localhost:64410/", + "http://localhost:64446/", + "http://localhost:64520/", + "http://localhost:64694/", + "http://localhost:64960/", + "http://localhost:64972/", + "http://localhost:65032/", + "http://localhost:65086/", + "http://localhost:65113/", + "http://localhost:65121/" + ], "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", diff --git a/src/app.js b/src/app.js index d1d010950..a37b01d19 100644 --- a/src/app.js +++ b/src/app.js @@ -454,6 +454,15 @@ app.route("/schemas/*") .all((req, res, next) => next(new NotFound())); +/* OpenID Connect 1.0 configuration. Retrieved by Nextstrain CLI to + * discovery necessary authentication details. + * + * + */ +app.routeAsync("/.well-known/openid-configuration") + .getAsync(endpoints.openid.providerConfiguration); + + /* Auspice HTML pages and assets. * * Auspice hardcodes URL paths that start with /dist/… in its Webpack config, diff --git a/src/config.js b/src/config.js index f64ce8bcf..7fe400583 100644 --- a/src/config.js +++ b/src/config.js @@ -246,6 +246,24 @@ export const OAUTH2_LOGOUT_URL = fromEnvOrConfig("OAUTH2_LOGOUT_URL", OIDC_CONFI export const OAUTH2_SCOPES_SUPPORTED = new Set(fromEnvOrConfig("OAUTH2_SCOPES_SUPPORTED", OIDC_CONFIGURATION.scopes_supported)); +/** + * Effective OpenID Connect (OIDC) identity provider configuration document + * after potential local overrides. + * + * Defined here to keep the overridden fields close to their declarations + * above. + */ +export const EFFECTIVE_OIDC_CONFIGURATION = { + ...OIDC_CONFIGURATION, + issuer: OIDC_ISSUER_URL, + jwks_uri: OIDC_JWKS_URL, + authorization_endpoint: OAUTH2_AUTHORIZATION_URL, + token_endpoint: OAUTH2_TOKEN_URL, + end_session_endpoint: OAUTH2_LOGOUT_URL, + scopes_supported: OAUTH2_SCOPES_SUPPORTED, +}; + + /** * OAuth2 client id of nextstrain.org server as registered with our IdP (e.g. * our Cognito user pool). @@ -279,6 +297,23 @@ export const OAUTH2_CLIENT_SECRET = fromEnvOrConfig("OAUTH2_CLIENT_SECRET", null export const OAUTH2_CLI_CLIENT_ID = fromEnvOrConfig("OAUTH2_CLI_CLIENT_ID"); +/** + * OAuth2 client redirect URIs (e.g. callback URLs) for Nextstrain CLI as + * registered with the IdP. + * + * These URLs are not themselves used by the server but are provided to + * (discovered by) Nextstrain CLI in a client configuration section of the + * OpenID configuration document served at /.well-known/openid-configuration. + * + * The name of this config var uses "redirect_uri" as the term since that's the + * literal field name used by the OIDC/OAuth2 specs in several places (initial + * auth requests, client metadata registration/querying, etc.). + * + * @type {string[]} + */ +export const OAUTH2_CLI_CLIENT_REDIRECT_URIS = fromEnvOrConfig("OAUTH2_CLI_CLIENT_REDIRECT_URIS"); + + /** * ID token claim field containing the username for a user. * diff --git a/src/endpoints/index.js b/src/endpoints/index.js index 6a9a57b4e..31d1870a7 100644 --- a/src/endpoints/index.js +++ b/src/endpoints/index.js @@ -1,6 +1,7 @@ import * as charon from './charon/index.js'; import * as cli from './cli.js'; import * as groups from "./groups.js"; +import * as openid from './openid.js'; import * as options from './options.js'; import * as sources from './sources.js'; import * as static_ from './static.js'; @@ -10,6 +11,7 @@ export { charon, cli, groups, + openid, options, sources, static_ as static, diff --git a/src/endpoints/openid.js b/src/endpoints/openid.js new file mode 100644 index 000000000..e1e778205 --- /dev/null +++ b/src/endpoints/openid.js @@ -0,0 +1,68 @@ +/** + * OpenID Connect 1.0 endpoints. + * + * @module endpoints.openid + */ + +import { + EFFECTIVE_OIDC_CONFIGURATION, + OAUTH2_CLI_CLIENT_ID, + OAUTH2_CLI_CLIENT_REDIRECT_URIS, + OIDC_USERNAME_CLAIM, + OIDC_GROUPS_CLAIM, + COGNITO_USER_POOL_ID, +} from "../config.js"; + + +/** + * A client configuration document for Nextstrain CLI to automatically discover + * the client metadata it should use for itself. + * + * Based on the OIDC dynamic client registration spec, see + * {@link https://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata} and + * {@link https://openid.net/specs/openid-connect-registration-1_0.html#ReadResponse}. + */ +const cliClientConfiguration = { + client_id: OAUTH2_CLI_CLIENT_ID, + + // Static/assumed values asserted by the CLI, but informative to include. + application_type: "native", + response_types: ["code"], + grant_types: ["authorization_code"], + + /* Used to know the list of ports that can be listened to on localhost, as + * not all IdPs follow RFC 8252 § 7.3.¹ + * + * ¹ + */ + redirect_uris: OAUTH2_CLI_CLIENT_REDIRECT_URIS, + + /******************************************** + * Custom metadata fields below this point. * + ********************************************/ + + /* Used by the CLI for display purposes only, but nice to get the display + * right. + */ + id_token_username_claim: OIDC_USERNAME_CLAIM, + id_token_groups_claim: OIDC_GROUPS_CLAIM, + + /* Used for Secure Remote Password auth flow with Cognito outside the + * OIDC/OAuth2 protocols. + */ + aws_cognito_user_pool_id: COGNITO_USER_POOL_ID, +}; + + +/** + * IdP metadata for the /.well-known/openid-configuration endpoint. + * + * As the spec allows, we extend the metadata with a client configuration + * section for Nextstrain CLI to allow it to perform automatic discovery. + * + * Refer to {@link https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata}. + */ +export const providerConfiguration = (req, res) => res.json({ + ...EFFECTIVE_OIDC_CONFIGURATION, + nextstrain_cli_client_configuration: cliClientConfiguration, +});