Adds Github to signin page providers (#1590)

This commit is contained in:
jaime-talkdesk
2020-07-15 10:20:20 +01:00
committed by GitHub
parent b75116edb0
commit 3217e85c85
6 changed files with 358 additions and 12 deletions
+1 -1
View File
@@ -33,7 +33,7 @@ const app = createApp({
SignInPage: props => (
<SignInPage
{...props}
providers={['guest', 'google', 'custom', 'okta', 'gitlab']}
providers={['guest', 'google', 'custom', 'okta', 'gitlab', 'github']}
/>
),
},
@@ -90,7 +90,7 @@ class GithubAuth implements OAuthApi, SessionStateApi {
const sessionManager = new StaticAuthSessionManager({
connector,
defaultScopes: new Set(['user']),
defaultScopes: new Set(['read:user']),
sessionScopes: (session: GithubSession) => session.providerInfo.scopes,
});
@@ -0,0 +1,89 @@
/*
* 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 React from 'react';
import { Grid, Typography, Button } from '@material-ui/core';
import { InfoCard } from '../InfoCard/InfoCard';
import { ProviderComponent, ProviderLoader, SignInProvider } from './types';
import { useApi, githubAuthApiRef, errorApiRef } from '@backstage/core-api';
const Component: ProviderComponent = ({ onResult }) => {
const githubAuthApi = useApi(githubAuthApiRef);
const errorApi = useApi(errorApiRef);
const handleLogin = async () => {
try {
const identity = await githubAuthApi.getBackstageIdentity({
instantPopup: true,
});
const profile = await githubAuthApi.getProfile();
onResult({
userId: identity!.id,
profile: profile!,
getIdToken: () => {
return githubAuthApi.getBackstageIdentity().then(i => i!.idToken);
},
logout: async () => {
await githubAuthApi.logout();
},
});
} catch (error) {
errorApi.post(error);
}
};
return (
<Grid item>
<InfoCard
title="Github"
actions={
<Button color="primary" variant="outlined" onClick={handleLogin}>
Sign In
</Button>
}
>
<Typography variant="body1">Sign In using Github</Typography>
</InfoCard>
</Grid>
);
};
const loader: ProviderLoader = async apis => {
const githubAuthApi = apis.get(githubAuthApiRef)!;
const identity = await githubAuthApi.getBackstageIdentity({
optional: true,
});
if (!identity) {
return undefined;
}
const profile = await githubAuthApi.getProfile();
return {
userId: identity.id,
profile: profile!,
getIdToken: () =>
githubAuthApi.getBackstageIdentity().then(i => i!.idToken),
logout: async () => {
await githubAuthApi.logout();
},
};
};
export const githubProvider: SignInProvider = { Component, loader };
@@ -20,6 +20,7 @@ import { googleProvider } from './googleProvider';
import { customProvider } from './customProvider';
import { gitlabProvider } from './gitlabProvider';
import { oktaProvider } from './oktaProvider';
import { githubProvider } from './githubProvider';
import {
SignInPageProps,
SignInResult,
@@ -37,7 +38,8 @@ export type SignInProviderId =
| 'google'
| 'gitlab'
| 'custom'
| 'okta';
| 'okta'
| 'github';
const signInProviders: { [id in SignInProviderId]: SignInProvider } = {
guest: guestProvider,
@@ -45,6 +47,7 @@ const signInProviders: { [id in SignInProviderId]: SignInProvider } = {
gitlab: gitlabProvider,
custom: customProvider,
okta: oktaProvider,
github: githubProvider,
};
export const useSignInProviders = (
@@ -0,0 +1,207 @@
/*
* 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 { GithubAuthProvider } from './provider';
describe('GithubAuthProvider', () => {
describe('should transform to type OAuthResponse', () => {
it('when all fields are present, it should be able to map them', () => {
const accessToken = '19xasczxcm9n7gacn9jdgm19me';
const rawProfile = {
id: 'uid-123',
username: 'jimmymarkum',
provider: 'github',
displayName: 'Jimmy Markum',
emails: [
{
value: 'jimmymarkum@gmail.com',
},
],
photos: [
{
value:
'https://a1cf74336522e87f135f-2f21ace9a6cf0052456644b80fa06d4f.ssl.cf2.rackcdn.com/images/characters_opt/p-mystic-river-sean-penn.jpg',
},
],
};
const params = {
scope: 'read:scope',
};
const expected = {
backstageIdentity: {
id: 'jimmymarkum',
},
providerInfo: {
accessToken: '19xasczxcm9n7gacn9jdgm19me',
expiresInSeconds: undefined,
idToken: undefined,
scope: 'read:scope',
},
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',
},
};
expect(
GithubAuthProvider.transformOAuthResponse(
accessToken,
rawProfile,
params,
),
).toEqual(expected);
});
it('when "email" is missing, it should be able to create the profile without it', () => {
const accessToken = '19xasczxcm9n7gacn9jdgm19me';
const rawProfile = {
id: 'uid-123',
username: 'jimmymarkum',
provider: 'github',
displayName: 'Jimmy Markum',
emails: null,
photos: [
{
value:
'https://a1cf74336522e87f135f-2f21ace9a6cf0052456644b80fa06d4f.ssl.cf2.rackcdn.com/images/characters_opt/p-mystic-river-sean-penn.jpg',
},
],
};
const params = {
scope: 'read:scope',
};
const expected = {
backstageIdentity: {
id: 'jimmymarkum',
},
providerInfo: {
accessToken: '19xasczxcm9n7gacn9jdgm19me',
expiresInSeconds: undefined,
idToken: undefined,
scope: 'read:scope',
},
profile: {
displayName: 'Jimmy Markum',
picture:
'https://a1cf74336522e87f135f-2f21ace9a6cf0052456644b80fa06d4f.ssl.cf2.rackcdn.com/images/characters_opt/p-mystic-river-sean-penn.jpg',
},
};
expect(
GithubAuthProvider.transformOAuthResponse(
accessToken,
rawProfile,
params,
),
).toEqual(expected);
});
it('when "displayName" is missing, it should be able to create the profile and map "displayName" with "username"', () => {
const accessToken = '19xasczxcm9n7gacn9jdgm19me';
const rawProfile = {
id: 'uid-123',
username: 'jimmymarkum',
provider: 'github',
displayName: null,
emails: null,
photos: [
{
value:
'https://a1cf74336522e87f135f-2f21ace9a6cf0052456644b80fa06d4f.ssl.cf2.rackcdn.com/images/characters_opt/p-mystic-river-sean-penn.jpg',
},
],
};
const params = {
scope: 'read:scope',
};
const expected = {
backstageIdentity: {
id: 'jimmymarkum',
},
providerInfo: {
accessToken: '19xasczxcm9n7gacn9jdgm19me',
expiresInSeconds: undefined,
idToken: undefined,
scope: 'read:scope',
},
profile: {
displayName: 'jimmymarkum',
picture:
'https://a1cf74336522e87f135f-2f21ace9a6cf0052456644b80fa06d4f.ssl.cf2.rackcdn.com/images/characters_opt/p-mystic-river-sean-penn.jpg',
},
};
expect(
GithubAuthProvider.transformOAuthResponse(
accessToken,
rawProfile,
params,
),
).toEqual(expected);
});
it('when "photos" is missing, it should be able to create the profile without it', () => {
const accessToken =
'ajakljsdoiahoawxbrouawucmbawe.awkxjemaneasdxwe.sodijxqeqwexeqwxe';
const rawProfile = {
id: 'ipd12039',
username: 'daveboyle',
provider: 'gitlab',
displayName: 'Dave Boyle',
emails: [
{
value: 'daveboyle@gitlab.org',
},
],
};
const params = {
scope: 'read:user',
};
const expected = {
backstageIdentity: {
id: 'daveboyle',
},
providerInfo: {
accessToken:
'ajakljsdoiahoawxbrouawucmbawe.awkxjemaneasdxwe.sodijxqeqwexeqwxe',
scope: 'read:user',
expiresInSeconds: undefined,
idToken: undefined,
},
profile: {
displayName: 'Dave Boyle',
email: 'daveboyle@gitlab.org',
},
};
expect(
GithubAuthProvider.transformOAuthResponse(
accessToken,
rawProfile,
params,
),
).toEqual(expected);
});
});
});
@@ -38,10 +38,60 @@ import {
} from '../../lib/EnvironmentHandler';
import { Logger } from 'winston';
import { TokenIssuer } from '../../identity';
import passport from 'passport';
export class GithubAuthProvider implements OAuthProviderHandlers {
private readonly _strategy: GithubStrategy;
static transformPassportProfile(rawProfile: any): passport.Profile {
const profile: passport.Profile = {
id: rawProfile.username,
username: rawProfile.username,
provider: rawProfile.provider,
displayName: rawProfile.displayName || rawProfile.username,
photos: rawProfile.photos,
emails: rawProfile.emails,
};
return profile;
}
static transformOAuthResponse(
accessToken: string,
rawProfile: any,
params: any = {},
): OAuthResponse {
const passportProfile = GithubAuthProvider.transformPassportProfile(
rawProfile,
);
const profile = makeProfileInfo(passportProfile, params.id_token);
const providerInfo = {
accessToken,
scope: params.scope,
expiresInSeconds: params.expires_in,
idToken: params.id_token,
};
// Github provides an id numeric value (123)
// as a fallback
const id = passportProfile!.id;
if (params.expires_in) {
providerInfo.expiresInSeconds = params.expires_in;
}
if (params.id_token) {
providerInfo.idToken = params.id_token;
}
return {
providerInfo,
profile,
backstageIdentity: {
id,
},
};
}
constructor(options: OAuthProviderOptions) {
this._strategy = new GithubStrategy(
{ ...options },
@@ -52,15 +102,12 @@ export class GithubAuthProvider implements OAuthProviderHandlers {
rawProfile: any,
done: PassportDoneCallback<OAuthResponse>,
) => {
const profile = makeProfileInfo(rawProfile);
done(undefined, {
providerInfo: {
accessToken,
scope: params.scope,
expiresInSeconds: params.expires_in,
},
profile,
});
const oauthResponse = GithubAuthProvider.transformOAuthResponse(
accessToken,
rawProfile,
params,
);
done(undefined, oauthResponse);
},
);
}