diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 9e9ca1edd0..d4d0cc0490 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -19,25 +19,20 @@ import { AlertDisplay, OAuthRequestDialog, SignInPage, - useApi, - configApiRef, } from '@backstage/core'; import React, { FC } from 'react'; import Root from './components/Root'; import * as plugins from './plugins'; import { apis } from './apis'; import { hot } from 'react-hot-loader/root'; +import { providers } from './authProviders'; const app = createApp({ apis, plugins: Object.values(plugins), components: { SignInPage: props => { - const configApi = useApi(configApiRef); - const providersConfig = configApi.getOptionalConfig('auth.providers'); - const providers = providersConfig?.keys() ?? []; - - return ; + return ; }, }, }); diff --git a/packages/app/src/authProviders.ts b/packages/app/src/authProviders.ts new file mode 100644 index 0000000000..d95848e014 --- /dev/null +++ b/packages/app/src/authProviders.ts @@ -0,0 +1,64 @@ +/* + * 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 { + googleAuthApiRef, + gitlabAuthApiRef, + oktaAuthApiRef, + githubAuthApiRef, + commonProvider, + customProvider, + guestProvider, +} from '@backstage/core'; + +export const providers = [ + { + id: 'google-auth-provider', + title: 'Google', + message: 'Sign In using Google', + apiRef: googleAuthApiRef, + provider: commonProvider, + }, + { + id: 'gitlab-auth-provider', + title: 'Gitlab', + message: 'Sign In using Gitlab', + apiRef: gitlabAuthApiRef, + provider: commonProvider, + }, + { + id: 'github-auth-provider', + title: 'Github', + message: 'Sign In using Github', + apiRef: githubAuthApiRef, + provider: commonProvider, + }, + { + id: 'okta-auth-provider', + title: 'Okta', + message: 'Sign In using Okta', + apiRef: oktaAuthApiRef, + provider: commonProvider, + }, + { + id: 'guest-provider', + provider: guestProvider, + }, + { + id: 'custom-provider', + provider: customProvider, + }, +]; diff --git a/packages/core/src/layout/SignInPage/SignInPage.tsx b/packages/core/src/layout/SignInPage/SignInPage.tsx index 8a1191f442..e07e6490ae 100644 --- a/packages/core/src/layout/SignInPage/SignInPage.tsx +++ b/packages/core/src/layout/SignInPage/SignInPage.tsx @@ -21,11 +21,12 @@ import { Content } from '../Content/Content'; import { ContentHeader } from '../ContentHeader/ContentHeader'; import { Grid } from '@material-ui/core'; import { SignInPageProps, useApi, configApiRef } from '@backstage/core-api'; -import { useSignInProviders, SignInProviderId } from './providers'; +import { useSignInProviders } from './providers'; +import { SignInProviderConfig } from './types'; import { Progress } from '../../components/Progress'; export type Props = SignInPageProps & { - providers: SignInProviderId[]; + providers: SignInProviderConfig[]; }; export const SignInPage: FC = ({ onResult, providers }) => { diff --git a/packages/core/src/layout/SignInPage/commonProvider.tsx b/packages/core/src/layout/SignInPage/commonProvider.tsx new file mode 100644 index 0000000000..da0247fd6f --- /dev/null +++ b/packages/core/src/layout/SignInPage/commonProvider.tsx @@ -0,0 +1,94 @@ +/* + * 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, + CommonSignInConfig, +} from './types'; +import { useApi, errorApiRef } from '@backstage/core-api'; + +const Component: ProviderComponent = ({ provider, onResult }) => { + const { apiRef, title, message } = provider as CommonSignInConfig; + const authApi = useApi(apiRef); + const errorApi = useApi(errorApiRef); + + const handleLogin = async () => { + try { + const identity = await authApi.getBackstageIdentity({ + instantPopup: true, + }); + + const profile = await authApi.getProfile(); + onResult({ + userId: identity!.id, + profile: profile!, + getIdToken: () => { + return authApi.getBackstageIdentity().then(i => i!.idToken); + }, + logout: async () => { + await authApi.logout(); + }, + }); + } catch (error) { + errorApi.post(error); + } + }; + + return ( + + + Sign In + + } + > + {message} + + + ); +}; + +const loader: ProviderLoader = async (apis, apiRef) => { + const authApi = apis.get(apiRef)!; + + const identity = await authApi.getBackstageIdentity({ + optional: true, + }); + + if (!identity) { + return undefined; + } + + const profile = await authApi.getProfile(); + + return { + userId: identity.id, + profile: profile!, + getIdToken: () => authApi.getBackstageIdentity().then(i => i!.idToken), + logout: async () => { + await authApi.logout(); + }, + }; +}; + +export const commonProvider: SignInProvider = { Component, loader }; diff --git a/packages/core/src/layout/SignInPage/index.ts b/packages/core/src/layout/SignInPage/index.ts index 49f55aefc5..e58d6a85df 100644 --- a/packages/core/src/layout/SignInPage/index.ts +++ b/packages/core/src/layout/SignInPage/index.ts @@ -15,3 +15,6 @@ */ export { SignInPage } from './SignInPage'; +export { commonProvider } from './commonProvider'; +export { guestProvider } from './guestProvider'; +export { customProvider } from './customProvider'; diff --git a/packages/core/src/layout/SignInPage/providers.tsx b/packages/core/src/layout/SignInPage/providers.tsx index 1eab51e3ee..9ab742b516 100644 --- a/packages/core/src/layout/SignInPage/providers.tsx +++ b/packages/core/src/layout/SignInPage/providers.tsx @@ -15,12 +15,6 @@ */ import React, { useLayoutEffect, useState, useMemo, useCallback } from 'react'; -import { guestProvider } from './guestProvider'; -import { googleProvider } from './googleProvider'; -import { customProvider } from './customProvider'; -import { gitlabProvider } from './gitlabProvider'; -import { oktaProvider } from './oktaProvider'; -import { githubProvider } from './githubProvider'; import { SignInPageProps, SignInResult, @@ -28,24 +22,12 @@ import { useApiHolder, errorApiRef, } from '@backstage/core-api'; -import { SignInProvider } from './types'; +import { SignInProviderConfig, CommonSignInConfig } from './types'; const PROVIDER_STORAGE_KEY = '@backstage/core:SignInPage:provider'; -// Separate list here to avoid exporting internal types -export type SignInProviderId = 'guest' | string; - -const signInProviders: { [id in SignInProviderId]: SignInProvider } = { - guest: guestProvider, - google: googleProvider, - gitlab: gitlabProvider, - oauth2: customProvider, - okta: oktaProvider, - github: githubProvider, -}; - export const useSignInProviders = ( - providers: SignInProviderId[], + providers: SignInProviderConfig[], onResult: SignInPageProps['onResult'], ) => { const errorApi = useApi(errorApiRef); @@ -74,25 +56,29 @@ export const useSignInProviders = ( } // We can't use storageApi here, as it might have a dependency on the IdentityApi - const selectedProvider = localStorage.getItem( + const selectedProviderId = localStorage.getItem( PROVIDER_STORAGE_KEY, - ) as SignInProviderId; + ) as string; // No provider selected, let the user pick one - if (selectedProvider === null) { + if (selectedProviderId === null) { setLoading(false); return undefined; } - const provider = signInProviders[selectedProvider]; - if (!provider) { + const config = providers.find( + provider => provider.id === selectedProviderId, + ); + if (!config) { setLoading(false); return undefined; } let didCancel = false; - provider - .loader(apiHolder) + const { apiRef } = config as CommonSignInConfig; + + config.provider + .loader(apiHolder, apiRef) .then(result => { if (didCancel) { return; @@ -119,20 +105,22 @@ export const useSignInProviders = ( // This renders all available sign-in providers const elements = useMemo( () => - providers.map(providerId => { - const provider = signInProviders[providerId]; - if (!provider) { - throw new Error(`Unknown sign-in provider: ${providerId}`); - } - const { Component } = provider; + providers.map(config => { + const { Component } = config.provider; const handleResult = (result: SignInResult) => { - localStorage.setItem(PROVIDER_STORAGE_KEY, providerId); + localStorage.setItem(PROVIDER_STORAGE_KEY, config.id); handleWrappedResult(result); }; - return ; + return ( + + ); }), [providers, handleWrappedResult], ); diff --git a/packages/core/src/layout/SignInPage/types.ts b/packages/core/src/layout/SignInPage/types.ts index e13cda5ddd..089ebac823 100644 --- a/packages/core/src/layout/SignInPage/types.ts +++ b/packages/core/src/layout/SignInPage/types.ts @@ -15,12 +15,46 @@ */ import { ComponentType } from 'react'; -import { SignInPageProps, SignInResult, ApiHolder } from '@backstage/core-api'; +import { + SignInPageProps, + SignInResult, + ApiHolder, + ApiRef, + OAuthApi, + ProfileInfoApi, + BackstageIdentityApi, + SessionStateApi, + OpenIdConnectApi, +} from '@backstage/core-api'; -export type ProviderComponent = ComponentType; +export type SignInConfig = { + id: string; + provider: SignInProvider; +}; + +export type CommonSignInConfig = SignInConfig & { + title: string; + message: string; + apiRef: ApiRef< + OAuthApi & + ProfileInfoApi & + BackstageIdentityApi & + SessionStateApi & + OpenIdConnectApi + >; +}; + +export type SignInProviderConfig = SignInConfig | CommonSignInConfig; + +export type ProviderComponent = ComponentType< + SignInPageProps & { provider: SignInProviderConfig } +>; export type ProviderLoader = ( apis: ApiHolder, + apiRef: ApiRef< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionStateApi + >, ) => Promise; export type SignInProvider = {