diff --git a/plugins/auth-node/src/oauth/createOAuthRouteHandlers.test.ts b/plugins/auth-node/src/oauth/createOAuthRouteHandlers.test.ts index c0d9cfcb40..926d08fa77 100644 --- a/plugins/auth-node/src/oauth/createOAuthRouteHandlers.test.ts +++ b/plugins/auth-node/src/oauth/createOAuthRouteHandlers.test.ts @@ -255,6 +255,11 @@ describe('createOAuthRouteHandlers', () => { } as OAuthState), }); + expect(mockAuthenticator.authenticate).toHaveBeenCalledWith( + { req: expect.anything() }, + { ctx: 'authenticator' }, + ); + expect(res.status).toBe(200); expect(parseWebMessageResponse(res.text).response).toEqual({ type: 'authorization_response', @@ -515,6 +520,184 @@ describe('createOAuthRouteHandlers', () => { }); }); + describe('refresh', () => { + it('should refresh', async () => { + const agent = request.agent( + wrapInApp(createOAuthRouteHandlers(baseConfig)), + ); + + agent.jar.setCookie( + 'my-provider-refresh-token=refresh-token', + '127.0.0.1', + '/my-provider', + ); + + mockAuthenticator.refresh.mockImplementation(async ({ scope }) => ({ + fullProfile: { id: 'id' } as PassportProfile, + session: { ...mockSession, scope }, + })); + + const res = await agent + .post('/my-provider/refresh') + .set('X-Requested-With', 'XMLHttpRequest') + .query({ scope: 'my-scope' }); + + expect(mockAuthenticator.refresh).toHaveBeenCalledWith( + { + req: expect.anything(), + refreshToken: 'refresh-token', + scope: 'my-scope', + }, + { ctx: 'authenticator' }, + ); + + expect(res.status).toBe(200); + expect(res.body).toEqual({ + profile: {}, + providerInfo: { + accessToken: 'access-token', + expiresInSeconds: 3, + idToken: 'id-token', + scope: 'my-scope', + }, + }); + }); + + it('should refresh with sign-in, profile transform, and persisted scopes', async () => { + const agent = request.agent( + wrapInApp( + createOAuthRouteHandlers({ + ...baseConfig, + authenticator: { + ...mockAuthenticator, + shouldPersistScopes: true, + }, + profileTransform: async () => ({ profile: { email: 'em@i.l' } }), + signInResolver: async () => ({ token: mockBackstageToken }), + }), + ), + ); + + agent.jar.setCookie( + 'my-provider-refresh-token=refresh-token', + '127.0.0.1', + '/my-provider', + ); + agent.jar.setCookie( + 'my-provider-granted-scope=persisted-scope', + '127.0.0.1', + '/my-provider', + ); + + mockAuthenticator.refresh.mockImplementation(async ({ scope }) => ({ + fullProfile: { id: 'id' } as PassportProfile, + session: { ...mockSession, scope, refreshToken: 'new-refresh-token' }, + })); + + const res = await agent + .post('/my-provider/refresh') + .set('X-Requested-With', 'XMLHttpRequest'); + + expect(mockAuthenticator.refresh).toHaveBeenCalledWith( + { + req: expect.anything(), + refreshToken: 'refresh-token', + scope: 'persisted-scope', + }, + { ctx: 'authenticator' }, + ); + + expect(res.status).toBe(200); + expect(res.body).toEqual({ + profile: { email: 'em@i.l' }, + providerInfo: { + accessToken: 'access-token', + expiresInSeconds: 3, + idToken: 'id-token', + scope: 'persisted-scope', + }, + backstageIdentity: { + identity: { + type: 'user', + ownershipEntityRefs: [], + userEntityRef: 'user:default/mock', + }, + token: mockBackstageToken, + }, + }); + expect(getRefreshTokenCookie(agent).value).toBe('new-refresh-token'); + }); + + it('should forward errors', async () => { + const agent = request.agent( + wrapInApp(createOAuthRouteHandlers(baseConfig)), + ); + + agent.jar.setCookie( + 'my-provider-refresh-token=refresh-token', + '127.0.0.1', + '/my-provider', + ); + + mockAuthenticator.refresh.mockRejectedValue(new Error('NOPE')); + + const res = await agent + .post('/my-provider/refresh') + .set('X-Requested-With', 'XMLHttpRequest'); + + expect(res.status).toBe(401); + expect(res.body).toMatchObject({ + error: { + name: 'AuthenticationError', + message: 'Refresh failed; caused by Error: NOPE', + }, + }); + }); + + it('should require refresh cookie', async () => { + const res = await request(wrapInApp(createOAuthRouteHandlers(baseConfig))) + .post('/my-provider/refresh') + .set('X-Requested-With', 'XMLHttpRequest'); + + expect(res.status).toBe(401); + expect(res.body).toMatchObject({ + error: { + name: 'AuthenticationError', + message: + 'Refresh failed; caused by InputError: Missing session cookie', + }, + }); + }); + + it('should reject requests without CSRF header', async () => { + const res = await request( + wrapInApp(createOAuthRouteHandlers(baseConfig)), + ).post('/my-provider/refresh'); + + 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 res = await request(wrapInApp(createOAuthRouteHandlers(baseConfig))) + .post('/my-provider/refresh') + .set('X-Requested-With', 'invalid'); + + expect(res.status).toBe(401); + expect(res.body).toMatchObject({ + error: { + name: 'AuthenticationError', + message: 'Invalid X-Requested-With header', + }, + }); + }); + }); + describe('logout', () => { it('should log out', async () => { const agent = request.agent(