|
|
|
@@ -16,6 +16,8 @@
|
|
|
|
|
|
|
|
|
|
import express from 'express';
|
|
|
|
|
import { Strategy as GitlabStrategy } from 'passport-gitlab2';
|
|
|
|
|
import { Logger } from 'winston';
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
executeRedirectStrategy,
|
|
|
|
|
executeFrameHandlerStrategy,
|
|
|
|
@@ -24,7 +26,12 @@ import {
|
|
|
|
|
makeProfileInfo,
|
|
|
|
|
PassportDoneCallback,
|
|
|
|
|
} from '../../lib/passport';
|
|
|
|
|
import { RedirectInfo, AuthProviderFactory } from '../types';
|
|
|
|
|
import {
|
|
|
|
|
RedirectInfo,
|
|
|
|
|
AuthProviderFactory,
|
|
|
|
|
SignInResolver,
|
|
|
|
|
AuthHandler,
|
|
|
|
|
} from '../types';
|
|
|
|
|
import {
|
|
|
|
|
OAuthAdapter,
|
|
|
|
|
OAuthProviderOptions,
|
|
|
|
@@ -36,6 +43,8 @@ import {
|
|
|
|
|
encodeState,
|
|
|
|
|
OAuthResult,
|
|
|
|
|
} from '../../lib/oauth';
|
|
|
|
|
import { TokenIssuer } from '../../identity';
|
|
|
|
|
import { CatalogIdentityClient } from '../../lib/catalog';
|
|
|
|
|
|
|
|
|
|
type FullProfile = OAuthResult['fullProfile'] & {
|
|
|
|
|
avatarUrl?: string;
|
|
|
|
@@ -47,29 +56,72 @@ type PrivateInfo = {
|
|
|
|
|
|
|
|
|
|
export type GitlabAuthProviderOptions = OAuthProviderOptions & {
|
|
|
|
|
baseUrl: string;
|
|
|
|
|
signInResolver?: SignInResolver<OAuthResult>;
|
|
|
|
|
authHandler: AuthHandler<OAuthResult>;
|
|
|
|
|
tokenIssuer: TokenIssuer;
|
|
|
|
|
catalogIdentityClient: CatalogIdentityClient;
|
|
|
|
|
logger: Logger;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function transformProfile(fullProfile: FullProfile) {
|
|
|
|
|
function transformResult(result: OAuthResult): OAuthResult {
|
|
|
|
|
const { fullProfile, ...authResult } = result;
|
|
|
|
|
|
|
|
|
|
const profile = makeProfileInfo({
|
|
|
|
|
...fullProfile,
|
|
|
|
|
photos: [
|
|
|
|
|
...(fullProfile.photos ?? []),
|
|
|
|
|
...(fullProfile.avatarUrl ? [{ value: fullProfile.avatarUrl }] : []),
|
|
|
|
|
...((fullProfile as FullProfile).avatarUrl
|
|
|
|
|
? [{ value: (fullProfile as FullProfile).avatarUrl as string }]
|
|
|
|
|
: []),
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let id = fullProfile.id;
|
|
|
|
|
|
|
|
|
|
if (profile.email) {
|
|
|
|
|
id = profile.email.split('@')[0];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { id, profile };
|
|
|
|
|
return {
|
|
|
|
|
...authResult,
|
|
|
|
|
fullProfile,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const gitlabDefaultSignInResolver: SignInResolver<OAuthResult> = async (
|
|
|
|
|
info,
|
|
|
|
|
ctx,
|
|
|
|
|
) => {
|
|
|
|
|
const { profile } = info;
|
|
|
|
|
|
|
|
|
|
if (!profile.email) {
|
|
|
|
|
throw new Error('Profile contained no email');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const userId = profile.email.split('@')[0];
|
|
|
|
|
|
|
|
|
|
const token = await ctx.tokenIssuer.issueToken({
|
|
|
|
|
claims: { sub: userId, ent: [`user:default/${userId}`] },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return { id: userId, token };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export class GitlabAuthProvider implements OAuthHandlers {
|
|
|
|
|
private readonly _strategy: GitlabStrategy;
|
|
|
|
|
private readonly signInResolver?: SignInResolver<OAuthResult>;
|
|
|
|
|
private readonly authHandler: AuthHandler<OAuthResult>;
|
|
|
|
|
private readonly tokenIssuer: TokenIssuer;
|
|
|
|
|
private readonly catalogIdentityClient: CatalogIdentityClient;
|
|
|
|
|
private readonly logger: Logger;
|
|
|
|
|
|
|
|
|
|
constructor(options: GitlabAuthProviderOptions) {
|
|
|
|
|
this.signInResolver = options.signInResolver;
|
|
|
|
|
this.authHandler = options.authHandler;
|
|
|
|
|
this.tokenIssuer = options.tokenIssuer;
|
|
|
|
|
this.logger = options.logger;
|
|
|
|
|
this.catalogIdentityClient = options.catalogIdentityClient;
|
|
|
|
|
|
|
|
|
|
this._strategy = new GitlabStrategy(
|
|
|
|
|
{
|
|
|
|
|
clientID: options.clientId,
|
|
|
|
@@ -109,23 +161,9 @@ export class GitlabAuthProvider implements OAuthHandlers {
|
|
|
|
|
OAuthResult,
|
|
|
|
|
PrivateInfo
|
|
|
|
|
>(req, this._strategy);
|
|
|
|
|
const { accessToken, params } = result;
|
|
|
|
|
|
|
|
|
|
const { id, profile } = transformProfile(result.fullProfile);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
response: {
|
|
|
|
|
profile,
|
|
|
|
|
providerInfo: {
|
|
|
|
|
accessToken,
|
|
|
|
|
scope: params.scope,
|
|
|
|
|
expiresInSeconds: params.expires_in,
|
|
|
|
|
idToken: params.id_token,
|
|
|
|
|
},
|
|
|
|
|
backstageIdentity: {
|
|
|
|
|
id,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
response: await this.handleResult(result),
|
|
|
|
|
refreshToken: privateInfo.refreshToken,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
@@ -145,30 +183,78 @@ export class GitlabAuthProvider implements OAuthHandlers {
|
|
|
|
|
this._strategy,
|
|
|
|
|
accessToken,
|
|
|
|
|
);
|
|
|
|
|
const { id, profile } = transformProfile(fullProfile);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
profile,
|
|
|
|
|
return this.handleResult({
|
|
|
|
|
fullProfile,
|
|
|
|
|
params,
|
|
|
|
|
accessToken,
|
|
|
|
|
refreshToken: newRefreshToken,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async handleResult(result: OAuthResult): Promise<OAuthResponse> {
|
|
|
|
|
const { profile } = await this.authHandler(transformResult(result));
|
|
|
|
|
|
|
|
|
|
const response: OAuthResponse = {
|
|
|
|
|
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,
|
|
|
|
|
idToken: result.params.id_token,
|
|
|
|
|
accessToken: result.accessToken,
|
|
|
|
|
scope: result.params.scope,
|
|
|
|
|
expiresInSeconds: result.params.expires_in,
|
|
|
|
|
},
|
|
|
|
|
profile,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (this.signInResolver) {
|
|
|
|
|
response.backstageIdentity = await this.signInResolver(
|
|
|
|
|
{
|
|
|
|
|
result,
|
|
|
|
|
profile,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
tokenIssuer: this.tokenIssuer,
|
|
|
|
|
catalogIdentityClient: this.catalogIdentityClient,
|
|
|
|
|
logger: this.logger,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return response;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type GitlabProviderOptions = {};
|
|
|
|
|
export type GitlabProviderOptions = {
|
|
|
|
|
/**
|
|
|
|
|
* The profile transformation function used to verify and convert the auth response
|
|
|
|
|
* into the profile that will be presented to the user.
|
|
|
|
|
*/
|
|
|
|
|
authHandler?: AuthHandler<OAuthResult>;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Configure sign-in for this provider, without it the provider can not be used to sign users in.
|
|
|
|
|
*/
|
|
|
|
|
/**
|
|
|
|
|
* Maps an auth result to a Backstage identity for the user.
|
|
|
|
|
*
|
|
|
|
|
* Set to `'email'` to use the default email-based sign in resolver, which will search
|
|
|
|
|
* the catalog for a single user entity that has a matching `microsoft.com/email` annotation.
|
|
|
|
|
*/
|
|
|
|
|
signIn?: {
|
|
|
|
|
resolver?: SignInResolver<OAuthResult>;
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const createGitlabProvider = (
|
|
|
|
|
_options?: GitlabProviderOptions,
|
|
|
|
|
options?: GitlabProviderOptions,
|
|
|
|
|
): AuthProviderFactory => {
|
|
|
|
|
return ({ providerId, globalConfig, config, tokenIssuer }) =>
|
|
|
|
|
return ({
|
|
|
|
|
providerId,
|
|
|
|
|
globalConfig,
|
|
|
|
|
config,
|
|
|
|
|
tokenIssuer,
|
|
|
|
|
catalogApi,
|
|
|
|
|
logger,
|
|
|
|
|
}) =>
|
|
|
|
|
OAuthEnvironmentHandler.mapConfig(config, envConfig => {
|
|
|
|
|
const clientId = envConfig.getString('clientId');
|
|
|
|
|
const clientSecret = envConfig.getString('clientSecret');
|
|
|
|
@@ -176,11 +262,37 @@ export const createGitlabProvider = (
|
|
|
|
|
const baseUrl = audience || 'https://gitlab.com';
|
|
|
|
|
const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
|
|
|
|
|
|
|
|
|
const catalogIdentityClient = new CatalogIdentityClient({
|
|
|
|
|
catalogApi,
|
|
|
|
|
tokenIssuer,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const authHandler: AuthHandler<OAuthResult> = options?.authHandler
|
|
|
|
|
? options.authHandler
|
|
|
|
|
: async ({ fullProfile, params }) => ({
|
|
|
|
|
profile: makeProfileInfo(fullProfile, params.id_token),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const signInResolverFn =
|
|
|
|
|
options?.signIn?.resolver ?? gitlabDefaultSignInResolver;
|
|
|
|
|
|
|
|
|
|
const signInResolver: SignInResolver<OAuthResult> = info =>
|
|
|
|
|
signInResolverFn(info, {
|
|
|
|
|
catalogIdentityClient,
|
|
|
|
|
tokenIssuer,
|
|
|
|
|
logger,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const provider = new GitlabAuthProvider({
|
|
|
|
|
clientId,
|
|
|
|
|
clientSecret,
|
|
|
|
|
callbackUrl,
|
|
|
|
|
baseUrl,
|
|
|
|
|
authHandler,
|
|
|
|
|
signInResolver,
|
|
|
|
|
catalogIdentityClient,
|
|
|
|
|
logger,
|
|
|
|
|
tokenIssuer,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
|
|
|
|