From 83941bb6179f91bb115b8923d385f5dcf95d4429 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 8 Aug 2023 13:53:49 +0200 Subject: [PATCH] auth-node: add initial OAuth route handlers test Signed-off-by: Patrik Oldsberg --- plugins/auth-node/package.json | 3 + .../oauth/createOAuthRouteHandlers.test.ts | 156 ++++++++++++++++++ yarn.lock | 5 +- 3 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 plugins/auth-node/src/oauth/createOAuthRouteHandlers.test.ts diff --git a/plugins/auth-node/package.json b/plugins/auth-node/package.json index b0c503c144..5e3ddd5a20 100644 --- a/plugins/auth-node/package.json +++ b/plugins/auth-node/package.json @@ -48,8 +48,11 @@ "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", + "cookie-parser": "^1.4.6", + "express-promise-router": "^4.1.1", "lodash": "^4.17.21", "msw": "^1.0.0", + "supertest": "^6.1.3", "uuid": "^8.0.0" }, "files": [ diff --git a/plugins/auth-node/src/oauth/createOAuthRouteHandlers.test.ts b/plugins/auth-node/src/oauth/createOAuthRouteHandlers.test.ts new file mode 100644 index 0000000000..46b6fe4b07 --- /dev/null +++ b/plugins/auth-node/src/oauth/createOAuthRouteHandlers.test.ts @@ -0,0 +1,156 @@ +/* + * 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 { ConfigReader } from '@backstage/config'; +import express from 'express'; +import request, { SuperAgentTest } from 'supertest'; +import cookieParser from 'cookie-parser'; +import PromiseRouter from 'express-promise-router'; +import { AuthProviderRouteHandlers, AuthResolverContext } from '../types'; +import { createOAuthRouteHandlers } from './createOAuthRouteHandlers'; +import { OAuthAuthenticator } from './types'; +import { errorHandler } from '@backstage/backend-common'; + +const mockAuthenticator: OAuthAuthenticator = { + initialize: jest.fn(), + start: jest.fn(), + authenticate: jest.fn(), + refresh: jest.fn(), + logout: jest.fn(), + defaultProfileTransform: jest.fn(async () => ({ profile: {} })), +}; + +const baseConfig = { + authenticator: mockAuthenticator, + appUrl: 'http://localhost:3000', + baseUrl: 'http://localhost:7007', + isOriginAllowed: () => true, + providerId: 'my-provider', + config: new ConfigReader({}), + resolverContext: { ctx: 'ctx' } as unknown as AuthResolverContext, +}; + +function wrapInApp(handlers: AuthProviderRouteHandlers) { + const app = express(); + + const router = PromiseRouter(); + + router.use(cookieParser()); + app.use(router); + app.use(errorHandler()); + + router.get('/start', handlers.start.bind(handlers)); + router.get('/handler/frame', handlers.frameHandler.bind(handlers)); + router.post('/handler/frame', handlers.frameHandler.bind(handlers)); + if (handlers.logout) { + router.post('/logout', handlers.logout.bind(handlers)); + } + if (handlers.refresh) { + router.get('/refresh', handlers.refresh.bind(handlers)); + router.post('/refresh', handlers.refresh.bind(handlers)); + } + + return app; +} + +function getCookie(test: SuperAgentTest, name: string) { + return test.jar.getCookie(`my-provider-${name}`, { + domain: 'localhost', + path: '/my-provider', + script: false, + secure: false, + }); +} + +describe('createOAuthRouteHandlers', () => { + it('should be created', () => { + const handlers = createOAuthRouteHandlers(baseConfig); + expect(handlers).toEqual({ + start: expect.any(Function), + frameHandler: expect.any(Function), + refresh: expect.any(Function), + logout: expect.any(Function), + }); + }); + + describe('start', () => { + it('should require an env query', async () => { + const app = wrapInApp(createOAuthRouteHandlers(baseConfig)); + const res = await request(app).get('/start'); + + expect(res.status).toBe(400); + expect(res.body).toMatchObject({ + error: { + name: 'InputError', + message: 'No env provided in request query parameters', + }, + }); + }); + }); + + describe('logout', () => { + it('should log out', async () => { + const agent = request.agent( + wrapInApp(createOAuthRouteHandlers(baseConfig)), + ); + + agent.jar.setCookie( + 'my-provider-refresh-token=my-refresh-token', + 'localhost', + '/my-provider', + ); + + expect(getCookie(agent, 'refresh-token').value).toBe('my-refresh-token'); + + const res = await agent + .post('/logout') + .set('X-Requested-With', 'XMLHttpRequest'); + + expect(res.status).toBe(200); + expect(res.body).toEqual({}); + + expect(getCookie(agent, 'refresh-token')).toBeUndefined(); + }); + + it('should reject requests without CSRF header', async () => { + const app = wrapInApp(createOAuthRouteHandlers(baseConfig)); + + const res = await request(app).post('/logout'); + expect(res.status).toBe(401); + expect(res.body).toMatchObject({ + error: { + name: 'AuthenticationError', + message: 'Invalid X-Requested-With header', + }, + }); + }); + + it('should reject requests with invalid CSRF header', async () => { + const app = wrapInApp(createOAuthRouteHandlers(baseConfig)); + + const res = await request(app) + .post('/logout') + .set('X-Requested-With', 'wrong-value'); + expect(res.status).toBe(401); + expect(res.body).toMatchObject({ + error: { + name: 'AuthenticationError', + message: 'Invalid X-Requested-With header', + }, + }); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 2a561a603f..2dd5f941c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4691,12 +4691,15 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/types": "workspace:^" "@types/express": "*" + cookie-parser: ^1.4.6 express: ^4.17.1 + express-promise-router: ^4.1.1 jose: ^4.6.0 lodash: ^4.17.21 msw: ^1.0.0 node-fetch: ^2.6.7 passport: ^0.6.0 + supertest: ^6.1.3 uuid: ^8.0.0 winston: ^3.2.1 zod: ^3.21.4 @@ -22029,7 +22032,7 @@ __metadata: languageName: node linkType: hard -"cookie-parser@npm:^1.4.5": +"cookie-parser@npm:^1.4.5, cookie-parser@npm:^1.4.6": version: 1.4.6 resolution: "cookie-parser@npm:1.4.6" dependencies: