From b9dba8e2cb94e99490f82b21700051ce447aaadd 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/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 ++ 6 files changed, 95 insertions(+) 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/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 9236863af1..f918162be6 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -60,6 +60,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" }, 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 f956984c4d..46043a0b12 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22496,6 +22496,7 @@ __metadata: "@types/global-agent": 2.1.3 app: "*" global-agent: 3.0.0 + jose: ^5.9.6 prettier: 3.4.2 undici: 6.19.8 winston: 3.14.2 @@ -32478,6 +32479,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"