core: refactor SessionStateApi to SessionApi with sign-in/out

This commit is contained in:
Patrik Oldsberg
2020-09-22 11:41:57 +02:00
parent 38991e622c
commit 0406ace29a
18 changed files with 138 additions and 271 deletions
@@ -46,9 +46,9 @@ export type IdentityApi = {
// TODO: getProfile(): Promise<Profile> - We want this to be async when added, but needs more work.
/**
* Log out the current user
* Sign out the current user
*/
logout(): Promise<void>;
signOut(): Promise<void>;
};
export const identityApiRef = createApiRef<IdentityApi>({
+24 -22
View File
@@ -90,11 +90,6 @@ export type OAuthApi = {
scope?: OAuthScope,
options?: AuthRequestOptions,
): Promise<string>;
/**
* Log out the user's session. This will reload the page.
*/
logout(): Promise<void>;
};
/**
@@ -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<string>;
/**
* Log out the user's session. This will reload the page.
*/
logout(): Promise<void>;
};
/**
@@ -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<void>;
/**
* Sign out from the current session. This will reload the page.
*/
signOut(): Promise<void>;
/**
* Observe the current state of the auth session. Emits the current state on subscription.
*/
sessionState$(): Observable<SessionState>;
};
@@ -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',
@@ -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<GithubSession>) {}
async signIn() {
await this.getAccessToken();
}
async signOut() {
await this.sessionManager.removeSession();
}
sessionState$(): Observable<SessionState> {
return this.sessionManager.sessionState$();
}
constructor(private readonly sessionManager: SessionManager<GithubSession>) {}
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<string> {
if (!scope) {
return new Set();
@@ -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<SessionState> {
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<BackstageIdentity | undefined> {
+5 -5
View File
@@ -26,7 +26,7 @@ export class AppIdentity implements IdentityApi {
private userId?: string;
private profile?: ProfileInfo;
private idTokenFunc?: () => Promise<string>;
private logoutFunc?: () => Promise<void>;
private signOutFunc?: () => Promise<void>;
getUserId(): string {
if (!this.hasIdentity) {
@@ -55,13 +55,13 @@ export class AppIdentity implements IdentityApi {
return this.idTokenFunc?.();
}
async logout(): Promise<void> {
async signOut(): Promise<void> {
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;
}
}
+3 -2
View File
@@ -38,10 +38,11 @@ export type SignInResult = {
* Function used to retrieve an ID token for the signed in user.
*/
getIdToken?: () => Promise<string>;
/**
* 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<void>;
signOut?: () => Promise<void>;
};
export type SignInPageProps = {
@@ -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') && (
<OIDCProviderSettings
<ProviderSettingsItem
title="Google"
apiRef={googleAuthApiRef}
icon={Star}
/>
)}
{providers.includes('microsoft') && (
<OIDCProviderSettings
<ProviderSettingsItem
title="Microsoft"
apiRef={microsoftAuthApiRef}
icon={Star}
/>
)}
{providers.includes('github') && (
<OAuthProviderSettings
<ProviderSettingsItem
title="Github"
apiRef={githubAuthApiRef}
icon={Star}
/>
)}
{providers.includes('gitlab') && (
<OAuthProviderSettings
<ProviderSettingsItem
title="Gitlab"
apiRef={gitlabAuthApiRef}
icon={Star}
/>
)}
{providers.includes('okta') && (
<OIDCProviderSettings
<ProviderSettingsItem
title="Okta"
apiRef={oktaAuthApiRef}
icon={Star}
/>
)}
{providers.includes('oauth2') && (
<OIDCProviderSettings
<ProviderSettingsItem
title="YourOrg"
apiRef={oauth2ApiRef}
icon={Star}
@@ -1,80 +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,
OAuthApi,
SessionStateApi,
useApi,
Subscription,
IconComponent,
SessionState,
} from '@backstage/core-api';
import React, { FC, useState, useEffect } from 'react';
import { ProviderSettingsItem } from './ProviderSettingsItem';
type OAuthProviderSidebarProps = {
title: string;
icon: IconComponent;
apiRef: ApiRef<OAuthApi & SessionStateApi>;
};
export const OAuthProviderSettings: FC<OAuthProviderSidebarProps> = ({
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 (
<ProviderSettingsItem
title={title}
icon={icon}
signedIn={signedIn}
api={api}
signInHandler={() => api.getAccessToken()}
/>
);
};
@@ -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<OpenIdConnectApi & SessionStateApi>;
};
export const OIDCProviderSettings: FC<OIDCProviderSidebarProps> = ({
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 (
<ProviderSettingsItem
title={title}
icon={icon}
signedIn={signedIn}
api={api}
signInHandler={() => api.getIdToken()}
/>
);
};
@@ -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<SessionApi>;
};
export const ProviderSettingsItem = ({
export const ProviderSettingsItem: FC<OAuthProviderSidebarProps> = ({
title,
icon: Icon,
signedIn,
api,
signInHandler,
}: Props) => (
<ListItem>
<ListItemIcon>
<Icon />
</ListItemIcon>
<ListItemText primary={title} />
<ListItemSecondaryAction>
<ToggleButton
size="small"
value={title}
selected={signedIn}
onChange={() => (signedIn ? api.logout() : signInHandler())}
>
<Tooltip
placement="top"
arrow
title={signedIn ? `Sign out from ${title}` : `Sign in to ${title}`}
apiRef,
}) => {
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 (
<ListItem>
<ListItemIcon>
<Icon />
</ListItemIcon>
<ListItemText primary={title} />
<ListItemSecondaryAction>
<ToggleButton
size="small"
value={title}
selected={signedIn}
onChange={() => (signedIn ? api.signOut() : api.signIn())}
>
<PowerButton />
</Tooltip>
</ToggleButton>
</ListItemSecondaryAction>
</ListItem>
);
<Tooltip
placement="top"
arrow
title={signedIn ? `Sign out from ${title}` : `Sign in to ${title}`}
>
<PowerButton />
</Tooltip>
</ToggleButton>
</ListItemSecondaryAction>
</ListItem>
);
};
@@ -43,7 +43,7 @@ export const UserSettingsMenu = () => {
<MoreVertIcon />
</IconButton>
<Menu anchorEl={anchorEl} open={open} onClose={handleClose}>
<MenuItem onClick={() => identityApi.logout()}>
<MenuItem onClick={() => identityApi.signOut()}>
<ListItemIcon>
<SignOutIcon />
</ListItemIcon>
@@ -15,6 +15,4 @@
*/
export { ProviderSettingsItem } from './ProviderSettingsItem';
export { OAuthProviderSettings } from './OAuthProviderSettings';
export { OIDCProviderSettings } from './OIDCProviderSettings';
export { SidebarUserSettings } from './UserSettings';
@@ -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 = () => (
<SidebarDivider />
<SidebarUserSettings
providerSettings={
<OAuthProviderSettings
<ProviderSettingsItem
title="Github"
apiRef={githubAuthApiRef}
icon={Star}
@@ -37,8 +37,8 @@ const Component: ProviderComponent = ({ onResult }) => {
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();
},
};
};
@@ -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();
},
};
};
@@ -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?.();
},
});
},
+3 -8
View File
@@ -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<ProfileInfoApi & BackstageIdentityApi & SessionApi>;
};
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<ProfileInfoApi & BackstageIdentityApi & SessionApi>,
) => Promise<SignInResult | undefined>;
export type SignInProvider = {
+1 -1
View File
@@ -36,7 +36,7 @@ builder.add(identityApiRef, {
getUserId: () => 'guest',
getProfile: () => ({ email: 'guest@example.com' }),
getIdToken: () => undefined,
logout: async () => {},
signOut: async () => {},
});
const oauthRequestApi = builder.add(