From 8e58de5913e1ab8c753ed2e74c8811ed968b08bb Mon Sep 17 00:00:00 2001 From: Jessica He Date: Thu, 28 Nov 2024 18:02:30 -0500 Subject: [PATCH] add additional OIDC auth resolvers Signed-off-by: Jessica He --- docs/auth.md | 2 + docs/dynamic-plugins/installing-plugins.md | 2 +- docs/ping-identity-oidc-setup.md | 4 + packages/backend/package.json | 1 + .../src/modules/authProvidersModule.ts | 6 ++ packages/backend/src/modules/authResolvers.ts | 74 +++++++++++++++++++ yarn.lock | 8 ++ 7 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 packages/backend/src/modules/authResolvers.ts diff --git a/docs/auth.md b/docs/auth.md index cd799f2cd2..98499c5759 100644 --- a/docs/auth.md +++ b/docs/auth.md @@ -106,6 +106,7 @@ For more information on setting up the OAuth2 Proxy auth provider, consult the [ # - resolver: preferredUsernameMatchingUserEntityName # - resolver: emailMatchingUserEntityProfileEmail # - resolver: emailLocalPartMatchingUserEntityName + # - resolver: oidcSubClaimMatchingKeycloakUserId ``` In an example using Keycloak for authentication with the OIDC provider, there are a few steps that need to be taken to get everything working: @@ -122,6 +123,7 @@ In an example using Keycloak for authentication with the OIDC provider, there ar The default resolver provided by the `oidc` auth provider is the `emailLocalPartMatchingUserEntityName` resolver. If you want to use a different resolver, add the resolver you want to use in the `auth.providers.oidc.[environment].signIn.resolvers` configuration as soon in the example above, and it will override the default resolver. +* For enhanced security, consider using the `oidcSubClaimMatchingKeycloakUserId` resolver which matches the user with the immutable `sub` parameter from OIDC to the Keycloak user ID. For more information on setting up the OIDC auth provider, consult the [Backstage documentation](https://backstage.io/docs/auth/oidc#the-configuration). diff --git a/docs/dynamic-plugins/installing-plugins.md b/docs/dynamic-plugins/installing-plugins.md index 1537dcdae8..bd7630e4da 100644 --- a/docs/dynamic-plugins/installing-plugins.md +++ b/docs/dynamic-plugins/installing-plugins.md @@ -3,7 +3,7 @@ To install a dynamic plugin, you need to add the plugin definition to the `dynamic-plugin-config.yaml` file. The placement of `dynamic-plugin-config.yaml` depends on the deployment method. -For more information, see [Installing Dynamic Plugins with the Red Hat Developer Hub Operator](https://docs.redhat.com/en/documentation/red_hat_developer_hub/1.3/html/installing_and_viewing_dynamic_plugins/proc-config-dynamic-plugins-rhdh-operator_title-plugins-rhdh-about) or [Installing Dynamic Plugins Using the Helm Chart](https://docs.redhat.com/en/documentation/red_hat_developer_hub/1.3/html/installing_and_viewing_dynamic_plugins/con-install-dynamic-plugin-helm_title-plugins-rhdh-about). +For more information, see [Installing Dynamic Plugins with the Red Hat Developer Hub Operator](https://docs.redhat.com/en/documentation/red_hat_developer_hub/1.3/html/installing_and_viewing_dynamic_plugins/index#proc-config-dynamic-plugins-rhdh-operator_title-plugins-rhdh-about) or [Installing Dynamic Plugins Using the Helm Chart](https://docs.redhat.com/en/documentation/red_hat_developer_hub/1.3/html/installing_and_viewing_dynamic_plugins/index#con-install-dynamic-plugin-helm_title-plugins-rhdh-about). Plugins are defined in the `plugins` array in the `dynamic-plugin-config.yaml` file. Each plugin is defined as an object with the following properties: diff --git a/docs/ping-identity-oidc-setup.md b/docs/ping-identity-oidc-setup.md index a98c28952f..b383ce6029 100644 --- a/docs/ping-identity-oidc-setup.md +++ b/docs/ping-identity-oidc-setup.md @@ -37,6 +37,9 @@ auth: clientId: ${PING_IDENTITY_CLIENT_ID} clientSecret: ${PING_IDENTITY_CLIENT_SECRET} prompt: auto #optional + signIn: + resolvers: + - resolver: oidcSubClaimMatchingPingIdentityUserId ``` The OIDC provider requires three mandatory configuration keys: @@ -46,6 +49,7 @@ The OIDC provider requires three mandatory configuration keys: - `metadataUrl`: Copy from `OIDC Discovery Endpoint` under `Configuration` tab in `URLs` drop down. - `prompt` (optional): Recommended to use auto so the browser will request login to the IDP if the user has no active session. - `additionalScopes` (optional): List of scopes for the App Registration, to be requested in addition to the required ones. +- `signIn.resolvers.resolver` (optional): `oidcSubClaimMatchingPingIdentityUserId` is a secure user resolver that matches the `sub` claim from OIDC to the Ping Identity user ID. #### Known Issues diff --git a/packages/backend/package.json b/packages/backend/package.json index 2f6ef88bac..5764577bb0 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -59,6 +59,7 @@ "@opentelemetry/sdk-node": "0.53.0", "app": "*", "global-agent": "3.0.0", + "jose": "^5.9.6", "undici": "6.19.8", "winston": "3.14.2", "winston-daily-rotate-file": "5.0.0" diff --git a/packages/backend/src/modules/authProvidersModule.ts b/packages/backend/src/modules/authProvidersModule.ts index beb6c43881..fdd7d3242b 100644 --- a/packages/backend/src/modules/authProvidersModule.ts +++ b/packages/backend/src/modules/authProvidersModule.ts @@ -24,6 +24,8 @@ import { createOAuthProviderFactory, } from '@backstage/plugin-auth-node'; +import { rhdhSignInResolvers } from './authResolvers'; + /** * Function is responsible for signing in a user with the catalog user and * creating an entity reference based on the provided name parameter. @@ -221,6 +223,10 @@ function getAuthProviderFactory(providerId: string): AuthProviderFactory { signInResolver: oidcSignInResolvers.emailLocalPartMatchingUserEntityName(), signInResolverFactories: { + oidcSubClaimMatchingKeycloakUserId: + rhdhSignInResolvers.oidcSubClaimMatchingKeycloakUserId, + oidcSubClaimMatchingPingIdentityUserId: + rhdhSignInResolvers.oidcSubClaimMatchingPingIdentityUserId, ...oidcSignInResolvers, }, }); diff --git a/packages/backend/src/modules/authResolvers.ts b/packages/backend/src/modules/authResolvers.ts new file mode 100644 index 0000000000..9f5c3987ff --- /dev/null +++ b/packages/backend/src/modules/authResolvers.ts @@ -0,0 +1,74 @@ +import { OidcAuthResult } from '@backstage/plugin-auth-backend-module-oidc-provider'; +import { + AuthResolverContext, + createSignInResolverFactory, + OAuthAuthenticatorResult, + SignInInfo, +} from '@backstage/plugin-auth-node'; + +import { decodeJwt } from 'jose'; + +const KEYCLOAK_ID_ANNOTATION = 'keycloak.org/id'; +const PING_IDENTITY_ID_ANNOTATION = 'pingidentity.org/id'; + +/** + * Creates an OIDC sign-in resolver that looks up the user using a specific annotation key. + * + * @param annotationKey - The annotation key to match the user's `sub` claim. + * @param providerName - The name of the identity provider to report in error message if the `sub` claim is missing. + */ +const createOidcSubClaimResolver = (userIdKey: string, providerName: string) => + createSignInResolverFactory({ + create() { + return async ( + info: SignInInfo>, + ctx: AuthResolverContext, + ) => { + const sub = info.result.fullProfile.userinfo.sub; + if (!sub) { + throw new Error( + `The user profile from ${providerName} is missing a 'sub' claim, likely due to a misconfiguration in the provider. Please contact your system administrator for assistance.`, + ); + } + + const idToken = info.result.fullProfile.tokenset.id_token; + if (!idToken) { + throw new Error( + `The user ID token from ${providerName} is missing a 'sub' claim, likely due to a misconfiguration in the provider. Please contact your system administrator for assistance.`, + ); + } + + const subFromIdToken = decodeJwt(idToken)?.sub; + if (sub !== subFromIdToken) { + throw new Error( + `There was a problem verifying your identity with ${providerName} due to a mismatching 'sub' claim. Please contact your system administrator for assistance.`, + ); + } + + return ctx.signInWithCatalogUser({ + annotations: { [userIdKey]: sub }, + }); + }; + }, + }); + +/** + * Additional sign-in resolvers for the Oidc auth provider. + * + * @public + */ +export namespace rhdhSignInResolvers { + /** + * An OIDC resolver that looks up the user using their Keycloak user ID. + */ + export const oidcSubClaimMatchingKeycloakUserId = createOidcSubClaimResolver( + KEYCLOAK_ID_ANNOTATION, + 'Keycloak', + ); + + /** + * An OIDC resolver that looks up the user using their Ping Identity user ID. + */ + export const oidcSubClaimMatchingPingIdentityUserId = + createOidcSubClaimResolver(PING_IDENTITY_ID_ANNOTATION, 'Ping Identity'); +} diff --git a/yarn.lock b/yarn.lock index 5bf3dbc824..f27547cf0d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22386,6 +22386,7 @@ __metadata: app: "*" global-agent: 3.0.0 prettier: 3.4.1 + jose: ^5.9.6 undici: 6.19.8 winston: 3.14.2 winston-daily-rotate-file: 5.0.0 @@ -32375,6 +32376,13 @@ __metadata: languageName: node linkType: hard +"jose@npm:^5.9.6": + version: 5.9.6 + resolution: "jose@npm:5.9.6" + checksum: 4b536da0201858ed4c4582e8bb479081f11e0c63dd0f5e473adde16fc539785e1f2f0409bc1fc7cbbb5b68026776c960b4952da3a06f6fdfff0b9764c9127ae0 + languageName: node + linkType: hard + "joycon@npm:^3.0.1": version: 3.1.1 resolution: "joycon@npm:3.1.1"