Adds Github to signin page providers (#1590)
This commit is contained in:
@@ -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);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user