diff --git a/.changeset/sharp-wombats-speak.md b/.changeset/sharp-wombats-speak.md index ff6a349ed8..0771b45f16 100644 --- a/.changeset/sharp-wombats-speak.md +++ b/.changeset/sharp-wombats-speak.md @@ -2,7 +2,7 @@ '@backstage/plugin-auth-backend': minor --- -Allow user-defined scopes for Okta auth in config yaml +Allow additional user-defined scopes for Okta auth in config yaml Example `app-config.yaml` excerpt @@ -18,11 +18,11 @@ auth: authServerId: ${AUTH_OKTA_AUTH_SERVER_ID} # Optional idp: ${AUTH_OKTA_IDP} # Optional # https://developer.okta.com/docs/reference/api/oidc/#scope-dependent-claims-not-always-returned - scope: openid profile email offline_access groups # Optional + additionalScopes: groups # Optional ``` -- Accept a new scope option during okta creation with `createAuthProviderIntegration` -- Pass the user-defined `scope` as an option to `OktaAuthProvider` -- Add `scope` as an option for `OktaAuthProvider` -- Set `scope` in `OktaAuthProvider` to the `scope` passed as an `option` or a default of `'openid email profile offline_access'` if a user-defined option is not provided -- Update the `start` and `refresh` methods to use `scope` from `OktaAuthProvider` rather than `scope` from the request +- Accept a new additionalScope option during okta creation with `createAuthProviderIntegration` +- Passes the the user-defined `additionalScopes` as an option to `OktaAuthProvider` +- Add `additionalScopes` as an option for `OktaAuthProvider` +- Set `scope` in `OktaAuthProvider` to the combined value of current scopes combined with the user-defined `additionalScopes` passed as an `option` +- Update the `start` and `refresh` methods to use the new combiend `scope` from `OktaAuthProvider` rather than `scope` from the request diff --git a/docs/auth/okta/provider.md b/docs/auth/okta/provider.md index 95381dd02c..99ff664a67 100644 --- a/docs/auth/okta/provider.md +++ b/docs/auth/okta/provider.md @@ -44,7 +44,7 @@ auth: authServerId: ${AUTH_OKTA_AUTH_SERVER_ID} # Optional idp: ${AUTH_OKTA_IDP} # Optional # https://developer.okta.com/docs/reference/api/oidc/#scope-dependent-claims-not-always-returned - scope: ${AUTH_OKTA_SCOPE} # Optional + additionalScopes: ${AUTH_OKTA_ADDITIONAL_SCOPES} # Optional ``` The values referenced are found on the Application page on your Okta site. @@ -57,7 +57,7 @@ The values referenced are found on the Application page on your Okta site. - `authServerId`: The authorization server ID for the Application - `idp`: The identity provider for the application, e.g. `0oaulob4BFVa4zQvt0g3` -`scope` is an optional value to change the `scope` value sent to Okta during OAuth. The default value is `openid profile email offline_access`. Changing the `scope` will have an impact on [the dependent claims returned](https://developer.okta.com/docs/reference/api/oidc/#scope-dependent-claims-not-always-returned). For example, adding `groups` to the `scope` by setting the value to `openid profile email offline_access groups` will result in the claim returning a list of the groups that the user is a member of that also match the ID token group filter of the client app. +`additionalScopes` is an optional value, a string of space separated scopes, that will be combined with the default `scope` value of `openid profile email offline_access` to adjust the `scope` sent to Okta during OAuth. This will have an impact on [the dependent claims returned](https://developer.okta.com/docs/reference/api/oidc/#scope-dependent-claims-not-always-returned). For example, setting the `additionalScopes` value to `groups` will result in the claim returning a list of the groups that the user is a member of that also match the ID token group filter of the client app. ## Adding the provider to the Backstage frontend diff --git a/plugins/auth-backend/config.d.ts b/plugins/auth-backend/config.d.ts index f8953e1588..36ed7eeaf8 100644 --- a/plugins/auth-backend/config.d.ts +++ b/plugins/auth-backend/config.d.ts @@ -131,6 +131,7 @@ export interface Config { authServerId?: string; idp?: string; callbackUrl?: string; + additionalScopes?: string; }; }; /** @visibility frontend */ diff --git a/plugins/auth-backend/src/providers/okta/provider.test.ts b/plugins/auth-backend/src/providers/okta/provider.test.ts index f91f7fff89..0534bfbb38 100644 --- a/plugins/auth-backend/src/providers/okta/provider.test.ts +++ b/plugins/auth-backend/src/providers/okta/provider.test.ts @@ -105,6 +105,7 @@ describe('createOktaProvider', () => { }); it('should pass a custom scope to start and refresh requests', async () => { + const additionalScopes = 'groups'; const mockScope = 'openid profile email offline_access groups'; const reqScope = 'openid profile email offline_access'; const provider = new OktaAuthProvider({ @@ -119,7 +120,7 @@ describe('createOktaProvider', () => { clientId: 'mock', clientSecret: 'mock', callbackUrl: 'mock', - scope: mockScope, + additionalScopes, }); mockRedirectStrategy.mockResolvedValueOnce({ diff --git a/plugins/auth-backend/src/providers/okta/provider.ts b/plugins/auth-backend/src/providers/okta/provider.ts index 3ebc865726..6b27fbb612 100644 --- a/plugins/auth-backend/src/providers/okta/provider.ts +++ b/plugins/auth-backend/src/providers/okta/provider.ts @@ -60,7 +60,7 @@ export type OktaAuthProviderOptions = OAuthProviderOptions & { signInResolver?: SignInResolver; authHandler: AuthHandler; resolverContext: AuthResolverContext; - scope?: string; + additionalScopes?: string; }; export class OktaAuthProvider implements OAuthHandlers { @@ -68,7 +68,7 @@ export class OktaAuthProvider implements OAuthHandlers { private readonly signInResolver?: SignInResolver; private readonly authHandler: AuthHandler; private readonly resolverContext: AuthResolverContext; - private readonly scope: string; + private readonly additionalScopes: string; /** * Due to passport-okta-oauth forcing options.state = true, @@ -91,7 +91,7 @@ export class OktaAuthProvider implements OAuthHandlers { this.signInResolver = options.signInResolver; this.authHandler = options.authHandler; this.resolverContext = options.resolverContext; - this.scope = options.scope || 'openid email profile offline_access'; + this.additionalScopes = options.additionalScopes || ''; this.strategy = new OktaStrategy( { @@ -128,11 +128,19 @@ export class OktaAuthProvider implements OAuthHandlers { ); } + private combineScopeStrings(scopesA: string, scopesB: string) { + const scopesAArray = scopesA.split(' '); + const scopesBArray = scopesB.split(' '); + const combinedScopes = new Set([...scopesAArray, ...scopesBArray]); + return Array.from(combinedScopes).join(' '); + } + async start(req: OAuthStartRequest): Promise { + const scope = this.combineScopeStrings(req.scope, this.additionalScopes); return await executeRedirectStrategy(req, this.strategy, { accessType: 'offline', prompt: 'consent', - scope: this.scope, + scope: scope, state: encodeState(req.state), }); } @@ -150,12 +158,9 @@ export class OktaAuthProvider implements OAuthHandlers { } async refresh(req: OAuthRefreshRequest) { + const scope = this.combineScopeStrings(req.scope, this.additionalScopes); const { accessToken, refreshToken, params } = - await executeRefreshTokenStrategy( - this.strategy, - req.refreshToken, - this.scope, - ); + await executeRefreshTokenStrategy(this.strategy, req.refreshToken, scope); const fullProfile = await executeFetchUserProfileStrategy( this.strategy, @@ -233,7 +238,8 @@ export const okta = createAuthProviderIntegration({ const callbackUrl = customCallbackUrl || `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const scope = envConfig.getOptionalString('scope'); + const additionalScopes = + envConfig.getOptionalString('additionalScopes'); // This is a safe assumption as `passport-okta-oauth` uses the audience // as the base for building the authorization, token, and user info URLs. @@ -258,7 +264,7 @@ export const okta = createAuthProviderIntegration({ authHandler, signInResolver: options?.signIn?.resolver, resolverContext, - scope, + additionalScopes, }); return OAuthAdapter.fromConfig(globalConfig, provider, {