auth-backend: add support for GitLab auth refresh
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -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,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user