diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx
index 3c8054d995..c569308fc7 100644
--- a/packages/app/src/App.tsx
+++ b/packages/app/src/App.tsx
@@ -33,7 +33,7 @@ const app = createApp({
SignInPage: props => (
),
},
diff --git a/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts b/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts
index 9309cfd437..aaea681cef 100644
--- a/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts
+++ b/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts
@@ -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,
});
diff --git a/packages/core/src/layout/SignInPage/githubProvider.tsx b/packages/core/src/layout/SignInPage/githubProvider.tsx
new file mode 100644
index 0000000000..fd7815e7b8
--- /dev/null
+++ b/packages/core/src/layout/SignInPage/githubProvider.tsx
@@ -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 (
+
+
+ Sign In
+
+ }
+ >
+ Sign In using Github
+
+
+ );
+};
+
+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 };
diff --git a/packages/core/src/layout/SignInPage/providers.tsx b/packages/core/src/layout/SignInPage/providers.tsx
index 53d6b75eac..e4f4aee8c7 100644
--- a/packages/core/src/layout/SignInPage/providers.tsx
+++ b/packages/core/src/layout/SignInPage/providers.tsx
@@ -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 = (
diff --git a/plugins/auth-backend/src/providers/github/provider.test.ts b/plugins/auth-backend/src/providers/github/provider.test.ts
new file mode 100644
index 0000000000..61d4bb887f
--- /dev/null
+++ b/plugins/auth-backend/src/providers/github/provider.test.ts
@@ -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);
+ });
+ });
+});
diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts
index ec444733ba..ed06c5786e 100644
--- a/plugins/auth-backend/src/providers/github/provider.ts
+++ b/plugins/auth-backend/src/providers/github/provider.ts
@@ -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,
) => {
- 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);
},
);
}