From 0406ace29aba7332a98ff9ef9feedd65adc75223 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 22 Sep 2020 11:41:57 +0200 Subject: [PATCH] core: refactor SessionStateApi to SessionApi with sign-in/out --- .../src/apis/definitions/IdentityApi.ts | 4 +- .../core-api/src/apis/definitions/auth.ts | 46 +++++----- .../implementations/auth/github/GithubAuth.ts | 20 +++-- .../implementations/auth/oauth2/OAuth2.ts | 16 ++-- packages/core-api/src/app/AppIdentity.ts | 10 +-- packages/core-api/src/app/types.ts | 5 +- .../Sidebar/DefaultProviderSettings.tsx | 14 +-- .../Settings/OAuthProviderSettings.tsx | 80 ----------------- .../Sidebar/Settings/OIDCProviderSettings.tsx | 81 ----------------- .../Sidebar/Settings/ProviderSettingsItem.tsx | 90 ++++++++++++------- .../Sidebar/Settings/UserSettingsMenu.tsx | 2 +- .../core/src/layout/Sidebar/Settings/index.ts | 2 - .../src/layout/Sidebar/Sidebar.stories.tsx | 4 +- .../src/layout/SignInPage/auth0Provider.tsx | 8 +- .../src/layout/SignInPage/commonProvider.tsx | 8 +- .../core/src/layout/SignInPage/providers.tsx | 6 +- packages/core/src/layout/SignInPage/types.ts | 11 +-- packages/storybook/.storybook/apis.js | 2 +- 18 files changed, 138 insertions(+), 271 deletions(-) delete mode 100644 packages/core/src/layout/Sidebar/Settings/OAuthProviderSettings.tsx delete mode 100644 packages/core/src/layout/Sidebar/Settings/OIDCProviderSettings.tsx diff --git a/packages/core-api/src/apis/definitions/IdentityApi.ts b/packages/core-api/src/apis/definitions/IdentityApi.ts index 13f4cd24bc..2684422b1e 100644 --- a/packages/core-api/src/apis/definitions/IdentityApi.ts +++ b/packages/core-api/src/apis/definitions/IdentityApi.ts @@ -46,9 +46,9 @@ export type IdentityApi = { // TODO: getProfile(): Promise - We want this to be async when added, but needs more work. /** - * Log out the current user + * Sign out the current user */ - logout(): Promise; + signOut(): Promise; }; export const identityApiRef = createApiRef({ diff --git a/packages/core-api/src/apis/definitions/auth.ts b/packages/core-api/src/apis/definitions/auth.ts index 1ff8c46dad..800a865fea 100644 --- a/packages/core-api/src/apis/definitions/auth.ts +++ b/packages/core-api/src/apis/definitions/auth.ts @@ -90,11 +90,6 @@ export type OAuthApi = { scope?: OAuthScope, options?: AuthRequestOptions, ): Promise; - - /** - * Log out the user's session. This will reload the page. - */ - logout(): Promise; }; /** @@ -114,11 +109,6 @@ export type OpenIdConnectApi = { * The returned promise can be rejected, but only if the user rejects the login request. */ getIdToken(options?: AuthRequestOptions): Promise; - - /** - * Log out the user's session. This will reload the page. - */ - logout(): Promise; }; /** @@ -187,7 +177,7 @@ export type ProfileInfo = { }; /** - * Session state values passed to subscribers of the SessionStateApi. + * Session state values passed to subscribers of the SessionApi. */ export enum SessionState { SignedIn = 'SignedIn', @@ -195,10 +185,22 @@ export enum SessionState { } /** - * This API provides access to an sessionState$ observable which provides an update when the - * user performs a sign in or sign out from an auth provider. + * The SessionApi provides basic controls for any auth provider that is tied to a persistent session. */ -export type SessionStateApi = { +export type SessionApi = { + /** + * Sign in with a minimum set of permissions. + */ + signIn(): Promise; + + /** + * Sign out from the current session. This will reload the page. + */ + signOut(): Promise; + + /** + * Observe the current state of the auth session. Emits the current state on subscription. + */ sessionState$(): Observable; }; @@ -215,7 +217,7 @@ export const googleAuthApiRef = createApiRef< OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & - SessionStateApi + SessionApi >({ id: 'core.auth.google', description: 'Provides authentication towards Google APIs and identities', @@ -228,7 +230,7 @@ export const googleAuthApiRef = createApiRef< * for a full list of supported scopes. */ export const githubAuthApiRef = createApiRef< - OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionStateApi + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi >({ id: 'core.auth.github', description: 'Provides authentication towards GitHub APIs', @@ -245,7 +247,7 @@ export const oktaAuthApiRef = createApiRef< OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & - SessionStateApi + SessionApi >({ id: 'core.auth.okta', description: 'Provides authentication towards Okta APIs', @@ -258,7 +260,7 @@ export const oktaAuthApiRef = createApiRef< * for a full list of supported scopes. */ export const gitlabAuthApiRef = createApiRef< - OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionStateApi + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi >({ id: 'core.auth.gitlab', description: 'Provides authentication towards GitLab APIs', @@ -271,7 +273,7 @@ export const gitlabAuthApiRef = createApiRef< * for a full list of supported scopes. */ export const auth0AuthApiRef = createApiRef< - OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionStateApi + OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi >({ id: 'core.auth.auth0', description: 'Provides authentication towards Auth0 APIs', @@ -289,7 +291,7 @@ export const microsoftAuthApiRef = createApiRef< OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & - SessionStateApi + SessionApi >({ id: 'core.auth.microsoft', description: 'Provides authentication towards Microsoft APIs and identities', @@ -302,8 +304,8 @@ export const oauth2ApiRef = createApiRef< OAuthApi & OpenIdConnectApi & ProfileInfoApi & - SessionStateApi & - BackstageIdentityApi + BackstageIdentityApi & + SessionApi >({ id: 'core.auth.oauth2', description: 'Example of how to use oauth2 custom provider', 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 8b9f807cd8..1d87ff3720 100644 --- a/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts @@ -19,7 +19,7 @@ import { DefaultAuthConnector } from '../../../../lib/AuthConnector'; import { GithubSession } from './types'; import { OAuthApi, - SessionStateApi, + SessionApi, SessionState, ProfileInfo, BackstageIdentity, @@ -61,7 +61,7 @@ const DEFAULT_PROVIDER = { icon: GithubIcon, }; -class GithubAuth implements OAuthApi, SessionStateApi { +class GithubAuth implements OAuthApi, SessionApi { static create({ discoveryApi, environment = 'development', @@ -102,12 +102,20 @@ class GithubAuth implements OAuthApi, SessionStateApi { return new GithubAuth(authSessionStore); } + constructor(private readonly sessionManager: SessionManager) {} + + async signIn() { + await this.getAccessToken(); + } + + async signOut() { + await this.sessionManager.removeSession(); + } + sessionState$(): Observable { return this.sessionManager.sessionState$(); } - constructor(private readonly sessionManager: SessionManager) {} - async getAccessToken(scope?: string, options?: AuthRequestOptions) { const session = await this.sessionManager.getSession({ ...options, @@ -128,10 +136,6 @@ class GithubAuth implements OAuthApi, SessionStateApi { return session?.profile; } - async logout() { - await this.sessionManager.removeSession(); - } - static normalizeScope(scope?: string): Set { if (!scope) { return new Set(); diff --git a/packages/core-api/src/apis/implementations/auth/oauth2/OAuth2.ts b/packages/core-api/src/apis/implementations/auth/oauth2/OAuth2.ts index 27626aac5a..d088ca9798 100644 --- a/packages/core-api/src/apis/implementations/auth/oauth2/OAuth2.ts +++ b/packages/core-api/src/apis/implementations/auth/oauth2/OAuth2.ts @@ -32,7 +32,7 @@ import { ProfileInfo, ProfileInfoApi, SessionState, - SessionStateApi, + SessionApi, BackstageIdentityApi, } from '../../../definitions/auth'; import { OAuth2Session } from './types'; @@ -75,7 +75,7 @@ class OAuth2 OpenIdConnectApi, ProfileInfoApi, BackstageIdentityApi, - SessionStateApi { + SessionApi { static create({ discoveryApi, environment = 'development', @@ -129,6 +129,14 @@ class OAuth2 this.scopeTransform = options.scopeTransform; } + async signIn() { + await this.getAccessToken(); + } + + async signOut() { + await this.sessionManager.removeSession(); + } + sessionState$(): Observable { return this.sessionManager.sessionState$(); } @@ -150,10 +158,6 @@ class OAuth2 return session?.providerInfo.idToken ?? ''; } - async logout() { - await this.sessionManager.removeSession(); - } - async getBackstageIdentity( options: AuthRequestOptions = {}, ): Promise { diff --git a/packages/core-api/src/app/AppIdentity.ts b/packages/core-api/src/app/AppIdentity.ts index 69ee5d28ac..d3e5fe567a 100644 --- a/packages/core-api/src/app/AppIdentity.ts +++ b/packages/core-api/src/app/AppIdentity.ts @@ -26,7 +26,7 @@ export class AppIdentity implements IdentityApi { private userId?: string; private profile?: ProfileInfo; private idTokenFunc?: () => Promise; - private logoutFunc?: () => Promise; + private signOutFunc?: () => Promise; getUserId(): string { if (!this.hasIdentity) { @@ -55,13 +55,13 @@ export class AppIdentity implements IdentityApi { return this.idTokenFunc?.(); } - async logout(): Promise { + async signOut(): Promise { if (!this.hasIdentity) { throw new Error( - 'Tried to access IdentityApi logoutFunc before app was loaded', + 'Tried to access IdentityApi signOutFunc before app was loaded', ); } - await this.logoutFunc?.(); + await this.signOutFunc?.(); location.reload(); } @@ -80,6 +80,6 @@ export class AppIdentity implements IdentityApi { this.userId = result.userId; this.profile = result.profile; this.idTokenFunc = result.getIdToken; - this.logoutFunc = result.logout; + this.signOutFunc = result.signOut; } } diff --git a/packages/core-api/src/app/types.ts b/packages/core-api/src/app/types.ts index 565e073aed..882ecd3d8b 100644 --- a/packages/core-api/src/app/types.ts +++ b/packages/core-api/src/app/types.ts @@ -38,10 +38,11 @@ export type SignInResult = { * Function used to retrieve an ID token for the signed in user. */ getIdToken?: () => Promise; + /** - * Logout handler that will be called if the user requests a logout. + * Sign out handler that will be called if the user requests to sign out. */ - logout?: () => Promise; + signOut?: () => Promise; }; export type SignInPageProps = { diff --git a/packages/core/src/layout/Sidebar/DefaultProviderSettings.tsx b/packages/core/src/layout/Sidebar/DefaultProviderSettings.tsx index 293cefb19a..f96c6adb2f 100644 --- a/packages/core/src/layout/Sidebar/DefaultProviderSettings.tsx +++ b/packages/core/src/layout/Sidebar/DefaultProviderSettings.tsx @@ -25,7 +25,7 @@ import { } from '@backstage/core-api'; import Star from '@material-ui/icons/Star'; import React from 'react'; -import { OAuthProviderSettings, OIDCProviderSettings } from './Settings'; +import { ProviderSettingsItem } from './Settings'; export const DefaultProviderSettings = () => { const configApi = useApi(configApiRef); @@ -35,42 +35,42 @@ export const DefaultProviderSettings = () => { return ( <> {providers.includes('google') && ( - )} {providers.includes('microsoft') && ( - )} {providers.includes('github') && ( - )} {providers.includes('gitlab') && ( - )} {providers.includes('okta') && ( - )} {providers.includes('oauth2') && ( - ; -}; - -export const OAuthProviderSettings: FC = ({ - title, - icon, - apiRef, -}) => { - const api = useApi(apiRef); - const [signedIn, setSignedIn] = useState(false); - - useEffect(() => { - let didCancel = false; - - const checkSession = async () => { - const session = await api.getAccessToken('', { optional: true }); - if (!didCancel) { - setSignedIn(!!session); - } - }; - let subscription: Subscription; - const observeSession = () => { - subscription = api - .sessionState$() - .subscribe((sessionState: SessionState) => { - if (!didCancel) { - setSignedIn(sessionState === SessionState.SignedIn); - } - }); - }; - - checkSession(); - observeSession(); - return () => { - didCancel = true; - subscription.unsubscribe(); - }; - }, [api]); - - return ( - api.getAccessToken()} - /> - ); -}; diff --git a/packages/core/src/layout/Sidebar/Settings/OIDCProviderSettings.tsx b/packages/core/src/layout/Sidebar/Settings/OIDCProviderSettings.tsx deleted file mode 100644 index 19ee00eee1..0000000000 --- a/packages/core/src/layout/Sidebar/Settings/OIDCProviderSettings.tsx +++ /dev/null @@ -1,81 +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 { - ApiRef, - OpenIdConnectApi, - SessionStateApi, - useApi, - Subscription, - IconComponent, - SessionState, -} from '@backstage/core-api'; -import React, { FC, useState, useEffect } from 'react'; -import { ProviderSettingsItem } from './ProviderSettingsItem'; - -export type OIDCProviderSidebarProps = { - title: string; - icon: IconComponent; - apiRef: ApiRef; -}; - -export const OIDCProviderSettings: FC = ({ - title, - icon, - apiRef, -}) => { - const api = useApi(apiRef); - const [signedIn, setSignedIn] = useState(false); - - useEffect(() => { - let didCancel = false; - - const checkSession = async () => { - const session = await api.getIdToken({ optional: true }); - if (!didCancel) { - setSignedIn(!!session); - } - }; - - let subscription: Subscription; - const observeSession = () => { - subscription = api - .sessionState$() - .subscribe((sessionState: SessionState) => { - if (!didCancel) { - setSignedIn(sessionState === SessionState.SignedIn); - } - }); - }; - - checkSession(); - observeSession(); - return () => { - didCancel = true; - subscription.unsubscribe(); - }; - }, [api]); - - return ( - api.getIdToken()} - /> - ); -}; diff --git a/packages/core/src/layout/Sidebar/Settings/ProviderSettingsItem.tsx b/packages/core/src/layout/Sidebar/Settings/ProviderSettingsItem.tsx index 653c64575b..4db7ea0823 100644 --- a/packages/core/src/layout/Sidebar/Settings/ProviderSettingsItem.tsx +++ b/packages/core/src/layout/Sidebar/Settings/ProviderSettingsItem.tsx @@ -14,8 +14,7 @@ * limitations under the License. */ -import React from 'react'; -import { IconComponent, OAuthApi, OpenIdConnectApi } from '@backstage/core-api'; +import React, { FC, useState, useEffect } from 'react'; import { ListItem, ListItemIcon, @@ -25,42 +24,67 @@ import { } from '@material-ui/core'; import PowerButton from '@material-ui/icons/PowerSettingsNew'; import { ToggleButton } from '@material-ui/lab'; +import { + ApiRef, + SessionApi, + useApi, + IconComponent, + SessionState, +} from '@backstage/core-api'; -type Props = { +type OAuthProviderSidebarProps = { title: string; icon: IconComponent; - signedIn: boolean; - api: OAuthApi | OpenIdConnectApi; - signInHandler: Function; + apiRef: ApiRef; }; -export const ProviderSettingsItem = ({ +export const ProviderSettingsItem: FC = ({ title, icon: Icon, - signedIn, - api, - signInHandler, -}: Props) => ( - - - - - - - (signedIn ? api.logout() : signInHandler())} - > - { + const api = useApi(apiRef); + const [signedIn, setSignedIn] = useState(false); + + useEffect(() => { + let didCancel = false; + + const subscription = api + .sessionState$() + .subscribe((sessionState: SessionState) => { + if (!didCancel) { + setSignedIn(sessionState === SessionState.SignedIn); + } + }); + + return () => { + didCancel = true; + subscription.unsubscribe(); + }; + }, [api]); + + return ( + + + + + + + (signedIn ? api.signOut() : api.signIn())} > - - - - - -); + + + + + + + ); +}; diff --git a/packages/core/src/layout/Sidebar/Settings/UserSettingsMenu.tsx b/packages/core/src/layout/Sidebar/Settings/UserSettingsMenu.tsx index 9ac287c57e..151ddb6e75 100644 --- a/packages/core/src/layout/Sidebar/Settings/UserSettingsMenu.tsx +++ b/packages/core/src/layout/Sidebar/Settings/UserSettingsMenu.tsx @@ -43,7 +43,7 @@ export const UserSettingsMenu = () => { - identityApi.logout()}> + identityApi.signOut()}> diff --git a/packages/core/src/layout/Sidebar/Settings/index.ts b/packages/core/src/layout/Sidebar/Settings/index.ts index 7abf061620..15042dc85d 100644 --- a/packages/core/src/layout/Sidebar/Settings/index.ts +++ b/packages/core/src/layout/Sidebar/Settings/index.ts @@ -15,6 +15,4 @@ */ export { ProviderSettingsItem } from './ProviderSettingsItem'; -export { OAuthProviderSettings } from './OAuthProviderSettings'; -export { OIDCProviderSettings } from './OIDCProviderSettings'; export { SidebarUserSettings } from './UserSettings'; diff --git a/packages/core/src/layout/Sidebar/Sidebar.stories.tsx b/packages/core/src/layout/Sidebar/Sidebar.stories.tsx index d7451965fe..61975a728a 100644 --- a/packages/core/src/layout/Sidebar/Sidebar.stories.tsx +++ b/packages/core/src/layout/Sidebar/Sidebar.stories.tsx @@ -23,7 +23,7 @@ import { SidebarSearchField, SidebarSpace, SidebarUserSettings, - OAuthProviderSettings, + ProviderSettingsItem, } from '.'; import HomeOutlinedIcon from '@material-ui/icons/HomeOutlined'; import AddCircleOutlineIcon from '@material-ui/icons/AddCircleOutline'; @@ -60,7 +60,7 @@ export const SampleSidebar = () => ( { profile: profile!, getIdToken: () => auth0AuthApi.getBackstageIdentity().then(i => i!.idToken), - logout: async () => { - await auth0AuthApi.logout(); + signOut: async () => { + await auth0AuthApi.signOut(); }, }); } catch (error) { @@ -79,8 +79,8 @@ const loader: ProviderLoader = async apis => { userId: identity.id, profile: profile!, getIdToken: () => auth0AuthApi.getBackstageIdentity().then(i => i!.idToken), - logout: async () => { - await auth0AuthApi.logout(); + signOut: async () => { + await auth0AuthApi.signOut(); }, }; }; diff --git a/packages/core/src/layout/SignInPage/commonProvider.tsx b/packages/core/src/layout/SignInPage/commonProvider.tsx index de9452a850..b7480afa49 100644 --- a/packages/core/src/layout/SignInPage/commonProvider.tsx +++ b/packages/core/src/layout/SignInPage/commonProvider.tsx @@ -44,8 +44,8 @@ const Component: ProviderComponent = ({ config, onResult }) => { getIdToken: () => { return authApi.getBackstageIdentity().then(i => i!.idToken); }, - logout: async () => { - await authApi.logout(); + signOut: async () => { + await authApi.signOut(); }, }); } catch (error) { @@ -87,8 +87,8 @@ const loader: ProviderLoader = async (apis, apiRef) => { userId: identity.id, profile: profile!, getIdToken: () => authApi.getBackstageIdentity().then(i => i!.idToken), - logout: async () => { - await authApi.logout(); + signOut: async () => { + await authApi.signOut(); }, }; }; diff --git a/packages/core/src/layout/SignInPage/providers.tsx b/packages/core/src/layout/SignInPage/providers.tsx index ff60eba228..e2c17ab80d 100644 --- a/packages/core/src/layout/SignInPage/providers.tsx +++ b/packages/core/src/layout/SignInPage/providers.tsx @@ -83,14 +83,14 @@ export const useSignInProviders = ( const apiHolder = useApiHolder(); const [loading, setLoading] = useState(true); - // This decorates the result with logout logic from this hook + // This decorates the result with sign out logic from this hook const handleWrappedResult = useCallback( (result: SignInResult) => { onResult({ ...result, - logout: async () => { + signOut: async () => { localStorage.removeItem(PROVIDER_STORAGE_KEY); - await result.logout?.(); + await result.signOut?.(); }, }); }, diff --git a/packages/core/src/layout/SignInPage/types.ts b/packages/core/src/layout/SignInPage/types.ts index 9c501e8095..48945e6988 100644 --- a/packages/core/src/layout/SignInPage/types.ts +++ b/packages/core/src/layout/SignInPage/types.ts @@ -20,19 +20,16 @@ import { SignInResult, ApiHolder, ApiRef, - OAuthApi, ProfileInfoApi, BackstageIdentityApi, - SessionStateApi, + SessionApi, } from '@backstage/core-api'; export type SignInConfig = { id: string; title: string; message: string; - apiRef: ApiRef< - OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionStateApi - >; + apiRef: ApiRef; }; export type IdentityProviders = ('guest' | 'custom' | SignInConfig)[]; @@ -43,9 +40,7 @@ export type ProviderComponent = ComponentType< export type ProviderLoader = ( apis: ApiHolder, - apiRef: ApiRef< - OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionStateApi - >, + apiRef: ApiRef, ) => Promise; export type SignInProvider = { diff --git a/packages/storybook/.storybook/apis.js b/packages/storybook/.storybook/apis.js index a0a220d3de..878256ff5c 100644 --- a/packages/storybook/.storybook/apis.js +++ b/packages/storybook/.storybook/apis.js @@ -36,7 +36,7 @@ builder.add(identityApiRef, { getUserId: () => 'guest', getProfile: () => ({ email: 'guest@example.com' }), getIdToken: () => undefined, - logout: async () => {}, + signOut: async () => {}, }); const oauthRequestApi = builder.add(