diff --git a/.changeset/long-llamas-push-atlassian.md b/.changeset/long-llamas-push-atlassian.md new file mode 100644 index 0000000000..3e0ae5ad14 --- /dev/null +++ b/.changeset/long-llamas-push-atlassian.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend-module-atlassian-provider': minor +--- + +**BREAKING**: The `scope` and `scopes` config options have been removed and replaced by the standard `additionalScopes` config. In addition, the `offline_access`, `read:jira-work`, and `read:jira-user` scopes have been set to required and will always be present. diff --git a/.changeset/long-llamas-push-bitbucket.md b/.changeset/long-llamas-push-bitbucket.md new file mode 100644 index 0000000000..36a68219eb --- /dev/null +++ b/.changeset/long-llamas-push-bitbucket.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend-module-bitbucket-provider': patch +--- + +Added support for the new shared `additionalScopes` configuration. In addition, the `account` scope has been set to required and will always be present. diff --git a/.changeset/long-llamas-push-github.md b/.changeset/long-llamas-push-github.md new file mode 100644 index 0000000000..c68fb8e925 --- /dev/null +++ b/.changeset/long-llamas-push-github.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend-module-github-provider': patch +--- + +Added support for the new shared `additionalScopes` configuration. In addition, the `read:user` scope has been set to required and will always be present. diff --git a/.changeset/long-llamas-push-gitlab.md b/.changeset/long-llamas-push-gitlab.md new file mode 100644 index 0000000000..fe5d293723 --- /dev/null +++ b/.changeset/long-llamas-push-gitlab.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend-module-gitlab-provider': patch +--- + +Added support for the new shared `additionalScopes` configuration. In addition, the `read_user` scope has been set to required and will always be present. diff --git a/.changeset/long-llamas-push-google.md b/.changeset/long-llamas-push-google.md new file mode 100644 index 0000000000..c9de73eb36 --- /dev/null +++ b/.changeset/long-llamas-push-google.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend-module-google-provider': patch +--- + +Added support for the new shared `additionalScopes` configuration. In addition, the `openid`, `userinfo.email`, and `userinfo.profile` scopes have been set to required and will always be present. diff --git a/.changeset/long-llamas-push-microsoft.md b/.changeset/long-llamas-push-microsoft.md new file mode 100644 index 0000000000..f6509db0f7 --- /dev/null +++ b/.changeset/long-llamas-push-microsoft.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend-module-microsoft-provider': patch +--- + +Added support for the new shared `additionalScopes` configuration. diff --git a/.changeset/long-llamas-push-oauth2.md b/.changeset/long-llamas-push-oauth2.md new file mode 100644 index 0000000000..2a233dbb42 --- /dev/null +++ b/.changeset/long-llamas-push-oauth2.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend-module-oauth2-provider': minor +--- + +**BREAKING**: The `scope` config option have been removed and replaced by the standard `additionalScopes` config. diff --git a/.changeset/long-llamas-push-oidc.md b/.changeset/long-llamas-push-oidc.md new file mode 100644 index 0000000000..248d762864 --- /dev/null +++ b/.changeset/long-llamas-push-oidc.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend-module-oidc-provider': minor +--- + +**BREAKING**: The `scope` config option have been removed and replaced by the standard `additionalScopes` config. In addition, `openid`, `profile`, and `email` scopes have been set to required and will always be present. diff --git a/.changeset/long-llamas-push-okta.md b/.changeset/long-llamas-push-okta.md new file mode 100644 index 0000000000..d330746d2e --- /dev/null +++ b/.changeset/long-llamas-push-okta.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend-module-okta-provider': patch +--- + +Added support for the new shared `additionalScopes` configuration, which means it can now also be specified as an array. In addition, the `openid`, `email`, `profile`, and `offline_access` scopes have been set to required and will always be present. diff --git a/.changeset/long-llamas-push-pinniped.md b/.changeset/long-llamas-push-pinniped.md new file mode 100644 index 0000000000..a9c6a54cee --- /dev/null +++ b/.changeset/long-llamas-push-pinniped.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend-module-pinniped-provider': patch +--- + +**BREAKING**: The `scope` config option have been removed and replaced by the standard `additionalScopes` config. In addition, the `openid`, `pinniped:request-audience`, `username`, and `offline_access` scopes have been set to required and will always be present. diff --git a/.changeset/long-llamas-push-vmware-cloud.md b/.changeset/long-llamas-push-vmware-cloud.md new file mode 100644 index 0000000000..9edfb01f68 --- /dev/null +++ b/.changeset/long-llamas-push-vmware-cloud.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend-module-vmware-cloud-provider': minor +--- + +**BREAKING**: The `scope` config option have been removed and replaced by the standard `additionalScopes` config. In addition, `openid`, and `offline_access` scopes have been set to required and will always be present. diff --git a/plugins/auth-backend-module-atlassian-provider/config.d.ts b/plugins/auth-backend-module-atlassian-provider/config.d.ts index 57def03aab..d2c8a249c1 100644 --- a/plugins/auth-backend-module-atlassian-provider/config.d.ts +++ b/plugins/auth-backend-module-atlassian-provider/config.d.ts @@ -27,7 +27,7 @@ export interface Config { clientSecret: string; audience?: string; callbackUrl?: string; - scope?: string; + additionalScopes?: string | string[]; }; }; }; diff --git a/plugins/auth-backend-module-atlassian-provider/src/authenticator.ts b/plugins/auth-backend-module-atlassian-provider/src/authenticator.ts index b31de13134..0ced9cf831 100644 --- a/plugins/auth-backend-module-atlassian-provider/src/authenticator.ts +++ b/plugins/auth-backend-module-atlassian-provider/src/authenticator.ts @@ -26,15 +26,20 @@ import { Strategy as AtlassianStrategy } from 'passport-atlassian-oauth2'; export const atlassianAuthenticator = createOAuthAuthenticator({ defaultProfileTransform: PassportOAuthAuthenticatorHelper.defaultProfileTransform, + scopes: { + required: ['offline_access', 'read:jira-work', 'read:jira-user'], + }, initialize({ callbackUrl, config }) { const clientId = config.getString('clientId'); const clientSecret = config.getString('clientSecret'); - const scope = - config.getOptionalString('scope') ?? - config.getOptionalString('scopes') ?? - 'offline_access read:jira-work read:jira-user'; const baseUrl = 'https://auth.atlassian.com'; + if (config.has('scope') || config.has('scopes')) { + throw new Error( + 'The atlassian provider no longer supports the "scope" or "scopes" configuration options. Please use the "additionalScopes" option instead.', + ); + } + return PassportOAuthAuthenticatorHelper.from( new AtlassianStrategy( { @@ -45,7 +50,6 @@ export const atlassianAuthenticator = createOAuthAuthenticator({ authorizationURL: `${baseUrl}/authorize`, tokenURL: `${baseUrl}/oauth/token`, profileURL: `${baseUrl}/api/v4/user`, - scope, }, ( accessToken: string, diff --git a/plugins/auth-backend-module-atlassian-provider/src/module.test.ts b/plugins/auth-backend-module-atlassian-provider/src/module.test.ts index 08b84ad165..7b92218d56 100644 --- a/plugins/auth-backend-module-atlassian-provider/src/module.test.ts +++ b/plugins/auth-backend-module-atlassian-provider/src/module.test.ts @@ -75,7 +75,8 @@ describe('authModuleAtlassianProvider', () => { nonce: decodeURIComponent(nonceCookie.value), }); }); - it('should start with and use custom scopes from scope config field', async () => { + + it('should start with and use custom scopes from additionalScopes config field', async () => { const { server } = await startTestBackend({ features: [ import('@backstage/plugin-auth-backend'), @@ -91,7 +92,10 @@ describe('authModuleAtlassianProvider', () => { development: { clientId: 'my-client-id', clientSecret: 'my-client-secret', - scope: 'offline_access read:filter:jira read:jira-work', + additionalScopes: [ + 'read:filter:jira', + 'read:jira-work', // already required + ], }, }, }, @@ -123,7 +127,7 @@ describe('authModuleAtlassianProvider', () => { client_id: 'my-client-id', redirect_uri: `http://localhost:${server.port()}/api/auth/atlassian/handler/frame`, state: expect.any(String), - scope: 'offline_access read:filter:jira read:jira-work', + scope: 'offline_access read:jira-work read:jira-user read:filter:jira', }); expect(decodeOAuthState(startUrl.searchParams.get('state')!)).toEqual({ @@ -131,60 +135,35 @@ describe('authModuleAtlassianProvider', () => { nonce: decodeURIComponent(nonceCookie.value), }); }); - it('should start with and use custom scopes from scopes config field for backward compatibility', async () => { - const { server } = await startTestBackend({ - features: [ - import('@backstage/plugin-auth-backend'), - authModuleAtlassianProvider, - mockServices.rootConfig.factory({ - data: { - app: { - baseUrl: 'http://localhost:3000', - }, - auth: { - providers: { - atlassian: { - development: { - clientId: 'my-client-id', - clientSecret: 'my-client-secret', - scopes: 'offline_access read:filter:jira read:jira-work', + + it('should fail to start with scope or scopes config', async () => { + await expect( + startTestBackend({ + features: [ + import('@backstage/plugin-auth-backend'), + authModuleAtlassianProvider, + mockServices.rootConfig.factory({ + data: { + app: { + baseUrl: 'http://localhost:3000', + }, + auth: { + providers: { + atlassian: { + development: { + clientId: 'my-client-id', + clientSecret: 'my-client-secret', + scope: 'foo', + }, }, }, }, }, - }, - }), - ], - }); - - const agent = request.agent(server); - - const res = await agent.get('/api/auth/atlassian/start?env=development'); - - expect(res.status).toEqual(302); - - const nonceCookie = agent.jar.getCookie('atlassian-nonce', { - domain: 'localhost', - path: '/api/auth/atlassian/handler', - script: false, - secure: false, - }); - expect(nonceCookie).toBeDefined(); - - const startUrl = new URL(res.get('location')); - expect(startUrl.origin).toBe('https://auth.atlassian.com'); - expect(startUrl.pathname).toBe('/authorize'); - expect(Object.fromEntries(startUrl.searchParams)).toEqual({ - response_type: 'code', - client_id: 'my-client-id', - redirect_uri: `http://localhost:${server.port()}/api/auth/atlassian/handler/frame`, - state: expect.any(String), - scope: 'offline_access read:filter:jira read:jira-work', - }); - - expect(decodeOAuthState(startUrl.searchParams.get('state')!)).toEqual({ - env: 'development', - nonce: decodeURIComponent(nonceCookie.value), - }); + }), + ], + }), + ).rejects.toThrow( + /atlassian provider no longer supports the "scope" or "scopes" configuration options/, + ); }); }); diff --git a/plugins/auth-backend-module-bitbucket-provider/config.d.ts b/plugins/auth-backend-module-bitbucket-provider/config.d.ts index 81e835a5e6..17cac1c194 100644 --- a/plugins/auth-backend-module-bitbucket-provider/config.d.ts +++ b/plugins/auth-backend-module-bitbucket-provider/config.d.ts @@ -25,6 +25,7 @@ export interface Config { * @visibility secret */ clientSecret: string; + additionalScopes?: string | string[]; }; }; }; diff --git a/plugins/auth-backend-module-bitbucket-provider/src/authenticator.ts b/plugins/auth-backend-module-bitbucket-provider/src/authenticator.ts index bc962ec773..a2d8275964 100644 --- a/plugins/auth-backend-module-bitbucket-provider/src/authenticator.ts +++ b/plugins/auth-backend-module-bitbucket-provider/src/authenticator.ts @@ -26,6 +26,9 @@ import { export const bitbucketAuthenticator = createOAuthAuthenticator({ defaultProfileTransform: PassportOAuthAuthenticatorHelper.defaultProfileTransform, + scopes: { + required: ['account'], + }, initialize({ callbackUrl, config }) { const clientID = config.getString('clientId'); const clientSecret = config.getString('clientSecret'); diff --git a/plugins/auth-backend-module-bitbucket-provider/src/module.test.ts b/plugins/auth-backend-module-bitbucket-provider/src/module.test.ts index e761369a9c..f1426d1b0d 100644 --- a/plugins/auth-backend-module-bitbucket-provider/src/module.test.ts +++ b/plugins/auth-backend-module-bitbucket-provider/src/module.test.ts @@ -67,6 +67,7 @@ describe('authModuleBitbucketProvider', () => { client_id: 'my-client-id', redirect_uri: `http://localhost:${server.port()}/api/auth/bitbucket/handler/frame`, state: expect.any(String), + scope: 'account', }); expect(decodeOAuthState(startUrl.searchParams.get('state')!)).toEqual({ diff --git a/plugins/auth-backend-module-github-provider/config.d.ts b/plugins/auth-backend-module-github-provider/config.d.ts index e79711310d..f60d4c239d 100644 --- a/plugins/auth-backend-module-github-provider/config.d.ts +++ b/plugins/auth-backend-module-github-provider/config.d.ts @@ -27,6 +27,7 @@ export interface Config { clientSecret: string; callbackUrl?: string; enterpriseInstanceUrl?: string; + additionalScopes?: string | string[]; }; }; }; diff --git a/plugins/auth-backend-module-github-provider/src/authenticator.ts b/plugins/auth-backend-module-github-provider/src/authenticator.ts index 4a9710d114..7c94a04c30 100644 --- a/plugins/auth-backend-module-github-provider/src/authenticator.ts +++ b/plugins/auth-backend-module-github-provider/src/authenticator.ts @@ -28,7 +28,10 @@ const ACCESS_TOKEN_PREFIX = 'access-token.'; export const githubAuthenticator = createOAuthAuthenticator({ defaultProfileTransform: PassportOAuthAuthenticatorHelper.defaultProfileTransform, - shouldPersistScopes: true, + scopes: { + persist: true, + required: ['read:user'], + }, initialize({ callbackUrl, config }) { const clientId = config.getString('clientId'); const clientSecret = config.getString('clientSecret'); diff --git a/plugins/auth-backend-module-github-provider/src/module.test.ts b/plugins/auth-backend-module-github-provider/src/module.test.ts index b592f07d4b..53308938c0 100644 --- a/plugins/auth-backend-module-github-provider/src/module.test.ts +++ b/plugins/auth-backend-module-github-provider/src/module.test.ts @@ -67,12 +67,13 @@ describe('authModuleGithubProvider', () => { client_id: 'my-client-id', redirect_uri: `http://localhost:${server.port()}/api/auth/github/handler/frame`, state: expect.any(String), + scope: 'read:user', }); expect(decodeOAuthState(startUrl.searchParams.get('state')!)).toEqual({ env: 'development', nonce: decodeURIComponent(nonceCookie.value), - scope: '', + scope: 'read:user', }); }); }); diff --git a/plugins/auth-backend-module-gitlab-provider/config.d.ts b/plugins/auth-backend-module-gitlab-provider/config.d.ts index d7f04d872a..18f7cb5005 100644 --- a/plugins/auth-backend-module-gitlab-provider/config.d.ts +++ b/plugins/auth-backend-module-gitlab-provider/config.d.ts @@ -27,6 +27,7 @@ export interface Config { clientSecret: string; audience?: string; callbackUrl?: string; + additionalScopes?: string | string[]; }; }; }; diff --git a/plugins/auth-backend-module-gitlab-provider/src/authenticator.ts b/plugins/auth-backend-module-gitlab-provider/src/authenticator.ts index 9cd5857b0d..8e93921b63 100644 --- a/plugins/auth-backend-module-gitlab-provider/src/authenticator.ts +++ b/plugins/auth-backend-module-gitlab-provider/src/authenticator.ts @@ -26,6 +26,9 @@ import { export const gitlabAuthenticator = createOAuthAuthenticator({ defaultProfileTransform: PassportOAuthAuthenticatorHelper.defaultProfileTransform, + scopes: { + required: ['read_user'], + }, initialize({ callbackUrl, config }) { const clientId = config.getString('clientId'); const clientSecret = config.getString('clientSecret'); diff --git a/plugins/auth-backend-module-google-provider/config.d.ts b/plugins/auth-backend-module-google-provider/config.d.ts index 6a1716c5bf..08a0e08f2b 100644 --- a/plugins/auth-backend-module-google-provider/config.d.ts +++ b/plugins/auth-backend-module-google-provider/config.d.ts @@ -26,6 +26,7 @@ export interface Config { */ clientSecret: string; callbackUrl?: string; + additionalScopes?: string | string[]; }; }; }; diff --git a/plugins/auth-backend-module-google-provider/src/authenticator.ts b/plugins/auth-backend-module-google-provider/src/authenticator.ts index a428519fee..9f1e298ae8 100644 --- a/plugins/auth-backend-module-google-provider/src/authenticator.ts +++ b/plugins/auth-backend-module-google-provider/src/authenticator.ts @@ -27,6 +27,13 @@ import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; export const googleAuthenticator = createOAuthAuthenticator({ defaultProfileTransform: PassportOAuthAuthenticatorHelper.defaultProfileTransform, + scopes: { + required: [ + 'openid', + `https://www.googleapis.com/auth/userinfo.email`, + `https://www.googleapis.com/auth/userinfo.profile`, + ], + }, initialize({ callbackUrl, config }) { const clientId = config.getString('clientId'); const clientSecret = config.getString('clientSecret'); diff --git a/plugins/auth-backend-module-google-provider/src/module.test.ts b/plugins/auth-backend-module-google-provider/src/module.test.ts index 59066acd4c..8ece0c1942 100644 --- a/plugins/auth-backend-module-google-provider/src/module.test.ts +++ b/plugins/auth-backend-module-google-provider/src/module.test.ts @@ -69,6 +69,8 @@ describe('authModuleGoogleProvider', () => { client_id: 'my-client-id', redirect_uri: `http://localhost:${server.port()}/api/auth/google/handler/frame`, state: expect.any(String), + scope: + 'openid https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile', }); expect(decodeOAuthState(startUrl.searchParams.get('state')!)).toEqual({ diff --git a/plugins/auth-backend-module-microsoft-provider/config.d.ts b/plugins/auth-backend-module-microsoft-provider/config.d.ts index df63e9ccf4..a7ec027083 100644 --- a/plugins/auth-backend-module-microsoft-provider/config.d.ts +++ b/plugins/auth-backend-module-microsoft-provider/config.d.ts @@ -28,7 +28,7 @@ export interface Config { clientSecret: string; domainHint?: string; callbackUrl?: string; - additionalScopes?: string[]; + additionalScopes?: string | string[]; }; }; }; diff --git a/plugins/auth-backend-module-oauth2-provider/config.d.ts b/plugins/auth-backend-module-oauth2-provider/config.d.ts index afbfcf8f67..9b1ee18f1b 100644 --- a/plugins/auth-backend-module-oauth2-provider/config.d.ts +++ b/plugins/auth-backend-module-oauth2-provider/config.d.ts @@ -27,7 +27,9 @@ export interface Config { clientSecret: string; authorizationUrl: string; tokenUrl: string; + /** @deprecated use `additionalScopes` instead */ scope?: string; + additionalScopes?: string | string[]; disableRefresh?: boolean; includeBasicAuth?: boolean; }; diff --git a/plugins/auth-backend-module-oauth2-provider/src/authenticator.ts b/plugins/auth-backend-module-oauth2-provider/src/authenticator.ts index d6f84cf1c0..698d9e51cb 100644 --- a/plugins/auth-backend-module-oauth2-provider/src/authenticator.ts +++ b/plugins/auth-backend-module-oauth2-provider/src/authenticator.ts @@ -31,9 +31,14 @@ export const oauth2Authenticator = createOAuthAuthenticator({ const clientSecret = config.getString('clientSecret'); const authorizationUrl = config.getString('authorizationUrl'); const tokenUrl = config.getString('tokenUrl'); - const scope = config.getOptionalString('scope'); const includeBasicAuth = config.getOptionalBoolean('includeBasicAuth'); + if (config.has('scope')) { + throw new Error( + 'The oauth2 provider no longer supports the "scope" configuration option. Please use the "additionalScopes" option instead.', + ); + } + return PassportOAuthAuthenticatorHelper.from( new Oauth2Strategy( { @@ -43,7 +48,6 @@ export const oauth2Authenticator = createOAuthAuthenticator({ authorizationURL: authorizationUrl, tokenURL: tokenUrl, passReqToCallback: false, - scope: scope, customHeaders: includeBasicAuth ? { Authorization: `Basic ${encodeClientCredentials( diff --git a/plugins/auth-backend-module-oidc-provider/api-report.md b/plugins/auth-backend-module-oidc-provider/api-report.md index d96d9952a0..eb9b4fb21e 100644 --- a/plugins/auth-backend-module-oidc-provider/api-report.md +++ b/plugins/auth-backend-module-oidc-provider/api-report.md @@ -19,7 +19,6 @@ export default authModuleOidcProvider; // @public (undocumented) export const oidcAuthenticator: OAuthAuthenticator< { - initializedScope: string | undefined; initializedPrompt: string | undefined; promise: Promise<{ helper: PassportOAuthAuthenticatorHelper; diff --git a/plugins/auth-backend-module-oidc-provider/config.d.ts b/plugins/auth-backend-module-oidc-provider/config.d.ts index 16131c0761..398e63c466 100644 --- a/plugins/auth-backend-module-oidc-provider/config.d.ts +++ b/plugins/auth-backend-module-oidc-provider/config.d.ts @@ -29,7 +29,7 @@ export interface Config { callbackUrl?: string; tokenEndpointAuthMethod?: string; tokenSignedResponseAlg?: string; - scope?: string; + additionalScopes?: string | string[]; prompt?: string; }; }; diff --git a/plugins/auth-backend-module-oidc-provider/src/authenticator.test.ts b/plugins/auth-backend-module-oidc-provider/src/authenticator.test.ts index 67bfd27b19..4203a70153 100644 --- a/plugins/auth-backend-module-oidc-provider/src/authenticator.test.ts +++ b/plugins/auth-backend-module-oidc-provider/src/authenticator.test.ts @@ -254,19 +254,6 @@ describe('oidcAuthenticator', () => { expect(fakeSession['oidc:oidc.test'].code_verifier).toBeDefined(); }); - it('requests default scopes if none are provided in config', async () => { - const startResponse = await oidcAuthenticator.start( - startRequest, - implementation, - ); - const { searchParams } = new URL(startResponse.url); - const scopes = searchParams.get('scope')?.split(' ') ?? []; - - expect(scopes).toEqual( - expect.arrayContaining(['openid', 'profile', 'email']), - ); - }); - it('encodes OAuth state in query param', async () => { const startResponse = await oidcAuthenticator.start( startRequest, diff --git a/plugins/auth-backend-module-oidc-provider/src/authenticator.ts b/plugins/auth-backend-module-oidc-provider/src/authenticator.ts index 2b6a9b34f1..efcf1b61dd 100644 --- a/plugins/auth-backend-module-oidc-provider/src/authenticator.ts +++ b/plugins/auth-backend-module-oidc-provider/src/authenticator.ts @@ -53,7 +53,6 @@ export type OidcAuthResult = { /** @public */ export const oidcAuthenticator = createOAuthAuthenticator({ - shouldPersistScopes: true, defaultProfileTransform: async ( input: OAuthAuthenticatorResult, ) => ({ @@ -63,6 +62,10 @@ export const oidcAuthenticator = createOAuthAuthenticator({ displayName: input.fullProfile.userinfo.name, }, }), + scopes: { + persist: true, + required: ['openid', 'profile', 'email'], + }, initialize({ callbackUrl, config }) { const clientId = config.getString('clientId'); const clientSecret = config.getString('clientSecret'); @@ -74,9 +77,14 @@ export const oidcAuthenticator = createOAuthAuthenticator({ const tokenSignedResponseAlg = config.getOptionalString( 'tokenSignedResponseAlg', ); - const initializedScope = config.getOptionalString('scope'); const initializedPrompt = config.getOptionalString('prompt'); + if (config.has('scope')) { + throw new Error( + 'The oidc provider no longer supports the "scope" configuration option. Please use the "additionalScopes" option instead.', + ); + } + Issuer[custom.http_options] = httpOptionsProvider; const promise = Issuer.discover(metadataUrl).then(issuer => { issuer[custom.http_options] = httpOptionsProvider; @@ -91,7 +99,6 @@ export const oidcAuthenticator = createOAuthAuthenticator({ token_endpoint_auth_method: tokenEndpointAuthMethod || 'client_secret_basic', id_token_signed_response_alg: tokenSignedResponseAlg || 'RS256', - scope: initializedScope || '', }); client[custom.http_options] = httpOptionsProvider; @@ -123,14 +130,14 @@ export const oidcAuthenticator = createOAuthAuthenticator({ return { helper, client, strategy }; }); - return { initializedScope, initializedPrompt, promise }; + return { initializedPrompt, promise }; }, async start(input, ctx) { - const { initializedScope, initializedPrompt, promise } = ctx; + const { initializedPrompt, promise } = ctx; const { helper, strategy } = await promise; const options: Record = { - scope: input.scope || initializedScope || 'openid profile email', + scope: input.scope, state: input.state, nonce: crypto.randomBytes(16).toString('base64'), }; diff --git a/plugins/auth-backend-module-oidc-provider/src/module.test.ts b/plugins/auth-backend-module-oidc-provider/src/module.test.ts index 8ceec1df15..1359634950 100644 --- a/plugins/auth-backend-module-oidc-provider/src/module.test.ts +++ b/plugins/auth-backend-module-oidc-provider/src/module.test.ts @@ -212,7 +212,7 @@ describe('authModuleOidcProvider', () => { expect(decodeOAuthState(startUrl.searchParams.get('state')!)).toEqual({ env: 'development', nonce: decodeURIComponent(nonceCookie.value), - scope: '', + scope: 'openid profile email', }); }); diff --git a/plugins/auth-backend-module-okta-provider/config.d.ts b/plugins/auth-backend-module-okta-provider/config.d.ts index 42688528ee..aaf6ec1f91 100644 --- a/plugins/auth-backend-module-okta-provider/config.d.ts +++ b/plugins/auth-backend-module-okta-provider/config.d.ts @@ -29,7 +29,7 @@ export interface Config { authServerId?: string; idp?: string; callbackUrl?: string; - additionalScopes?: string; + additionalScopes?: string | string[]; }; }; }; diff --git a/plugins/auth-backend-module-okta-provider/src/authenticator.ts b/plugins/auth-backend-module-okta-provider/src/authenticator.ts index 42201232f1..1454d580cb 100644 --- a/plugins/auth-backend-module-okta-provider/src/authenticator.ts +++ b/plugins/auth-backend-module-okta-provider/src/authenticator.ts @@ -26,25 +26,15 @@ import { export const oktaAuthenticator = createOAuthAuthenticator({ defaultProfileTransform: PassportOAuthAuthenticatorHelper.defaultProfileTransform, + scopes: { + required: ['openid', 'email', 'profile', 'offline_access'], + }, initialize({ callbackUrl, config }) { const clientId = config.getString('clientId'); const clientSecret = config.getString('clientSecret'); const audience = config.getOptionalString('audience') || 'https://okta.com'; const authServerId = config.getOptionalString('authServerId'); const idp = config.getOptionalString('idp'); - // default scopes are taken from - // https://developer.okta.com/docs/reference/api/oidc/#response-example-success-refresh-token - const defaultScopes = 'openid profile email'; - // additional scopes can be configured in the config as a space separated string - const additionalScopes = config.getOptionalString('additionalScopes') || ''; - // combine default and additional scopes and remove duplicates - const combineScopeStrings = (scopesA: string, scopesB: string) => { - const scopesAArray = scopesA.split(' '); - const scopesBArray = scopesB.split(' '); - const combinedScopes = new Set([...scopesAArray, ...scopesBArray]); - return Array.from(combinedScopes).join(' '); - }; - const scope = combineScopeStrings(defaultScopes, additionalScopes); return PassportOAuthAuthenticatorHelper.from( new OktaStrategy( @@ -57,7 +47,6 @@ export const oktaAuthenticator = createOAuthAuthenticator({ idp: idp, passReqToCallback: false, response_type: 'code', - scope, }, ( accessToken: string, diff --git a/plugins/auth-backend-module-okta-provider/src/module.test.ts b/plugins/auth-backend-module-okta-provider/src/module.test.ts index ee410e4d2e..16a58b8713 100644 --- a/plugins/auth-backend-module-okta-provider/src/module.test.ts +++ b/plugins/auth-backend-module-okta-provider/src/module.test.ts @@ -21,7 +21,6 @@ import { decodeOAuthState } from '@backstage/plugin-auth-node'; describe('authModuleOktaProvider', () => { it('should start', async () => { - const additionalScopes = 'groups phone'; const { server } = await startTestBackend({ features: [ import('@backstage/plugin-auth-backend'), @@ -37,7 +36,7 @@ describe('authModuleOktaProvider', () => { development: { clientId: 'my-client-id', clientSecret: 'my-client-secret', - additionalScopes, + additionalScopes: 'groups phone', }, }, }, @@ -66,7 +65,7 @@ describe('authModuleOktaProvider', () => { expect(startUrl.pathname).toBe('/oauth2/v1/authorize'); expect(Object.fromEntries(startUrl.searchParams)).toEqual({ response_type: 'code', - scope: additionalScopes, + scope: 'openid email profile offline_access groups phone', client_id: 'my-client-id', redirect_uri: `http://localhost:${server.port()}/api/auth/okta/handler/frame`, state: expect.any(String), diff --git a/plugins/auth-backend-module-pinniped-provider/src/authenticator.test.ts b/plugins/auth-backend-module-pinniped-provider/src/authenticator.test.ts index 2ecd54e80a..e2dedf2129 100644 --- a/plugins/auth-backend-module-pinniped-provider/src/authenticator.test.ts +++ b/plugins/auth-backend-module-pinniped-provider/src/authenticator.test.ts @@ -249,22 +249,14 @@ describe('pinnipedAuthenticator', () => { expect(fakeSession['oidc:pinniped.test'].code_verifier).toBeDefined(); }); - it('requests sufficient scopes for token exchange by default', async () => { + it('forwards scopes for token exchange', async () => { const startResponse = await pinnipedAuthenticator.start( - startRequest, + { ...startRequest, scope: 'openid username' }, authCtx, ); const { searchParams } = new URL(startResponse.url); - const scopes = searchParams.get('scope')?.split(' ') ?? []; - expect(scopes).toEqual( - expect.arrayContaining([ - 'openid', - 'pinniped:request-audience', - 'username', - 'offline_access', - ]), - ); + expect(searchParams.get('scope')).toBe('openid username'); }); it('encodes OAuth state in query param', async () => { diff --git a/plugins/auth-backend-module-pinniped-provider/src/authenticator.ts b/plugins/auth-backend-module-pinniped-provider/src/authenticator.ts index e07eaba759..abacb14ccc 100644 --- a/plugins/auth-backend-module-pinniped-provider/src/authenticator.ts +++ b/plugins/auth-backend-module-pinniped-provider/src/authenticator.ts @@ -127,7 +127,6 @@ export class PinnipedStrategyCache { client_secret: this.config.getString('clientSecret'), redirect_uris: [this.callbackUrl], response_types: ['code'], - scope: this.config.getOptionalString('scope') || '', id_token_signed_response_alg: 'ES256', }); const providerStrategy = new OidcStrategy( @@ -154,7 +153,20 @@ export class PinnipedStrategyCache { /** @public */ export const pinnipedAuthenticator = createOAuthAuthenticator({ defaultProfileTransform: async (_r, _c) => ({ profile: {} }), + scopes: { + required: [ + 'openid', + 'pinniped:request-audience', + 'username', + 'offline_access', + ], + }, initialize({ callbackUrl, config }) { + if (config.has('scope')) { + throw new Error( + 'The pinniped provider no longer supports the "scope" configuration option. Please use the "additionalScopes" option instead.', + ); + } return new PinnipedStrategyCache(callbackUrl, config); }, async start(input, ctx): Promise<{ url: string; status?: number }> { @@ -163,9 +175,7 @@ export const pinnipedAuthenticator = createOAuthAuthenticator({ const decodedState = decodeOAuthState(input.state); const state = { ...decodedState, audience: stringifiedAudience }; const options: Record = { - scope: - input.scope || - 'openid pinniped:request-audience username offline_access', + scope: input.scope, state: encodeOAuthState(state), }; diff --git a/plugins/auth-backend-module-vmware-cloud-provider/config.d.ts b/plugins/auth-backend-module-vmware-cloud-provider/config.d.ts index 10b3566899..6ea07f1936 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/config.d.ts +++ b/plugins/auth-backend-module-vmware-cloud-provider/config.d.ts @@ -24,6 +24,7 @@ export interface Config { organizationId: string; scope?: string; consoleEndpoint?: string; + additionalScopes?: string | string[]; }; }; }; diff --git a/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.test.ts b/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.test.ts index f38ceeb462..c69d4e9ab1 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.test.ts +++ b/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.test.ts @@ -163,9 +163,9 @@ describe('vmwareCloudAuthenticator', () => { expect(searchParams.get('redirect_uri')).toBe('http://callbackUrl'); }); - it('requests scopes for ID and refresh token', async () => { + it('forwards scopes for ID and refresh token', async () => { const startResponse = await vmwareCloudAuthenticator.start( - startRequest, + { ...startRequest, scope: 'openid offline_access' }, authenticatorCtx, ); const { searchParams } = new URL(startResponse.url); diff --git a/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.ts b/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.ts index 4db4836f13..55b3b5daec 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.ts +++ b/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.ts @@ -96,6 +96,9 @@ export const vmwareCloudAuthenticator = createOAuthAuthenticator< }, }; }, + scopes: { + required: ['openid', 'offline_access'], + }, initialize({ callbackUrl, config }) { const consoleEndpoint = config.getOptionalString('consoleEndpoint') ?? @@ -106,7 +109,12 @@ export const vmwareCloudAuthenticator = createOAuthAuthenticator< const clientSecret = ''; const authorizationUrl = `${consoleEndpoint}/csp/gateway/discovery`; const tokenUrl = `${consoleEndpoint}/csp/gateway/am/api/auth/token`; - const scope = config.getOptionalString('scope') ?? 'openid offline_access'; + + if (config.has('scope')) { + throw new Error( + 'The vmware-cloud provider no longer supports the "scope" configuration option. Please use the "additionalScopes" option instead.', + ); + } const providerStrategy = new OAuth2Strategy( { @@ -118,7 +126,6 @@ export const vmwareCloudAuthenticator = createOAuthAuthenticator< passReqToCallback: false, pkce: true, state: true, - scope: scope, customHeaders: { Authorization: `Basic ${encodeClientCredentials( clientId,