Allow passing configurable list of identity providers to SignInPage component

This commit is contained in:
Kinga Sieminiak
2020-08-04 09:03:34 +02:00
parent e031a06e1e
commit 25a869f579
7 changed files with 225 additions and 46 deletions
+2 -7
View File
@@ -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 <SignInPage {...props} providers={['guest', ...providers]} />;
return <SignInPage {...props} providers={providers} />;
},
},
});
+64
View File
@@ -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,
},
];
@@ -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<Props> = ({ onResult, providers }) => {
@@ -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 (
<Grid item>
<InfoCard
title={title}
actions={
<Button color="primary" variant="outlined" onClick={handleLogin}>
Sign In
</Button>
}
>
<Typography variant="body1">{message}</Typography>
</InfoCard>
</Grid>
);
};
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 };
@@ -15,3 +15,6 @@
*/
export { SignInPage } from './SignInPage';
export { commonProvider } from './commonProvider';
export { guestProvider } from './guestProvider';
export { customProvider } from './customProvider';
@@ -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 <Component key={providerId} onResult={handleResult} />;
return (
<Component
key={config.id}
provider={config}
onResult={handleResult}
/>
);
}),
[providers, handleWrappedResult],
);
+36 -2
View File
@@ -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<SignInPageProps>;
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<SignInResult | undefined>;
export type SignInProvider = {