diff --git a/.changeset/violet-ads-change.md b/.changeset/violet-ads-change.md new file mode 100644 index 0000000000..1de5813982 --- /dev/null +++ b/.changeset/violet-ads-change.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Add support for refreshing GitLab auth sessions. diff --git a/plugins/auth-backend/src/providers/gitlab/provider.test.ts b/plugins/auth-backend/src/providers/gitlab/provider.test.ts index 82588250cf..db941f0213 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.test.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.test.ts @@ -27,24 +27,29 @@ describe('GitlabAuthProvider', () => { it('should transform to type OAuthResponse', async () => { const tests = [ { - result: { - accessToken: '19xasczxcm9n7gacn9jdgm19me', - fullProfile: { - id: 'uid-123', - username: 'jimmymarkum', - provider: 'gitlab', - displayName: 'Jimmy Markum', - emails: [ - { - value: 'jimmymarkum@gmail.com', - }, - ], - avatarUrl: - 'https://a1cf74336522e87f135f-2f21ace9a6cf0052456644b80fa06d4f.ssl.cf2.rackcdn.com/images/characters_opt/p-mystic-river-sean-penn.jpg', + input: { + result: { + accessToken: '19xasczxcm9n7gacn9jdgm19me', + fullProfile: { + id: 'uid-123', + username: 'jimmymarkum', + provider: 'gitlab', + displayName: 'Jimmy Markum', + emails: [ + { + value: 'jimmymarkum@gmail.com', + }, + ], + avatarUrl: + 'https://a1cf74336522e87f135f-2f21ace9a6cf0052456644b80fa06d4f.ssl.cf2.rackcdn.com/images/characters_opt/p-mystic-river-sean-penn.jpg', + }, + params: { + scope: 'user_read write_repository', + expires_in: 100, + }, }, - params: { - scope: 'user_read write_repository', - expires_in: 100, + privateInfo: { + refreshToken: 'gacn9jdgm19me19xasczxcm9n7', }, }, expect: { @@ -65,23 +70,28 @@ describe('GitlabAuthProvider', () => { }, }, { - result: { - accessToken: - 'ajakljsdoiahoawxbrouawucmbawe.awkxjemaneasdxwe.sodijxqeqwexeqwxe', - fullProfile: { - id: 'ipd12039', - username: 'daveboyle', - provider: 'gitlab', - displayName: 'Dave Boyle', - emails: [ - { - value: 'daveboyle@gitlab.org', - }, - ], + input: { + result: { + accessToken: + 'ajakljsdoiahoawxbrouawucmbawe.awkxjemaneasdxwe.sodijxqeqwexeqwxe', + fullProfile: { + id: 'ipd12039', + username: 'daveboyle', + provider: 'gitlab', + displayName: 'Dave Boyle', + emails: [ + { + value: 'daveboyle@gitlab.org', + }, + ], + }, + params: { + scope: 'read_repository', + expires_in: 200, + }, }, - params: { - scope: 'read_repository', - expires_in: 200, + privateInfo: { + refreshToken: 'gacn96f3y6y5jdgm19mec348nqrty719xasczf356yxcm9n7', }, }, expect: { @@ -109,7 +119,7 @@ describe('GitlabAuthProvider', () => { baseUrl: 'mock', }); for (const test of tests) { - mockFrameHandler.mockResolvedValueOnce({ result: test.result }); + mockFrameHandler.mockResolvedValueOnce(test.input); const { response } = await provider.handler({} as any); expect(response).toEqual(test.expect); } diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index b8de398af5..3749bf7134 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -17,8 +17,10 @@ import express from 'express'; import { Strategy as GitlabStrategy } from 'passport-gitlab2'; import { - executeFrameHandlerStrategy, executeRedirectStrategy, + executeFrameHandlerStrategy, + executeRefreshTokenStrategy, + executeFetchUserProfileStrategy, makeProfileInfo, PassportDoneCallback, } from '../../lib/passport'; @@ -30,14 +32,40 @@ import { OAuthResponse, OAuthEnvironmentHandler, OAuthStartRequest, + OAuthRefreshRequest, encodeState, OAuthResult, } from '../../lib/oauth'; +type FullProfile = OAuthResult['fullProfile'] & { + avatarUrl?: string; +}; + +type PrivateInfo = { + refreshToken: string; +}; + export type GitlabAuthProviderOptions = OAuthProviderOptions & { baseUrl: string; }; +function transformProfile(fullProfile: FullProfile) { + const profile = makeProfileInfo({ + ...fullProfile, + photos: [ + ...(fullProfile.photos ?? []), + ...(fullProfile.avatarUrl ? [{ value: fullProfile.avatarUrl }] : []), + ], + }); + + let id = fullProfile.id; + if (profile.email) { + id = profile.email.split('@')[0]; + } + + return { id, profile }; +} + export class GitlabAuthProvider implements OAuthHandlers { private readonly _strategy: GitlabStrategy; @@ -51,12 +79,18 @@ export class GitlabAuthProvider implements OAuthHandlers { }, ( accessToken: any, - _refreshToken: any, + refreshToken: any, params: any, fullProfile: any, - done: PassportDoneCallback, + done: PassportDoneCallback, ) => { - done(undefined, { fullProfile, params, accessToken }); + done( + undefined, + { fullProfile, params, accessToken }, + { + refreshToken, + }, + ); }, ); } @@ -68,33 +102,16 @@ export class GitlabAuthProvider implements OAuthHandlers { }); } - async handler(req: express.Request): Promise<{ response: OAuthResponse }> { - const { result } = await executeFrameHandlerStrategy( - req, - this._strategy, - ); + async handler( + req: express.Request, + ): Promise<{ response: OAuthResponse; refreshToken: string }> { + const { result, privateInfo } = await executeFrameHandlerStrategy< + OAuthResult, + PrivateInfo + >(req, this._strategy); const { accessToken, params } = result; - const fullProfile = result.fullProfile as OAuthResult['fullProfile'] & { - avatarUrl?: string; - }; - const profile = makeProfileInfo( - { - ...fullProfile, - photos: [ - ...(fullProfile.photos ?? []), - ...(fullProfile.avatarUrl ? [{ value: fullProfile.avatarUrl }] : []), - ], - }, - params.id_token, - ); - - // gitlab provides an id numeric value (123) - // as a fallback - let id = fullProfile.id; - if (profile.email) { - id = profile.email.split('@')[0]; - } + const { id, profile } = transformProfile(result.fullProfile); return { response: { @@ -109,6 +126,39 @@ export class GitlabAuthProvider implements OAuthHandlers { id, }, }, + refreshToken: privateInfo.refreshToken, + }; + } + + async refresh(req: OAuthRefreshRequest): Promise { + const { + accessToken, + refreshToken: newRefreshToken, + params, + } = await executeRefreshTokenStrategy( + this._strategy, + req.refreshToken, + req.scope, + ); + + const fullProfile = await executeFetchUserProfileStrategy( + this._strategy, + accessToken, + ); + const { id, profile } = transformProfile(fullProfile); + + return { + profile, + providerInfo: { + accessToken, + refreshToken: newRefreshToken, // GitLab expires the old refresh token when used + idToken: params.id_token, + expiresInSeconds: params.expires_in, + scope: params.scope, + }, + backstageIdentity: { + id, + }, }; } } @@ -134,7 +184,7 @@ export const createGitlabProvider = ( }); return OAuthAdapter.fromConfig(globalConfig, provider, { - disableRefresh: true, + disableRefresh: false, providerId, tokenIssuer, });