auth-backend: add support for GitLab auth refresh

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2021-06-16 12:13:27 +02:00
parent e855326632
commit 1aa31f0afc
3 changed files with 129 additions and 64 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-auth-backend': patch
---
Add support for refreshing GitLab auth sessions.
@@ -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);
}
@@ -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<OAuthResult>,
done: PassportDoneCallback<OAuthResult, PrivateInfo>,
) => {
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<OAuthResult>(
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<OAuthResponse> {
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,
});