From f524bbdea2f6fbfa47cd01a594459081132572bf Mon Sep 17 00:00:00 2001 From: danztran Date: Sun, 28 Jun 2020 22:24:47 +0700 Subject: [PATCH 1/6] plugin/auth-backend: add gitlab oauth --- packages/app/src/apis.ts | 11 ++ .../core-api/src/apis/definitions/auth.ts | 23 ++- .../auth/gitlab/GitlabAuth.test.ts | 46 +++++ .../implementations/auth/gitlab/GitlabAuth.ts | 135 ++++++++++++++ .../apis/implementations/auth/gitlab/index.ts | 18 ++ .../apis/implementations/auth/gitlab/types.ts | 27 +++ .../src/apis/implementations/auth/index.ts | 1 + .../core/src/layout/Sidebar/UserSettings.tsx | 6 + packages/dev-utils/src/devApp/apiFactories.ts | 13 ++ packages/storybook/.storybook/apis.js | 10 ++ plugins/auth-backend/package.json | 1 + .../src/lib/PassportStrategyHelper.ts | 4 +- .../auth-backend/src/providers/factories.ts | 2 + .../src/providers/gitlab/index.ts | 17 ++ .../src/providers/gitlab/provider.ts | 166 ++++++++++++++++++ plugins/auth-backend/src/providers/types.ts | 4 + plugins/auth-backend/src/service/router.ts | 13 +- yarn.lock | 9 +- 18 files changed, 497 insertions(+), 9 deletions(-) create mode 100644 packages/core-api/src/apis/implementations/auth/gitlab/GitlabAuth.test.ts create mode 100644 packages/core-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts create mode 100644 packages/core-api/src/apis/implementations/auth/gitlab/index.ts create mode 100644 packages/core-api/src/apis/implementations/auth/gitlab/types.ts create mode 100644 plugins/auth-backend/src/providers/gitlab/index.ts create mode 100644 plugins/auth-backend/src/providers/gitlab/provider.ts diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index d2b2706464..40a8c9f275 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -27,11 +27,13 @@ import { GoogleAuth, GithubAuth, OktaAuth, + GitlabAuth, oauthRequestApiRef, OAuthRequestManager, googleAuthApiRef, githubAuthApiRef, oktaAuthApiRef, + gitlabAuthApiRef, storageApiRef, WebStorage, } from '@backstage/core'; @@ -102,6 +104,15 @@ export const apis = (config: ConfigApi) => { }), ); + builder.add( + gitlabAuthApiRef, + GitlabAuth.create({ + apiOrigin: 'http://localhost:7000', + basePath: '/auth/', + oauthRequestApi, + }), + ); + builder.add( techRadarApiRef, new TechRadar({ diff --git a/packages/core-api/src/apis/definitions/auth.ts b/packages/core-api/src/apis/definitions/auth.ts index a944e85a7e..c9a17f0153 100644 --- a/packages/core-api/src/apis/definitions/auth.ts +++ b/packages/core-api/src/apis/definitions/auth.ts @@ -242,11 +242,24 @@ export const githubAuthApiRef = createApiRef< */ export const oktaAuthApiRef = createApiRef< OAuthApi & - OpenIdConnectApi & - ProfileInfoApi & - BackstageIdentityApi & - SessionStateApi + OpenIdConnectApi & + ProfileInfoApi & + BackstageIdentityApi & + SessionStateApi >({ id: 'core.auth.okta', description: 'Provides authentication towards Okta APIs', -}); +}); + +/** + * Provides authentication towards Gitlab APIs. + * + * See https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#limiting-scopes-of-a-personal-access-token + * for a full list of supported scopes. + */ +export const gitlabAuthApiRef = createApiRef< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionStateApi +>({ + id: 'core.auth.gitlab', + description: 'Provides authentication towards Gitlab APIs', +}); diff --git a/packages/core-api/src/apis/implementations/auth/gitlab/GitlabAuth.test.ts b/packages/core-api/src/apis/implementations/auth/gitlab/GitlabAuth.test.ts new file mode 100644 index 0000000000..18346b3208 --- /dev/null +++ b/packages/core-api/src/apis/implementations/auth/gitlab/GitlabAuth.test.ts @@ -0,0 +1,46 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import GitlabAuth from './GitlabAuth'; + +describe('GitlabAuth', () => { + it('should get access token', async () => { + const getSession = jest + .fn() + .mockResolvedValue({ providerInfo: { accessToken: 'access-token' } }); + const gitlabAuth = new GitlabAuth({ getSession } as any); + + expect(await gitlabAuth.getAccessToken()).toBe('access-token'); + expect(getSession).toBeCalledTimes(1); + }); + + it('should normalize scope', () => { + const tests = [ + { + arguments: ['read_user api write_repository'], + expect: new Set(['read_user', 'api', 'write_repository']), + }, + { + arguments: ['read_repository sudo'], + expect: new Set(['read_repository', 'sudo']), + }, + ]; + + for (const test of tests) { + expect(GitlabAuth.normalizeScope(...test.arguments)).toEqual(test.expect); + } + }); +}); diff --git a/packages/core-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts b/packages/core-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts new file mode 100644 index 0000000000..20e13f8a03 --- /dev/null +++ b/packages/core-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts @@ -0,0 +1,135 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import GitlabIcon from '@material-ui/icons/AcUnit'; +import { DefaultAuthConnector } from '../../../../lib/AuthConnector'; +import { GitlabSession } from './types'; +import { + OAuthApi, + SessionStateApi, + SessionState, + ProfileInfo, + BackstageIdentity, + AuthRequestOptions, +} from '../../../definitions/auth'; +import { OAuthRequestApi, AuthProvider } from '../../../definitions'; +import { SessionManager } from '../../../../lib/AuthSessionManager/types'; +import { StaticAuthSessionManager } from '../../../../lib/AuthSessionManager'; +import { Observable } from '../../../../types'; + +type CreateOptions = { + apiOrigin: string; + basePath: string; + + oauthRequestApi: OAuthRequestApi; + + environment?: string; + provider?: AuthProvider & { id: string }; +}; + +export type GitlabAuthResponse = { + providerInfo: { + accessToken: string; + scope: string; + expiresInSeconds: number; + }; + profile: ProfileInfo; + backstageIdentity: BackstageIdentity; +}; + +const DEFAULT_PROVIDER = { + id: 'gitlab', + title: 'Gitlab', + icon: GitlabIcon, +}; + +class GitlabAuth implements OAuthApi, SessionStateApi { + static create({ + apiOrigin, + basePath, + environment = 'development', + provider = DEFAULT_PROVIDER, + oauthRequestApi, + }: CreateOptions) { + const connector = new DefaultAuthConnector({ + apiOrigin, + basePath, + environment, + provider, + oauthRequestApi, + sessionTransform(res: GitlabAuthResponse): GitlabSession { + return { + ...res, + providerInfo: { + accessToken: res.providerInfo.accessToken, + scopes: GitlabAuth.normalizeScope(res.providerInfo.scope), + expiresAt: new Date( + Date.now() + res.providerInfo.expiresInSeconds * 1000, + ), + }, + }; + }, + }); + + const sessionManager = new StaticAuthSessionManager({ + connector, + defaultScopes: new Set(['read_user']), + sessionScopes: (session: GitlabSession) => session.providerInfo.scopes, + }); + + return new GitlabAuth(sessionManager); + } + + sessionState$(): Observable { + return this.sessionManager.sessionState$(); + } + + constructor(private readonly sessionManager: SessionManager) {} + + async getAccessToken(scope?: string, options?: AuthRequestOptions) { + const session = await this.sessionManager.getSession({ + ...options, + scopes: GitlabAuth.normalizeScope(scope), + }); + return session?.providerInfo.accessToken ?? ''; + } + + async getBackstageIdentity( + options: AuthRequestOptions = {}, + ): Promise { + const session = await this.sessionManager.getSession(options); + return session?.backstageIdentity; + } + + async getProfile(options: AuthRequestOptions = {}) { + const session = await this.sessionManager.getSession(options); + return session?.profile; + } + + async logout() { + await this.sessionManager.removeSession(); + } + + static normalizeScope(scope?: string): Set { + if (!scope) { + return new Set(); + } + + const scopeList = Array.isArray(scope) ? scope : scope.split(' '); + return new Set(scopeList); + } +} +export default GitlabAuth; diff --git a/packages/core-api/src/apis/implementations/auth/gitlab/index.ts b/packages/core-api/src/apis/implementations/auth/gitlab/index.ts new file mode 100644 index 0000000000..ee17b7de06 --- /dev/null +++ b/packages/core-api/src/apis/implementations/auth/gitlab/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './types'; +export { default as GitlabAuth } from './GitlabAuth'; diff --git a/packages/core-api/src/apis/implementations/auth/gitlab/types.ts b/packages/core-api/src/apis/implementations/auth/gitlab/types.ts new file mode 100644 index 0000000000..b03dfec084 --- /dev/null +++ b/packages/core-api/src/apis/implementations/auth/gitlab/types.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ProfileInfo, BackstageIdentity } from '../../../definitions'; + +export type GitlabSession = { + providerInfo: { + accessToken: string; + scopes: Set; + expiresAt: Date; + }; + profile: ProfileInfo; + backstageIdentity: BackstageIdentity; +}; diff --git a/packages/core-api/src/apis/implementations/auth/index.ts b/packages/core-api/src/apis/implementations/auth/index.ts index 4fa992dfb5..9d89ba5b04 100644 --- a/packages/core-api/src/apis/implementations/auth/index.ts +++ b/packages/core-api/src/apis/implementations/auth/index.ts @@ -16,4 +16,5 @@ export * from './google'; export * from './github'; +export * from './gitlab'; export * from './okta'; diff --git a/packages/core/src/layout/Sidebar/UserSettings.tsx b/packages/core/src/layout/Sidebar/UserSettings.tsx index 668cb1354a..937169c8bf 100644 --- a/packages/core/src/layout/Sidebar/UserSettings.tsx +++ b/packages/core/src/layout/Sidebar/UserSettings.tsx @@ -22,6 +22,7 @@ import { SidebarContext } from './config'; import { googleAuthApiRef, githubAuthApiRef, + gitlabAuthApiRef, identityApiRef, oktaAuthApiRef, useApi, @@ -57,6 +58,11 @@ export function SidebarUserSettings() { apiRef={githubAuthApiRef} icon={Star} /> + + GitlabAuth.create({ + apiOrigin: 'http://localhost:7000', + basePath: '/auth/', + oauthRequestApi, + }), +}); diff --git a/packages/storybook/.storybook/apis.js b/packages/storybook/.storybook/apis.js index 0d0cf74e8f..4851b872a8 100644 --- a/packages/storybook/.storybook/apis.js +++ b/packages/storybook/.storybook/apis.js @@ -6,6 +6,7 @@ import { OAuthRequestManager, googleAuthApiRef, githubAuthApiRef, + gitlabAuthApiRef, oktaAuthApiRef, AlertApiForwarder, ErrorApiForwarder, @@ -52,6 +53,15 @@ builder.add( }), ); +builder.add( + gitlabAuthApiRef, + GitlabAuth.create({ + apiOrigin: 'http://localhost:7000', + basePath: '/auth/', + oauthRequestApi, + }), +); + builder.add( oktaAuthApiRef, OktaAuth.create({ diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index a336441dc2..d756704a62 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -39,6 +39,7 @@ "morgan": "^1.10.0", "passport": "^0.4.1", "passport-github2": "^0.1.12", + "passport-gitlab2": "^5.0.0", "passport-google-oauth20": "^2.0.0", "passport-oauth2": "^1.5.0", "passport-okta-oauth": "^0.0.1", diff --git a/plugins/auth-backend/src/lib/PassportStrategyHelper.ts b/plugins/auth-backend/src/lib/PassportStrategyHelper.ts index 1167946560..1936e2ff05 100644 --- a/plugins/auth-backend/src/lib/PassportStrategyHelper.ts +++ b/plugins/auth-backend/src/lib/PassportStrategyHelper.ts @@ -137,7 +137,9 @@ export const executeRefreshTokenStrategy = async ( params: any, ) => { if (err) { - reject(new Error(`Failed to refresh access token ${err}`)); + reject( + new Error(`Failed to refresh access token: ${JSON.stringify(err)}`), + ); } if (!accessToken) { reject( diff --git a/plugins/auth-backend/src/providers/factories.ts b/plugins/auth-backend/src/providers/factories.ts index 9feefdac9f..7fa375bb6b 100644 --- a/plugins/auth-backend/src/providers/factories.ts +++ b/plugins/auth-backend/src/providers/factories.ts @@ -17,6 +17,7 @@ import Router from 'express-promise-router'; import { createGithubProvider } from './github'; import { createGoogleProvider } from './google'; +import { createGitlabProvider } from './gitlab'; import { createSamlProvider } from './saml'; import { createOktaProvider } from './okta'; import { AuthProviderFactory, AuthProviderConfig } from './types'; @@ -26,6 +27,7 @@ import { TokenIssuer } from '../identity'; const factories: { [providerId: string]: AuthProviderFactory } = { google: createGoogleProvider, github: createGithubProvider, + gitlab: createGitlabProvider, saml: createSamlProvider, okta: createOktaProvider, }; diff --git a/plugins/auth-backend/src/providers/gitlab/index.ts b/plugins/auth-backend/src/providers/gitlab/index.ts new file mode 100644 index 0000000000..7fba2dd95c --- /dev/null +++ b/plugins/auth-backend/src/providers/gitlab/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { createGitlabProvider } from './provider'; diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts new file mode 100644 index 0000000000..ffc24751d3 --- /dev/null +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -0,0 +1,166 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import express from 'express'; +import { Strategy as GitlabStrategy } from 'passport-gitlab2'; +import { + executeFrameHandlerStrategy, + executeRedirectStrategy, + makeProfileInfo, +} from '../../lib/PassportStrategyHelper'; +import { + OAuthProviderHandlers, + OAuthPrivateInfo, + AuthProviderConfig, + RedirectInfo, + EnvironmentProviderConfig, + OAuthProviderOptions, + OAuthProviderConfig, + OAuthResponse, + PassportDoneCallback, +} from '../types'; +import { OAuthProvider } from '../../lib/OAuthProvider'; +import { + EnvironmentHandlers, + EnvironmentHandler, +} from '../../lib/EnvironmentHandler'; +import { Logger } from 'winston'; +import { TokenIssuer } from '../../identity'; +import passport from 'passport'; + +export class GitlabAuthProvider implements OAuthProviderHandlers { + private readonly _strategy: GitlabStrategy; + private static _defaultExpiresInSeconds = 2 * 3600; + + static transformPassportProfile(rawProfile: any): passport.Profile { + const profile: passport.Profile = { + id: rawProfile.id, + username: rawProfile.username, + provider: rawProfile.provider, + displayName: rawProfile.displayName, + }; + if (rawProfile.emails && rawProfile.emails.length > 0) { + profile.emails = rawProfile.emails; + } + if (rawProfile.avatarUrl) { + profile.photos = [{ value: rawProfile.avatarUrl }]; + } + return profile; + } + + static transformOAuthResponse( + accessToken: string, + rawProfile: any, + params: any = {}, + ): OAuthResponse { + const passportProfile = GitlabAuthProvider.transformPassportProfile( + rawProfile, + ); + const profile = makeProfileInfo(passportProfile, params.id_token); + return { + providerInfo: { + accessToken, + scope: params.scope, + expiresInSeconds: + params.expires_in || GitlabAuthProvider._defaultExpiresInSeconds, + }, + profile, + }; + } + + constructor(options: OAuthProviderOptions) { + this._strategy = new GitlabStrategy( + { ...options }, + ( + accessToken: any, + _: any, + params: any, + rawProfile: any, + done: PassportDoneCallback, + ) => { + const oauthResponse = GitlabAuthProvider.transformOAuthResponse( + accessToken, + rawProfile, + params, + ); + done(undefined, oauthResponse); + }, + ); + } + + async start( + req: express.Request, + options: Record, + ): Promise { + return await executeRedirectStrategy(req, this._strategy, options); + } + + async handler( + req: express.Request, + ): Promise<{ response: OAuthResponse; refreshToken: string }> { + const { response } = await executeFrameHandlerStrategy< + OAuthResponse, + OAuthPrivateInfo + >(req, this._strategy); + + return { response }; + } +} + +export function createGitlabProvider( + { baseUrl }: AuthProviderConfig, + providerConfig: EnvironmentProviderConfig, + logger: Logger, + tokenIssuer: TokenIssuer, +) { + const envProviders: EnvironmentHandlers = {}; + + for (const [env, envConfig] of Object.entries(providerConfig)) { + const config = (envConfig as unknown) as OAuthProviderConfig; + const { secure, appOrigin, baseUrl: oauthBaseUrl } = config; + const callbackURLParam = `?env=${env}`; + + const opts = { + clientID: config.clientId, + clientSecret: config.clientSecret, + callbackURL: `${baseUrl}/gitlab/handler/frame${callbackURLParam}`, + baseURL: oauthBaseUrl, + }; + + if (!opts.clientID || !opts.clientSecret) { + if (process.env.NODE_ENV !== 'development') { + throw new Error( + 'Failed to initialize Gitlab auth provider, set AUTH_GITLAB_CLIENT_ID and AUTH_GITLAB_CLIENT_SECRET env vars', + ); + } + + logger.warn( + 'Gitlab auth provider disabled, set AUTH_GITLAB_CLIENT_ID and AUTH_GITLAB_CLIENT_SECRET env vars to enable', + ); + continue; + } + + envProviders[env] = new OAuthProvider(new GitlabAuthProvider(opts), { + disableRefresh: true, + providerId: 'gitlab', + secure, + baseUrl, + appOrigin, + tokenIssuer, + }); + } + return new EnvironmentHandler(envProviders); +} diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index aec4f9dfd0..8dff5bb60d 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -47,6 +47,10 @@ export type OAuthProviderConfig = { * to the window that initiates an auth request. */ appOrigin: string; + /** + * Base URL of the auth provider. + */ + baseUrl: string; /** * Client ID of the auth provider. */ diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index b9c0ff4496..1ba821247f 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -76,6 +76,15 @@ export async function createRouter( clientSecret: process.env.AUTH_GITHUB_CLIENT_SECRET!, }, }, + gitlab: { + development: { + appOrigin: 'http://localhost:3000', + secure: false, + baseUrl: process.env.GITLAB_BASE_URL || 'https://gitlab.com', + clientId: process.env.AUTH_GITLAB_CLIENT_ID!, + clientSecret: process.env.AUTH_GITLAB_CLIENT_SECRET!, + }, + }, saml: { development: { entryPoint: 'http://localhost:7001/', @@ -89,8 +98,8 @@ export async function createRouter( clientId: process.env.AUTH_OKTA_CLIENT_ID!, clientSecret: process.env.AUTH_OKTA_CLIENT_SECRET!, audience: process.env.AUTH_OKTA_AUDIENCE, - } - } + }, + }, }, }, }; diff --git a/yarn.lock b/yarn.lock index f4fa34b993..d442d3ce80 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14019,6 +14019,13 @@ passport-github2@^0.1.12: dependencies: passport-oauth2 "1.x.x" +passport-gitlab2@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/passport-gitlab2/-/passport-gitlab2-5.0.0.tgz#ea37e5285321c026a02671e87469cac28cce9b69" + integrity sha512-cXQMgM6JQx9wHVh7JLH30D8fplfwjsDwRz+zS0pqC8JS+4bNmc1J04NGp5g2M4yfwylH9kQRrMN98GxMw7q7cg== + dependencies: + passport-oauth2 "^1.4.0" + passport-google-oauth20@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz#0d241b2d21ebd3dc7f2b60669ec4d587e3a674ef" @@ -14035,7 +14042,7 @@ passport-oauth1@1.x.x: passport-strategy "1.x.x" utils-merge "1.x.x" -passport-oauth2@1.x.x, passport-oauth2@^1.5.0: +passport-oauth2@1.x.x, passport-oauth2@^1.4.0, passport-oauth2@^1.5.0: version "1.5.0" resolved "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.5.0.tgz#64babbb54ac46a4dcab35e7f266ed5294e3c4108" integrity sha512-kqBt6vR/5VlCK8iCx1/KpY42kQ+NEHZwsSyt4Y6STiNjU+wWICG1i8ucc1FapXDGO15C5O5VZz7+7vRzrDPXXQ== From 9fca100dc7b92065d78f7e258e154fcd75d93d83 Mon Sep 17 00:00:00 2001 From: danztran Date: Sun, 28 Jun 2020 23:21:37 +0700 Subject: [PATCH 2/6] plugin/auth-backend: update docs --- plugins/auth-backend/README.md | 47 +++++++++++++++---- .../src/providers/gitlab/provider.ts | 15 ++++-- plugins/auth-backend/src/providers/types.ts | 4 -- plugins/auth-backend/src/service/router.ts | 2 +- 4 files changed, 49 insertions(+), 19 deletions(-) diff --git a/plugins/auth-backend/README.md b/plugins/auth-backend/README.md index 147853f06f..ff0b1c7a3d 100644 --- a/plugins/auth-backend/README.md +++ b/plugins/auth-backend/README.md @@ -7,17 +7,46 @@ This is the backend part of the auth plugin. It responds to auth requests from the frontend, and fulfills them by delegating to the appropriate provider in the backend. -## Requirements - -Needs AUTH_GOOGLE_CLIENT_ID and AUTH_GOOGLE_CLIENT_SECRET set in the environment for the backend to startup - ## Local development -export AUTH_GOOGLE_CLIENT_ID= -read -r AUTH_GOOGLE_CLIENT_SECRET - -export AUTH_GOOGLE_CLIENT_SECRET -run `yarn start` in packages/backend folder +Choose your OAuth Providers, replace `x` with actual value and then start backend: +Example for Google Oauth Provider at root directory: + +```bash +export AUTH_GOOGLE_CLIENT_ID=x +export AUTH_GOOGLE_CLIENT_SECRET=x +yarn --cwd packages/backend start +``` + +### Google + +```bash +export AUTH_GOOGLE_CLIENT_ID=x +export AUTH_GOOGLE_CLIENT_SECRET=x +``` + +### Github + +```bash +export AUTH_GITHUB_CLIENT_ID=x +export AUTH_GITHUB_CLIENT_SECRET=x +``` + +### Gitlab + +```bash +export GITLAB_BASE_URL=x # default is https://gitlab.com +export AUTH_GITLAB_CLIENT_ID=x +export AUTH_GITLAB_CLIENT_SECRET=x +``` + +### Okta + +```bash +export AUTH_OKTA_AUDIENCE=x +export AUTH_OKTA_CLIENT_ID=x +export AUTH_OKTA_CLIENT_SECRET=x +``` ### SAML diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index ffc24751d3..6e823a83f9 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -129,15 +129,20 @@ export function createGitlabProvider( const envProviders: EnvironmentHandlers = {}; for (const [env, envConfig] of Object.entries(providerConfig)) { - const config = (envConfig as unknown) as OAuthProviderConfig; - const { secure, appOrigin, baseUrl: oauthBaseUrl } = config; + const { + secure, + appOrigin, + clientId, + clientSecret, + audience, + } = (envConfig as unknown) as OAuthProviderConfig; const callbackURLParam = `?env=${env}`; const opts = { - clientID: config.clientId, - clientSecret: config.clientSecret, + clientID: clientId, + clientSecret: clientSecret, callbackURL: `${baseUrl}/gitlab/handler/frame${callbackURLParam}`, - baseURL: oauthBaseUrl, + baseURL: audience, }; if (!opts.clientID || !opts.clientSecret) { diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 8dff5bb60d..aec4f9dfd0 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -47,10 +47,6 @@ export type OAuthProviderConfig = { * to the window that initiates an auth request. */ appOrigin: string; - /** - * Base URL of the auth provider. - */ - baseUrl: string; /** * Client ID of the auth provider. */ diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 1ba821247f..fb533a4bdd 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -80,9 +80,9 @@ export async function createRouter( development: { appOrigin: 'http://localhost:3000', secure: false, - baseUrl: process.env.GITLAB_BASE_URL || 'https://gitlab.com', clientId: process.env.AUTH_GITLAB_CLIENT_ID!, clientSecret: process.env.AUTH_GITLAB_CLIENT_SECRET!, + audience: process.env.GITLAB_BASE_URL! || 'https://gitlab.com', }, }, saml: { From eeaf25fce8ef176625c43e02c6404abb3e036f71 Mon Sep 17 00:00:00 2001 From: danztran Date: Mon, 29 Jun 2020 10:55:54 +0700 Subject: [PATCH 3/6] plugin/auth-backend: add gitlab types --- .../src/providers/gitlab/provider.ts | 17 +++++-------- .../src/providers/gitlab/types.d.ts | 25 +++++++++++++++++++ 2 files changed, 31 insertions(+), 11 deletions(-) create mode 100644 plugins/auth-backend/src/providers/gitlab/types.d.ts diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index 6e823a83f9..c2b1802708 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -23,7 +23,6 @@ import { } from '../../lib/PassportStrategyHelper'; import { OAuthProviderHandlers, - OAuthPrivateInfo, AuthProviderConfig, RedirectInfo, EnvironmentProviderConfig, @@ -89,7 +88,7 @@ export class GitlabAuthProvider implements OAuthProviderHandlers { _: any, params: any, rawProfile: any, - done: PassportDoneCallback, + done: PassportDoneCallback, ) => { const oauthResponse = GitlabAuthProvider.transformOAuthResponse( accessToken, @@ -108,15 +107,11 @@ export class GitlabAuthProvider implements OAuthProviderHandlers { return await executeRedirectStrategy(req, this._strategy, options); } - async handler( - req: express.Request, - ): Promise<{ response: OAuthResponse; refreshToken: string }> { - const { response } = await executeFrameHandlerStrategy< - OAuthResponse, - OAuthPrivateInfo - >(req, this._strategy); - - return { response }; + async handler(req: express.Request): Promise<{ response: OAuthResponse }> { + return await executeFrameHandlerStrategy( + req, + this._strategy, + ); } } diff --git a/plugins/auth-backend/src/providers/gitlab/types.d.ts b/plugins/auth-backend/src/providers/gitlab/types.d.ts new file mode 100644 index 0000000000..8cd7373c52 --- /dev/null +++ b/plugins/auth-backend/src/providers/gitlab/types.d.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +declare module 'passport-gitlab2' { + import { Request } from 'express'; + import { StrategyCreated } from 'passport'; + + export class Strategy { + constructor(options: any, verify: any); + authenticate(this: StrategyCreated, req: Request, options?: any): any; + } +} From c1806a0e2d479615def038ffccb546a140a71f3f Mon Sep 17 00:00:00 2001 From: danztran Date: Mon, 29 Jun 2020 11:43:17 +0700 Subject: [PATCH 4/6] plugin/auth-backend: add test for gitlab provider --- .../src/providers/gitlab/provider.test.ts | 100 ++++++++++++++++++ .../src/providers/gitlab/provider.ts | 24 +++-- 2 files changed, 117 insertions(+), 7 deletions(-) create mode 100644 plugins/auth-backend/src/providers/gitlab/provider.test.ts diff --git a/plugins/auth-backend/src/providers/gitlab/provider.test.ts b/plugins/auth-backend/src/providers/gitlab/provider.test.ts new file mode 100644 index 0000000000..5eec6c47f9 --- /dev/null +++ b/plugins/auth-backend/src/providers/gitlab/provider.test.ts @@ -0,0 +1,100 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { GitlabAuthProvider } from './provider'; + +describe('GitlabAuthProvider', () => { + it('should transform to type OAuthResponse', () => { + const tests = [ + { + arguments: { + accessToken: '19xasczxcm9n7gacn9jdgm19me', + rawProfile: { + 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, + }, + }, + expect: { + providerInfo: { + accessToken: '19xasczxcm9n7gacn9jdgm19me', + expiresInSeconds: 100, + scope: 'user_read write_repository', + }, + profile: { + email: 'jimmymarkum@gmail.com', + displayName: 'Jimmy Markum', + picture: + 'https://a1cf74336522e87f135f-2f21ace9a6cf0052456644b80fa06d4f.ssl.cf2.rackcdn.com/images/characters_opt/p-mystic-river-sean-penn.jpg', + }, + }, + }, + { + arguments: { + accessToken: + 'ajakljsdoiahoawxbrouawucmbawe.awkxjemaneasdxwe.sodijxqeqwexeqwxe', + rawProfile: { + id: 'ipd12039', + username: 'daveboyle', + provider: 'gitlab', + displayName: 'Dave Boyle', + emails: [ + { + value: 'daveboyle@gitlab.org', + }, + ], + }, + params: { + scope: 'read_repository', + }, + }, + expect: { + providerInfo: { + accessToken: + 'ajakljsdoiahoawxbrouawucmbawe.awkxjemaneasdxwe.sodijxqeqwexeqwxe', + scope: 'read_repository', + }, + profile: { + displayName: 'Dave Boyle', + email: 'daveboyle@gitlab.org', + }, + }, + }, + ]; + + for (const test of tests) { + expect( + GitlabAuthProvider.transformOAuthResponse( + test.arguments.accessToken, + test.arguments.rawProfile, + test.arguments.params, + ), + ).toEqual(test.expect); + } + }); +}); diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index c2b1802708..890fbb3531 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -42,7 +42,6 @@ import passport from 'passport'; export class GitlabAuthProvider implements OAuthProviderHandlers { private readonly _strategy: GitlabStrategy; - private static _defaultExpiresInSeconds = 2 * 3600; static transformPassportProfile(rawProfile: any): passport.Profile { const profile: passport.Profile = { @@ -51,12 +50,14 @@ export class GitlabAuthProvider implements OAuthProviderHandlers { provider: rawProfile.provider, displayName: rawProfile.displayName, }; + if (rawProfile.emails && rawProfile.emails.length > 0) { profile.emails = rawProfile.emails; } if (rawProfile.avatarUrl) { profile.photos = [{ value: rawProfile.avatarUrl }]; } + return profile; } @@ -68,14 +69,23 @@ export class GitlabAuthProvider implements OAuthProviderHandlers { const passportProfile = GitlabAuthProvider.transformPassportProfile( rawProfile, ); + const profile = makeProfileInfo(passportProfile, params.id_token); + const providerInfo = { + accessToken, + scope: params.scope, + expiresInSeconds: params.expires_in, + idToken: params.id_token, + }; + + if (params.expires_in) { + providerInfo.expiresInSeconds = params.expires_in; + } + if (params.id_token) { + providerInfo.idToken = params.id_token; + } return { - providerInfo: { - accessToken, - scope: params.scope, - expiresInSeconds: - params.expires_in || GitlabAuthProvider._defaultExpiresInSeconds, - }, + providerInfo, profile, }; } From fba2c8c3b28d9cc9c9db8227715c85be64a4c527 Mon Sep 17 00:00:00 2001 From: danztran Date: Mon, 29 Jun 2020 12:03:51 +0700 Subject: [PATCH 5/6] plugin/auth-backend: fix test on passport strategy helper --- plugins/auth-backend/src/lib/PassportStrategyHelper.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/plugins/auth-backend/src/lib/PassportStrategyHelper.ts b/plugins/auth-backend/src/lib/PassportStrategyHelper.ts index 1936e2ff05..4cc0de4444 100644 --- a/plugins/auth-backend/src/lib/PassportStrategyHelper.ts +++ b/plugins/auth-backend/src/lib/PassportStrategyHelper.ts @@ -137,9 +137,7 @@ export const executeRefreshTokenStrategy = async ( params: any, ) => { if (err) { - reject( - new Error(`Failed to refresh access token: ${JSON.stringify(err)}`), - ); + reject(new Error(`Failed to refresh access token ${err.toString()}`)); } if (!accessToken) { reject( From 11307d46b989729c8da28bec8a23b01a2959ceb3 Mon Sep 17 00:00:00 2001 From: danztran Date: Mon, 29 Jun 2020 12:13:30 +0700 Subject: [PATCH 6/6] storybook: fix gitlab auth --- packages/storybook/.storybook/apis.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/storybook/.storybook/apis.js b/packages/storybook/.storybook/apis.js index 4851b872a8..5b00a3c3e0 100644 --- a/packages/storybook/.storybook/apis.js +++ b/packages/storybook/.storybook/apis.js @@ -13,6 +13,7 @@ import { ErrorAlerter, GoogleAuth, GithubAuth, + GitlabAuth, OktaAuth, identityApiRef, } from '@backstage/core';