From 7289b5ec4eaf8d6e01fd45ad61649762d114fb9e Mon Sep 17 00:00:00 2001 From: Alessandro Dalfovo Date: Tue, 13 Sep 2022 16:27:30 +0200 Subject: [PATCH] Call logout OAuth handler in OAuthAdapter Signed-off-by: Alessandro Dalfovo --- .../src/lib/oauth/OAuthAdapter.test.ts | 10 ++++++++-- .../auth-backend/src/lib/oauth/OAuthAdapter.ts | 16 ++++++++++++++-- plugins/auth-backend/src/lib/oauth/index.ts | 1 + plugins/auth-backend/src/lib/oauth/types.ts | 7 ++++++- .../src/providers/google/provider.ts | 7 +++++++ 5 files changed, 36 insertions(+), 5 deletions(-) diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts index 526b05d0ab..e0d4f0e051 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts @@ -17,7 +17,7 @@ import express from 'express'; import { THOUSAND_DAYS_MS, TEN_MINUTES_MS, OAuthAdapter } from './OAuthAdapter'; import { encodeState } from './helpers'; -import { OAuthHandlers, OAuthState } from './types'; +import { OAuthHandlers, OAuthLogoutRequest, OAuthState } from './types'; import { CookieConfigurer } from '../../providers/types'; const mockResponseData = { @@ -60,6 +60,7 @@ describe('OAuthAdapter', () => { refreshToken: 'token', }; } + async logout(_: OAuthLogoutRequest) {} } const providerInstance = new MyAuthProvider(); const mockCookieConfig: ReturnType = { @@ -262,13 +263,17 @@ describe('OAuthAdapter', () => { ); }); - it('removes refresh cookie when logging out', async () => { + it('removes refresh cookie and calls logout handler when logging out', async () => { + const logoutSpy = jest.spyOn(providerInstance, 'logout'); const oauthProvider = new OAuthAdapter(providerInstance, { ...oAuthProviderOptions, isOriginAllowed: () => false, }); const mockRequest = { + cookies: { + 'test-provider-refresh-token': 'token', + }, header: () => 'XMLHttpRequest', get: jest.fn(), } as unknown as express.Request; @@ -281,6 +286,7 @@ describe('OAuthAdapter', () => { await oauthProvider.logout(mockRequest, mockResponse); expect(mockRequest.get).toHaveBeenCalledTimes(1); + expect(logoutSpy).toHaveBeenCalledTimes(1); expect(mockResponse.cookie).toHaveBeenCalledTimes(1); expect(mockResponse.cookie).toHaveBeenCalledWith( expect.stringContaining('test-provider-refresh-token'), diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts index 8f08cfbbd0..85104e9608 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts @@ -39,6 +39,7 @@ import { OAuthStartRequest, OAuthRefreshRequest, OAuthState, + OAuthLogoutRequest, } from './types'; import { prepareBackstageIdentityResponse } from '../../providers/prepareBackstageIdentityResponse'; @@ -190,6 +191,14 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { throw new AuthenticationError('Invalid X-Requested-With header'); } + if (this.handlers.logout) { + const refreshToken = this.getRefreshTokenFromCookie(req); + const revokeRequest: OAuthLogoutRequest = Object.assign(req, { + refreshToken, + }); + await this.handlers.logout(revokeRequest); + } + // remove refresh token cookie if it is set const origin = req.get('origin'); const cookieConfig = this.getCookieConfig(origin); @@ -210,8 +219,7 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { } try { - const refreshToken = - req.cookies[`${this.options.providerId}-refresh-token`]; + const refreshToken = this.getRefreshTokenFromCookie(req); // throw error if refresh token is missing in the request if (!refreshToken) { @@ -286,6 +294,10 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { }); }; + private getRefreshTokenFromCookie = (req: express.Request) => { + return req.cookies[`${this.options.providerId}-refresh-token`]; + }; + private getGrantedScopeFromCookie = (req: express.Request) => { return req.cookies[`${this.options.providerId}-granted-scope`]; }; diff --git a/plugins/auth-backend/src/lib/oauth/index.ts b/plugins/auth-backend/src/lib/oauth/index.ts index 671e8e48e9..3643898bed 100644 --- a/plugins/auth-backend/src/lib/oauth/index.ts +++ b/plugins/auth-backend/src/lib/oauth/index.ts @@ -26,5 +26,6 @@ export type { OAuthState, OAuthStartRequest, OAuthRefreshRequest, + OAuthLogoutRequest, OAuthResult, } from './types'; diff --git a/plugins/auth-backend/src/lib/oauth/types.ts b/plugins/auth-backend/src/lib/oauth/types.ts index e8b5282aad..3902c3f026 100644 --- a/plugins/auth-backend/src/lib/oauth/types.ts +++ b/plugins/auth-backend/src/lib/oauth/types.ts @@ -104,6 +104,11 @@ export type OAuthRefreshRequest = express.Request<{}> & { refreshToken: string; }; +/** @public */ +export type OAuthLogoutRequest = express.Request<{}> & { + refreshToken: string; +}; + /** * Any OAuth provider needs to implement this interface which has provider specific * handlers for different methods to perform authentication, get access tokens, @@ -136,5 +141,5 @@ export interface OAuthHandlers { /** * (Optional) Sign out of the auth provider. */ - logout?(): Promise; + logout?(req: OAuthLogoutRequest): Promise; } diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index 7689cd1a3a..afe5c27f6b 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -16,6 +16,7 @@ import express from 'express'; import passport from 'passport'; +import { OAuth2Client } from 'google-auth-library'; import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; import { encodeState, @@ -27,6 +28,7 @@ import { OAuthResponse, OAuthResult, OAuthStartRequest, + OAuthLogoutRequest, } from '../../lib/oauth'; import { executeFetchUserProfileStrategy, @@ -119,6 +121,11 @@ export class GoogleAuthProvider implements OAuthHandlers { }; } + async logout(req: OAuthLogoutRequest) { + const oauthClient = new OAuth2Client(); + await oauthClient.revokeToken(req.refreshToken); + } + async refresh(req: OAuthRefreshRequest) { const { accessToken, refreshToken, params } = await executeRefreshTokenStrategy(