From 269b4c1786200603268524b74e032b20bcc4cfce Mon Sep 17 00:00:00 2001 From: Spencer Post Date: Thu, 28 Mar 2024 19:34:12 -0600 Subject: [PATCH 1/3] pass scopes to strategy Signed-off-by: Spencer Post --- .changeset/curly-seals-pull.md | 5 ++ .../config.d.ts | 1 + .../src/authenticator.ts | 4 ++ .../src/module.test.ts | 57 +++++++++++++++++++ 4 files changed, 67 insertions(+) create mode 100644 .changeset/curly-seals-pull.md diff --git a/.changeset/curly-seals-pull.md b/.changeset/curly-seals-pull.md new file mode 100644 index 0000000000..eba8b5c0b2 --- /dev/null +++ b/.changeset/curly-seals-pull.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend-module-atlassian-provider': patch +--- + +Read scopes from config and pass to AtlassianProvider as they are required diff --git a/plugins/auth-backend-module-atlassian-provider/config.d.ts b/plugins/auth-backend-module-atlassian-provider/config.d.ts index 999bf1b304..57def03aab 100644 --- a/plugins/auth-backend-module-atlassian-provider/config.d.ts +++ b/plugins/auth-backend-module-atlassian-provider/config.d.ts @@ -27,6 +27,7 @@ export interface Config { clientSecret: string; audience?: string; callbackUrl?: string; + scope?: string; }; }; }; diff --git a/plugins/auth-backend-module-atlassian-provider/src/authenticator.ts b/plugins/auth-backend-module-atlassian-provider/src/authenticator.ts index 340c61052c..0538d8d742 100644 --- a/plugins/auth-backend-module-atlassian-provider/src/authenticator.ts +++ b/plugins/auth-backend-module-atlassian-provider/src/authenticator.ts @@ -29,6 +29,9 @@ export const atlassianAuthenticator = createOAuthAuthenticator({ initialize({ callbackUrl, config }) { const clientId = config.getString('clientId'); const clientSecret = config.getString('clientSecret'); + const scope = + config.getOptionalString('scope') || + 'offline_access read:jira-work read:jira-user'; const baseUrl = 'https://auth.atlassian.com'; return PassportOAuthAuthenticatorHelper.from( @@ -41,6 +44,7 @@ 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 024a45d43d..213df4b0fc 100644 --- a/plugins/auth-backend-module-atlassian-provider/src/module.test.ts +++ b/plugins/auth-backend-module-atlassian-provider/src/module.test.ts @@ -67,6 +67,63 @@ 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:jira-work read:jira-user', + }); + + expect(decodeOAuthState(startUrl.searchParams.get('state')!)).toEqual({ + env: 'development', + nonce: decodeURIComponent(nonceCookie.value), + }); + }); + it('should start with and use custom scopes', 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', + scope: 'offline_access read:filter:jira read:jira-work', + }, + }, + }, + }, + }, + }), + ], + }); + + 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({ From 9a0859a0d39564717e0cfb06ab80707c8ee0040f Mon Sep 17 00:00:00 2001 From: Spencer Post Date: Fri, 29 Mar 2024 13:51:03 -0600 Subject: [PATCH 2/3] update documentation Signed-off-by: Spencer Post --- docs/auth/atlassian/provider.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/auth/atlassian/provider.md b/docs/auth/atlassian/provider.md index cded6e73ca..2680370d93 100644 --- a/docs/auth/atlassian/provider.md +++ b/docs/auth/atlassian/provider.md @@ -46,16 +46,16 @@ auth: development: clientId: ${AUTH_ATLASSIAN_CLIENT_ID} clientSecret: ${AUTH_ATLASSIAN_CLIENT_SECRET} - scopes: ${AUTH_ATLASSIAN_SCOPES} + scope: ${AUTH_ATLASSIAN_SCOPES} ``` The Atlassian provider is a structure with three configuration keys: - `clientId`: The Key you generated in the developer console. - `clientSecret`: The Secret tied to the generated Key. -- `scopes`: List of scopes the app has permissions for, separated by spaces. +- `scope`: List of scopes the app has permissions for, separated by spaces. -**NOTE:** the scopes `offline_access` and `read:me` are provided by default. +**NOTE:** the scopes `offline_access`, `read:jira-work`, and `read:jira-user` are provided by default. ## Adding the provider to the Backstage frontend From 7a7e10388257f12c9fda022d6aba8502844926a6 Mon Sep 17 00:00:00 2001 From: Spencer Post Date: Fri, 29 Mar 2024 14:53:48 -0600 Subject: [PATCH 3/3] support scope and scopes in config Signed-off-by: Spencer Post --- app-config.yaml | 2 +- .../src/authenticator.ts | 3 +- .../src/module.test.ts | 58 ++++++++++++++++++- 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index 7950768350..c016260bf8 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -396,7 +396,7 @@ auth: development: clientId: ${AUTH_ATLASSIAN_CLIENT_ID} clientSecret: ${AUTH_ATLASSIAN_CLIENT_SECRET} - scopes: ${AUTH_ATLASSIAN_SCOPES} + scope: ${AUTH_ATLASSIAN_SCOPES} myproxy: {} guest: {} diff --git a/plugins/auth-backend-module-atlassian-provider/src/authenticator.ts b/plugins/auth-backend-module-atlassian-provider/src/authenticator.ts index 0538d8d742..b31de13134 100644 --- a/plugins/auth-backend-module-atlassian-provider/src/authenticator.ts +++ b/plugins/auth-backend-module-atlassian-provider/src/authenticator.ts @@ -30,7 +30,8 @@ export const atlassianAuthenticator = createOAuthAuthenticator({ const clientId = config.getString('clientId'); const clientSecret = config.getString('clientSecret'); const scope = - config.getOptionalString('scope') || + config.getOptionalString('scope') ?? + config.getOptionalString('scopes') ?? 'offline_access read:jira-work read:jira-user'; const baseUrl = 'https://auth.atlassian.com'; 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 213df4b0fc..08b84ad165 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,7 @@ describe('authModuleAtlassianProvider', () => { nonce: decodeURIComponent(nonceCookie.value), }); }); - it('should start with and use custom scopes', async () => { + it('should start with and use custom scopes from scope config field', async () => { const { server } = await startTestBackend({ features: [ import('@backstage/plugin-auth-backend'), @@ -126,6 +126,62 @@ describe('authModuleAtlassianProvider', () => { scope: 'offline_access read:filter:jira read:jira-work', }); + expect(decodeOAuthState(startUrl.searchParams.get('state')!)).toEqual({ + env: 'development', + 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', + }, + }, + }, + }, + }, + }), + ], + }); + + 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),