Merge pull request #1478 from danztran/gitlab_auth

Add Gitlab Auth #343
This commit is contained in:
Fredrik Adelöw
2020-06-30 13:39:24 +02:00
committed by GitHub
20 changed files with 665 additions and 18 deletions
+38 -9
View File
@@ -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=<INSERT_CLIENT_ID_HERE>
read -r AUTH_GOOGLE_CLIENT_SECRET
<COPY_PASTE_CLIENT_SECRET_HERE>
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
+1
View File
@@ -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",
@@ -137,7 +137,7 @@ 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 ${err.toString()}`));
}
if (!accessToken) {
reject(
@@ -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,
};
@@ -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';
@@ -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);
}
});
});
@@ -0,0 +1,176 @@
/*
* 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,
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;
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);
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,
profile,
};
}
constructor(options: OAuthProviderOptions) {
this._strategy = new GitlabStrategy(
{ ...options },
(
accessToken: any,
_: any,
params: any,
rawProfile: any,
done: PassportDoneCallback<OAuthResponse>,
) => {
const oauthResponse = GitlabAuthProvider.transformOAuthResponse(
accessToken,
rawProfile,
params,
);
done(undefined, oauthResponse);
},
);
}
async start(
req: express.Request,
options: Record<string, string>,
): Promise<RedirectInfo> {
return await executeRedirectStrategy(req, this._strategy, options);
}
async handler(req: express.Request): Promise<{ response: OAuthResponse }> {
return await executeFrameHandlerStrategy<OAuthResponse>(
req,
this._strategy,
);
}
}
export function createGitlabProvider(
{ baseUrl }: AuthProviderConfig,
providerConfig: EnvironmentProviderConfig,
logger: Logger,
tokenIssuer: TokenIssuer,
) {
const envProviders: EnvironmentHandlers = {};
for (const [env, envConfig] of Object.entries(providerConfig)) {
const {
secure,
appOrigin,
clientId,
clientSecret,
audience,
} = (envConfig as unknown) as OAuthProviderConfig;
const callbackURLParam = `?env=${env}`;
const opts = {
clientID: clientId,
clientSecret: clientSecret,
callbackURL: `${baseUrl}/gitlab/handler/frame${callbackURLParam}`,
baseURL: audience,
};
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);
}
+25
View File
@@ -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<this>, req: Request, options?: any): any;
}
}
+11 -2
View File
@@ -76,6 +76,15 @@ export async function createRouter(
clientSecret: process.env.AUTH_GITHUB_CLIENT_SECRET!,
},
},
gitlab: {
development: {
appOrigin: 'http://localhost:3000',
secure: false,
clientId: process.env.AUTH_GITLAB_CLIENT_ID!,
clientSecret: process.env.AUTH_GITLAB_CLIENT_SECRET!,
audience: process.env.GITLAB_BASE_URL! || 'https://gitlab.com',
},
},
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,
}
}
},
},
},
},
};