diff --git a/plugins/auth-backend-module-github-provider/src/authenticator.test.ts b/plugins/auth-backend-module-github-provider/src/authenticator.test.ts new file mode 100644 index 0000000000..dc5c7208ea --- /dev/null +++ b/plugins/auth-backend-module-github-provider/src/authenticator.test.ts @@ -0,0 +1,142 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + PassportOAuthAuthenticatorHelper, + PassportProfile, +} from '@backstage/plugin-auth-node'; +import { githubAuthenticator } from './authenticator'; + +describe('githubAuthenticator', () => { + it('should store access token without expiration as refresh token', async () => { + await expect( + githubAuthenticator.authenticate( + {} as any, + { + authenticate: async _input => ({ + fullProfile: { id: 'id' } as PassportProfile, + session: { + accessToken: 'my-token', + scope: 'user:read', + tokenType: 'bearer', + }, + }), + } as PassportOAuthAuthenticatorHelper, + ), + ).resolves.toEqual({ + fullProfile: { id: 'id' }, + session: { + accessToken: 'my-token', + scope: 'user:read', + tokenType: 'bearer', + refreshToken: 'access-token.my-token', + }, + }); + }); + + it('should not use access token as refresh token if it expires', async () => { + await expect( + githubAuthenticator.authenticate( + {} as any, + { + authenticate: async _input => ({ + fullProfile: { id: 'id' } as PassportProfile, + session: { + accessToken: 'my-token', + scope: 'user:read', + tokenType: 'bearer', + expiresInSeconds: 3, + }, + }), + } as PassportOAuthAuthenticatorHelper, + ), + ).resolves.toEqual({ + fullProfile: { id: 'id' }, + session: { + accessToken: 'my-token', + scope: 'user:read', + tokenType: 'bearer', + expiresInSeconds: 3, + }, + }); + }); + + it('should not store access token without expiration if a refresh token is provided', async () => { + await expect( + githubAuthenticator.authenticate( + {} as any, + { + authenticate: async _input => ({ + fullProfile: { id: 'id' } as PassportProfile, + session: { + accessToken: 'my-token', + scope: 'user:read', + tokenType: 'bearer', + refreshToken: 'my-refresh-token', + }, + }), + } as PassportOAuthAuthenticatorHelper, + ), + ).resolves.toEqual({ + fullProfile: { id: 'id' }, + session: { + accessToken: 'my-token', + scope: 'user:read', + tokenType: 'bearer', + refreshToken: 'my-refresh-token', + }, + }); + }); + + it('should refresh with access token', async () => { + await expect( + githubAuthenticator.refresh( + { + refreshToken: 'access-token.my-token', + req: {} as any, + scope: 'user:read', + }, + { + fetchProfile: async _input => ({ id: 'id' } as PassportProfile), + } as PassportOAuthAuthenticatorHelper, + ), + ).resolves.toEqual({ + fullProfile: { id: 'id' }, + session: { + accessToken: 'my-token', + scope: 'user:read', + tokenType: 'bearer', + refreshToken: 'access-token.my-token', + }, + }); + }); + + it('should refresh with refresh token', async () => { + const res = {}; + await expect( + githubAuthenticator.refresh( + { + refreshToken: 'my-refresh-token', + req: {} as any, + scope: 'user:read', + }, + { + refresh: async _input => res as any, + } as PassportOAuthAuthenticatorHelper, + ), + ).resolves.toBe(res); + }); +}); diff --git a/plugins/auth-backend-module-github-provider/src/module.test.ts b/plugins/auth-backend-module-github-provider/src/module.test.ts new file mode 100644 index 0000000000..d4fc36cbc6 --- /dev/null +++ b/plugins/auth-backend-module-github-provider/src/module.test.ts @@ -0,0 +1,78 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; +import { authPlugin } from '@backstage/plugin-auth-backend'; +import { authModuleGithubProvider } from './module'; +import request from 'supertest'; +import { decodeOAuthState } from '@backstage/plugin-auth-node'; + +describe('authModuleGithubProvider', () => { + it('should start', async () => { + const { server } = await startTestBackend({ + features: [ + authPlugin, + authModuleGithubProvider, + mockServices.rootConfig.factory({ + data: { + app: { + baseUrl: 'http://localhost:3000', + }, + auth: { + providers: { + github: { + development: { + clientId: 'my-client-id', + clientSecret: 'my-client-secret', + }, + }, + }, + }, + }, + }), + ], + }); + + const agent = request.agent(server); + + const res = await agent.get('/api/auth/github/start?env=development'); + + expect(res.status).toEqual(302); + + const nonceCookie = agent.jar.getCookie('github-nonce', { + domain: 'localhost', + path: '/api/auth/github/handler', + script: false, + secure: false, + }); + expect(nonceCookie).toBeDefined(); + + const startUrl = new URL(res.get('location')); + expect(startUrl.origin).toBe('https://github.com'); + expect(startUrl.pathname).toBe('/login/oauth/authorize'); + expect(Object.fromEntries(startUrl.searchParams)).toEqual({ + response_type: 'code', + client_id: 'my-client-id', + redirect_uri: `http://localhost:${server.port()}/api/auth/github/handler/frame`, + state: expect.any(String), + }); + + expect(decodeOAuthState(startUrl.searchParams.get('state')!)).toEqual({ + env: 'development', + nonce: decodeURIComponent(nonceCookie.value), + }); + }); +});