diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 9e9ca1edd0..9a9fdfe5c4 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -19,25 +19,22 @@ 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 './identityProviders'; 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/identityProviders.ts b/packages/app/src/identityProviders.ts new file mode 100644 index 0000000000..e80b1d6fea --- /dev/null +++ b/packages/app/src/identityProviders.ts @@ -0,0 +1,49 @@ +/* + * 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, +} from '@backstage/core'; + +export const providers = [ + { + id: 'google-auth-provider', + title: 'Google', + message: 'Sign In using Google', + apiRef: googleAuthApiRef, + }, + { + id: 'gitlab-auth-provider', + title: 'Gitlab', + message: 'Sign In using Gitlab', + apiRef: gitlabAuthApiRef, + }, + { + id: 'github-auth-provider', + title: 'Github', + message: 'Sign In using Github', + apiRef: githubAuthApiRef, + }, + { + id: 'okta-auth-provider', + title: 'Okta', + message: 'Sign In using Okta', + apiRef: oktaAuthApiRef, + }, +]; diff --git a/packages/core/src/layout/SignInPage/SignInPage.tsx b/packages/core/src/layout/SignInPage/SignInPage.tsx index 8a1191f442..52a27f190e 100644 --- a/packages/core/src/layout/SignInPage/SignInPage.tsx +++ b/packages/core/src/layout/SignInPage/SignInPage.tsx @@ -21,17 +21,22 @@ 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, getSignInProviders } from './providers'; +import { IdentityProviders } from './types'; import { Progress } from '../../components/Progress'; export type Props = SignInPageProps & { - providers: SignInProviderId[]; + providers: IdentityProviders; }; -export const SignInPage: FC = ({ onResult, providers }) => { +export const SignInPage: FC = ({ onResult, providers = [] }) => { const configApi = useApi(configApiRef); - const [loading, providerElements] = useSignInProviders(providers, onResult); + const signInProviders = getSignInProviders(providers); + const [loading, providerElements] = useSignInProviders( + signInProviders, + onResult, + ); if (loading) { return ; diff --git a/packages/core/src/layout/SignInPage/githubProvider.tsx b/packages/core/src/layout/SignInPage/commonProvider.tsx similarity index 60% rename from packages/core/src/layout/SignInPage/githubProvider.tsx rename to packages/core/src/layout/SignInPage/commonProvider.tsx index fd7815e7b8..f9611342a2 100644 --- a/packages/core/src/layout/SignInPage/githubProvider.tsx +++ b/packages/core/src/layout/SignInPage/commonProvider.tsx @@ -17,28 +17,34 @@ 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'; +import { + ProviderComponent, + ProviderLoader, + SignInProvider, + SignInConfig, +} from './types'; +import { useApi, errorApiRef } from '@backstage/core-api'; -const Component: ProviderComponent = ({ onResult }) => { - const githubAuthApi = useApi(githubAuthApiRef); +const Component: ProviderComponent = ({ config, onResult }) => { + const { apiRef, title, message } = config as SignInConfig; + const authApi = useApi(apiRef); const errorApi = useApi(errorApiRef); const handleLogin = async () => { try { - const identity = await githubAuthApi.getBackstageIdentity({ + const identity = await authApi.getBackstageIdentity({ instantPopup: true, }); - const profile = await githubAuthApi.getProfile(); + const profile = await authApi.getProfile(); onResult({ userId: identity!.id, profile: profile!, getIdToken: () => { - return githubAuthApi.getBackstageIdentity().then(i => i!.idToken); + return authApi.getBackstageIdentity().then(i => i!.idToken); }, logout: async () => { - await githubAuthApi.logout(); + await authApi.logout(); }, }); } catch (error) { @@ -49,23 +55,23 @@ const Component: ProviderComponent = ({ onResult }) => { return ( Sign In } > - Sign In using Github + {message} ); }; -const loader: ProviderLoader = async apis => { - const githubAuthApi = apis.get(githubAuthApiRef)!; +const loader: ProviderLoader = async (apis, apiRef) => { + const authApi = apis.get(apiRef)!; - const identity = await githubAuthApi.getBackstageIdentity({ + const identity = await authApi.getBackstageIdentity({ optional: true, }); @@ -73,17 +79,16 @@ const loader: ProviderLoader = async apis => { return undefined; } - const profile = await githubAuthApi.getProfile(); + const profile = await authApi.getProfile(); return { userId: identity.id, profile: profile!, - getIdToken: () => - githubAuthApi.getBackstageIdentity().then(i => i!.idToken), + getIdToken: () => authApi.getBackstageIdentity().then(i => i!.idToken), logout: async () => { - await githubAuthApi.logout(); + await authApi.logout(); }, }; }; -export const githubProvider: SignInProvider = { Component, loader }; +export const commonProvider: SignInProvider = { Component, loader }; diff --git a/packages/core/src/layout/SignInPage/gitlabProvider.tsx b/packages/core/src/layout/SignInPage/gitlabProvider.tsx deleted file mode 100644 index 5e417cb32d..0000000000 --- a/packages/core/src/layout/SignInPage/gitlabProvider.tsx +++ /dev/null @@ -1,88 +0,0 @@ -/* - * 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, gitlabAuthApiRef, errorApiRef } from '@backstage/core-api'; - -const Component: ProviderComponent = ({ onResult }) => { - const gitlabAuthApi = useApi(gitlabAuthApiRef); - const errorApi = useApi(errorApiRef); - - const handleLogin = async () => { - try { - const identity = await gitlabAuthApi.getBackstageIdentity({ - instantPopup: true, - }); - - const profile = await gitlabAuthApi.getProfile(); - onResult({ - userId: identity!.id, - profile: profile!, - getIdToken: () => - gitlabAuthApi.getBackstageIdentity().then(i => i!.idToken), - logout: async () => { - await gitlabAuthApi.logout(); - }, - }); - } catch (error) { - errorApi.post(error); - } - }; - - return ( - - - Sign In - - } - > - Sign In using Gitlab - - - ); -}; - -const loader: ProviderLoader = async apis => { - const gitlabAuthApi = apis.get(gitlabAuthApiRef)!; - - const identity = await gitlabAuthApi.getBackstageIdentity({ - optional: true, - }); - - if (!identity) { - return undefined; - } - - const profile = await gitlabAuthApi.getProfile(); - - return { - userId: identity.id, - profile: profile!, - getIdToken: () => - gitlabAuthApi.getBackstageIdentity().then(i => i!.idToken), - logout: async () => { - await gitlabAuthApi.logout(); - }, - }; -}; - -export const gitlabProvider: SignInProvider = { Component, loader }; diff --git a/packages/core/src/layout/SignInPage/googleProvider.tsx b/packages/core/src/layout/SignInPage/googleProvider.tsx deleted file mode 100644 index 632b81ad17..0000000000 --- a/packages/core/src/layout/SignInPage/googleProvider.tsx +++ /dev/null @@ -1,89 +0,0 @@ -/* - * 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, googleAuthApiRef, errorApiRef } from '@backstage/core-api'; - -const Component: ProviderComponent = ({ onResult }) => { - const googleAuthApi = useApi(googleAuthApiRef); - const errorApi = useApi(errorApiRef); - - const handleLogin = async () => { - try { - const identity = await googleAuthApi.getBackstageIdentity({ - instantPopup: true, - }); - - const profile = await googleAuthApi.getProfile(); - - onResult({ - userId: identity!.id, - profile: profile!, - getIdToken: () => - googleAuthApi.getBackstageIdentity().then(i => i!.idToken), - logout: async () => { - await googleAuthApi.logout(); - }, - }); - } catch (error) { - errorApi.post(error); - } - }; - - return ( - - - Sign In - - } - > - Sign In using Google - - - ); -}; - -const loader: ProviderLoader = async apis => { - const googleAuthApi = apis.get(googleAuthApiRef)!; - - const identity = await googleAuthApi.getBackstageIdentity({ - optional: true, - }); - - if (!identity) { - return undefined; - } - - const profile = await googleAuthApi.getProfile(); - - return { - userId: identity.id, - profile: profile!, - getIdToken: () => - googleAuthApi.getBackstageIdentity().then(i => i!.idToken), - logout: async () => { - await googleAuthApi.logout(); - }, - }; -}; - -export const googleProvider: SignInProvider = { Component, loader }; diff --git a/packages/core/src/layout/SignInPage/oktaProvider.tsx b/packages/core/src/layout/SignInPage/oktaProvider.tsx deleted file mode 100644 index cf15fe5b22..0000000000 --- a/packages/core/src/layout/SignInPage/oktaProvider.tsx +++ /dev/null @@ -1,93 +0,0 @@ -/* - * 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, - oktaAuthApiRef, - errorApiRef, -} from '@backstage/core-api'; - -const Component: ProviderComponent = ({ onResult }) => { - const oktaAuthApi = useApi(oktaAuthApiRef); - const errorApi = useApi(errorApiRef); - - const handleLogin = async () => { - try { - const identity = await oktaAuthApi.getBackstageIdentity({ - instantPopup: true, - }); - - const profile = await oktaAuthApi.getProfile(); - - onResult({ - userId: identity!.id, - profile: profile!, - getIdToken: () => - oktaAuthApi.getBackstageIdentity().then(i => i!.idToken), - logout: async () => { - await oktaAuthApi.logout(); - }, - }); - } catch (error) { - errorApi.post(error); - } - }; - - return ( - - - Sign In - - } - > - Sign In using Okta - - - ); -}; - -const loader: ProviderLoader = async apis => { - const oktaAuthApi = apis.get(oktaAuthApiRef)!; - - const identity = await oktaAuthApi.getBackstageIdentity({ - optional: true, - }); - - if (!identity) { - return undefined; - } - - const profile = await oktaAuthApi.getProfile(); - - return { - userId: identity.id, - profile: profile!, - getIdToken: () => - oktaAuthApi.getBackstageIdentity().then(i => i!.idToken), - logout: async () => { - await oktaAuthApi.logout(); - }, - }; -}; - -export const oktaProvider: SignInProvider = { Component, loader }; \ No newline at end of file diff --git a/packages/core/src/layout/SignInPage/providers.tsx b/packages/core/src/layout/SignInPage/providers.tsx index 1eab51e3ee..ff60eba228 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,61 @@ import { useApiHolder, errorApiRef, } from '@backstage/core-api'; -import { SignInProvider } from './types'; +import { SignInConfig, IdentityProviders, SignInProvider } from './types'; +import { commonProvider } from './commonProvider'; +import { guestProvider } from './guestProvider'; +import { customProvider } from './customProvider'; 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 type SignInProviderType = { + [key: string]: { + components: SignInProvider; + id: string; + config?: SignInConfig; + }; }; +const signInProviders: { [key: string]: SignInProvider } = { + guest: guestProvider, + custom: customProvider, + common: commonProvider, +}; + +function validateIDs(id: string, providers: SignInProviderType): void { + if (id in providers) + throw new Error( + `"${id}" ID is duplicated. IDs of identity providers have to be unique.`, + ); +} + +export function getSignInProviders( + identityProviders: IdentityProviders, +): SignInProviderType { + const providers = identityProviders.reduce( + (acc: SignInProviderType, config) => { + if (typeof config === 'string') { + validateIDs(config, acc); + acc[config] = { components: signInProviders[config], id: config }; + + return acc; + } + + const { id } = config as SignInConfig; + validateIDs(id, acc); + + acc[id] = { components: signInProviders.common, id, config }; + + return acc; + }, + {}, + ); + + return providers; +} + export const useSignInProviders = ( - providers: SignInProviderId[], + providers: SignInProviderType, onResult: SignInPageProps['onResult'], ) => { const errorApi = useApi(errorApiRef); @@ -74,25 +105,26 @@ 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]; + const provider = providers[selectedProviderId]; if (!provider) { setLoading(false); return undefined; } let didCancel = false; - provider - .loader(apiHolder) + + provider.components + .loader(apiHolder, provider.config?.apiRef!) .then(result => { if (didCancel) { return; @@ -119,20 +151,24 @@ 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; + Object.keys(providers).map(key => { + const provider = providers[key]; + + const { Component } = provider.components; const handleResult = (result: SignInResult) => { - localStorage.setItem(PROVIDER_STORAGE_KEY, providerId); + localStorage.setItem(PROVIDER_STORAGE_KEY, provider.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..9c501e8095 100644 --- a/packages/core/src/layout/SignInPage/types.ts +++ b/packages/core/src/layout/SignInPage/types.ts @@ -15,12 +15,37 @@ */ import { ComponentType } from 'react'; -import { SignInPageProps, SignInResult, ApiHolder } from '@backstage/core-api'; +import { + SignInPageProps, + SignInResult, + ApiHolder, + ApiRef, + OAuthApi, + ProfileInfoApi, + BackstageIdentityApi, + SessionStateApi, +} from '@backstage/core-api'; -export type ProviderComponent = ComponentType; +export type SignInConfig = { + id: string; + title: string; + message: string; + apiRef: ApiRef< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionStateApi + >; +}; + +export type IdentityProviders = ('guest' | 'custom' | SignInConfig)[]; + +export type ProviderComponent = ComponentType< + SignInPageProps & { config: SignInConfig } +>; export type ProviderLoader = ( apis: ApiHolder, + apiRef: ApiRef< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionStateApi + >, ) => Promise; export type SignInProvider = {