Merge pull request #8283 from backstage/mob/identity-api
core-plugin-api: stabilize IdentityApi
This commit is contained in:
@@ -39,6 +39,7 @@ import { FeatureFlagsSaveOptions } from '@backstage/core-plugin-api';
|
||||
import { gitlabAuthApiRef } from '@backstage/core-plugin-api';
|
||||
import { googleAuthApiRef } from '@backstage/core-plugin-api';
|
||||
import { IconComponent } from '@backstage/core-plugin-api';
|
||||
import { IdentityApi } from '@backstage/core-plugin-api';
|
||||
import { microsoftAuthApiRef } from '@backstage/core-plugin-api';
|
||||
import { OAuthApi } from '@backstage/core-plugin-api';
|
||||
import { OAuthRequestApi } from '@backstage/core-plugin-api';
|
||||
@@ -574,10 +575,10 @@ export type SamlSession = {
|
||||
|
||||
// @public
|
||||
export type SignInPageProps = {
|
||||
onResult(result: SignInResult): void;
|
||||
onSignInSuccess(identityApi: IdentityApi): void;
|
||||
};
|
||||
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export type SignInResult = {
|
||||
userId: string;
|
||||
profile: ProfileInfo;
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* 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 {
|
||||
IdentityApi,
|
||||
ProfileInfo,
|
||||
BackstageUserIdentity,
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
function mkError(thing: string) {
|
||||
return new Error(
|
||||
`Tried to access IdentityApi ${thing} before app was loaded`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of the connection between the App-wide IdentityApi
|
||||
* and sign-in page.
|
||||
*/
|
||||
export class AppIdentityProxy implements IdentityApi {
|
||||
private target?: IdentityApi;
|
||||
|
||||
// This is called by the app manager once the sign-in page provides us with an implementation
|
||||
setTarget(identityApi: IdentityApi) {
|
||||
this.target = identityApi;
|
||||
}
|
||||
|
||||
getUserId(): string {
|
||||
if (!this.target) {
|
||||
throw mkError('getUserId');
|
||||
}
|
||||
return this.target.getUserId();
|
||||
}
|
||||
|
||||
getProfile(): ProfileInfo {
|
||||
if (!this.target) {
|
||||
throw mkError('getProfile');
|
||||
}
|
||||
return this.target.getProfile();
|
||||
}
|
||||
|
||||
async getProfileInfo(): Promise<ProfileInfo> {
|
||||
if (!this.target) {
|
||||
throw mkError('getProfileInfo');
|
||||
}
|
||||
return this.target.getProfileInfo();
|
||||
}
|
||||
|
||||
async getBackstageIdentity(): Promise<BackstageUserIdentity> {
|
||||
if (!this.target) {
|
||||
throw mkError('getBackstageIdentity');
|
||||
}
|
||||
return this.target.getBackstageIdentity();
|
||||
}
|
||||
|
||||
async getCredentials(): Promise<{ token?: string | undefined }> {
|
||||
if (!this.target) {
|
||||
throw mkError('getCredentials');
|
||||
}
|
||||
return this.target.getCredentials();
|
||||
}
|
||||
|
||||
async getIdToken(): Promise<string | undefined> {
|
||||
if (!this.target) {
|
||||
throw mkError('getIdToken');
|
||||
}
|
||||
return this.target.getIdToken();
|
||||
}
|
||||
|
||||
async signOut(): Promise<void> {
|
||||
if (!this.target) {
|
||||
throw mkError('signOut');
|
||||
}
|
||||
await this.target.signOut();
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* 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 { IdentityApi, ProfileInfo } from '@backstage/core-plugin-api';
|
||||
import { SignInResult } from './types';
|
||||
|
||||
/**
|
||||
* Implementation of the connection between the App-wide IdentityApi
|
||||
* and sign-in page.
|
||||
*/
|
||||
export class AppIdentity implements IdentityApi {
|
||||
private hasIdentity = false;
|
||||
private userId?: string;
|
||||
private profile?: ProfileInfo;
|
||||
private idTokenFunc?: () => Promise<string>;
|
||||
private signOutFunc?: () => Promise<void>;
|
||||
|
||||
getUserId(): string {
|
||||
if (!this.hasIdentity) {
|
||||
throw new Error(
|
||||
'Tried to access IdentityApi userId before app was loaded',
|
||||
);
|
||||
}
|
||||
return this.userId!;
|
||||
}
|
||||
|
||||
getProfile(): ProfileInfo {
|
||||
if (!this.hasIdentity) {
|
||||
throw new Error(
|
||||
'Tried to access IdentityApi profile before app was loaded',
|
||||
);
|
||||
}
|
||||
return this.profile!;
|
||||
}
|
||||
|
||||
async getIdToken(): Promise<string | undefined> {
|
||||
if (!this.hasIdentity) {
|
||||
throw new Error(
|
||||
'Tried to access IdentityApi idToken before app was loaded',
|
||||
);
|
||||
}
|
||||
return this.idTokenFunc?.();
|
||||
}
|
||||
|
||||
async signOut(): Promise<void> {
|
||||
if (!this.hasIdentity) {
|
||||
throw new Error(
|
||||
'Tried to access IdentityApi signOutFunc before app was loaded',
|
||||
);
|
||||
}
|
||||
await this.signOutFunc?.();
|
||||
location.reload();
|
||||
}
|
||||
|
||||
// This is indirectly called by the sign-in page to continue into the app.
|
||||
setSignInResult(result: SignInResult) {
|
||||
if (this.hasIdentity) {
|
||||
return;
|
||||
}
|
||||
if (!result.userId) {
|
||||
throw new Error('Invalid sign-in result, userId not set');
|
||||
}
|
||||
if (!result.profile) {
|
||||
throw new Error('Invalid sign-in result, profile not set');
|
||||
}
|
||||
this.hasIdentity = true;
|
||||
this.userId = result.userId;
|
||||
this.profile = result.profile;
|
||||
this.idTokenFunc = result.getIdToken;
|
||||
this.signOutFunc = result.signOut;
|
||||
}
|
||||
}
|
||||
@@ -43,12 +43,14 @@ import {
|
||||
AppThemeApi,
|
||||
ConfigApi,
|
||||
featureFlagsApiRef,
|
||||
IdentityApi,
|
||||
identityApiRef,
|
||||
BackstagePlugin,
|
||||
RouteRef,
|
||||
SubRouteRef,
|
||||
ExternalRouteRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { UserIdentity } from '@backstage/core-components';
|
||||
import { ApiFactoryRegistry, ApiResolver } from '../apis/system';
|
||||
import {
|
||||
childDiscoverer,
|
||||
@@ -66,7 +68,7 @@ import { RoutingProvider } from '../routing/RoutingProvider';
|
||||
import { RouteTracker } from '../routing/RouteTracker';
|
||||
import { validateRoutes } from '../routing/validation';
|
||||
import { AppContextProvider } from './AppContext';
|
||||
import { AppIdentity } from './AppIdentity';
|
||||
import { AppIdentityProxy } from '../apis/implementations/IdentityApi/AppIdentityProxy';
|
||||
import {
|
||||
AppComponents,
|
||||
AppConfigLoader,
|
||||
@@ -75,7 +77,6 @@ import {
|
||||
AppRouteBinder,
|
||||
BackstageApp,
|
||||
SignInPageProps,
|
||||
SignInResult,
|
||||
} from './types';
|
||||
import { AppThemeProvider } from './AppThemeProvider';
|
||||
import { defaultConfigLoader } from './defaultConfigLoader';
|
||||
@@ -189,7 +190,7 @@ export class AppManager implements BackstageApp {
|
||||
private readonly defaultApis: Iterable<AnyApiFactory>;
|
||||
private readonly bindRoutes: AppOptions['bindRoutes'];
|
||||
|
||||
private readonly identityApi = new AppIdentity();
|
||||
private readonly appIdentityProxy = new AppIdentityProxy();
|
||||
private readonly apiFactoryRegistry: ApiFactoryRegistry;
|
||||
|
||||
constructor(options: AppOptions) {
|
||||
@@ -344,14 +345,14 @@ export class AppManager implements BackstageApp {
|
||||
component: ComponentType<SignInPageProps>;
|
||||
children: ReactElement;
|
||||
}) => {
|
||||
const [result, setResult] = useState<SignInResult>();
|
||||
const [identityApi, setIdentityApi] = useState<IdentityApi>();
|
||||
|
||||
if (result) {
|
||||
this.identityApi.setSignInResult(result);
|
||||
return children;
|
||||
if (!identityApi) {
|
||||
return <Component onSignInSuccess={setIdentityApi} />;
|
||||
}
|
||||
|
||||
return <Component onResult={setResult} />;
|
||||
this.appIdentityProxy.setTarget(identityApi);
|
||||
return children;
|
||||
};
|
||||
|
||||
const AppRouter = ({ children }: PropsWithChildren<{}>) => {
|
||||
@@ -360,13 +361,7 @@ export class AppManager implements BackstageApp {
|
||||
|
||||
// If the app hasn't configured a sign-in page, we just continue as guest.
|
||||
if (!SignInPageComponent) {
|
||||
this.identityApi.setSignInResult({
|
||||
userId: 'guest',
|
||||
profile: {
|
||||
email: 'guest@example.com',
|
||||
displayName: 'Guest',
|
||||
},
|
||||
});
|
||||
this.appIdentityProxy.setTarget(UserIdentity.createGuest());
|
||||
|
||||
return (
|
||||
<RouterComponent>
|
||||
@@ -430,7 +425,7 @@ export class AppManager implements BackstageApp {
|
||||
this.apiFactoryRegistry.register('static', {
|
||||
api: identityApiRef,
|
||||
deps: {},
|
||||
factory: () => this.identityApi,
|
||||
factory: () => this.appIdentityProxy,
|
||||
});
|
||||
|
||||
// It's possible to replace the feature flag API, but since we must have at least
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
SubRouteRef,
|
||||
ExternalRouteRef,
|
||||
PluginOutput,
|
||||
IdentityApi,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { AppConfig } from '@backstage/config';
|
||||
|
||||
@@ -42,6 +43,7 @@ export type BootErrorPageProps = {
|
||||
* The outcome of signing in on the sign-in page.
|
||||
*
|
||||
* @public
|
||||
* @deprecated replaced by passing the {@link @backstage/core-plugin-api#IdentityApi} to the {@link SignInPageProps.onSignInSuccess} instead.
|
||||
*/
|
||||
export type SignInResult = {
|
||||
/**
|
||||
@@ -69,9 +71,9 @@ export type SignInResult = {
|
||||
*/
|
||||
export type SignInPageProps = {
|
||||
/**
|
||||
* Set the sign-in result for the app. This should only be called once.
|
||||
* Set the IdentityApi on successful sign in. This should only be called once.
|
||||
*/
|
||||
onResult(result: SignInResult): void;
|
||||
onSignInSuccess(identityApi: IdentityApi): void;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,6 +9,7 @@ import { ApiRef } from '@backstage/core-plugin-api';
|
||||
import { BackstageIdentityApi } from '@backstage/core-plugin-api';
|
||||
import { BackstagePalette } from '@backstage/theme';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
import { BackstageUserIdentity } from '@backstage/core-plugin-api';
|
||||
import { ButtonProps as ButtonProps_2 } from '@material-ui/core/Button';
|
||||
import { CardHeaderProps } from '@material-ui/core/CardHeader';
|
||||
import { Column } from '@material-table/core';
|
||||
@@ -20,6 +21,7 @@ import { CSSProperties } from 'react';
|
||||
import { ElementType } from 'react';
|
||||
import { ErrorInfo } from 'react';
|
||||
import { IconComponent } from '@backstage/core-plugin-api';
|
||||
import { IdentityApi } from '@backstage/core-plugin-api';
|
||||
import { LinearProgressProps } from '@material-ui/core/LinearProgress';
|
||||
import { LinkProps as LinkProps_2 } from '@material-ui/core/Link';
|
||||
import { LinkProps as LinkProps_3 } from 'react-router-dom';
|
||||
@@ -27,6 +29,7 @@ import MaterialBreadcrumbs from '@material-ui/core/Breadcrumbs';
|
||||
import { MaterialTableProps } from '@material-table/core';
|
||||
import { NavLinkProps } from 'react-router-dom';
|
||||
import { Overrides } from '@material-ui/core/styles/overrides';
|
||||
import { ProfileInfo } from '@backstage/core-plugin-api';
|
||||
import { ProfileInfoApi } from '@backstage/core-plugin-api';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { default as React_2 } from 'react';
|
||||
@@ -35,6 +38,7 @@ import { ReactElement } from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
import { SessionApi } from '@backstage/core-plugin-api';
|
||||
import { SignInPageProps } from '@backstage/core-plugin-api';
|
||||
import { SignInResult } from '@backstage/core-plugin-api';
|
||||
import { SparklinesLineProps } from 'react-sparklines';
|
||||
import { SparklinesProps } from 'react-sparklines';
|
||||
import { StyledComponentProps } from '@material-ui/core/styles';
|
||||
@@ -2314,6 +2318,33 @@ export function useQueryParamState<T>(
|
||||
// @public (undocumented)
|
||||
export function UserIcon(props: IconComponentProps): JSX.Element;
|
||||
|
||||
// @public
|
||||
export class UserIdentity implements IdentityApi {
|
||||
static create(options: {
|
||||
identity: BackstageUserIdentity;
|
||||
authApi: ProfileInfoApi & BackstageIdentityApi & SessionApi;
|
||||
profile?: ProfileInfo;
|
||||
}): IdentityApi;
|
||||
static createGuest(): IdentityApi;
|
||||
static fromLegacy(result: SignInResult): IdentityApi;
|
||||
// (undocumented)
|
||||
getBackstageIdentity(): Promise<BackstageUserIdentity>;
|
||||
// (undocumented)
|
||||
getCredentials(): Promise<{
|
||||
token?: string | undefined;
|
||||
}>;
|
||||
// (undocumented)
|
||||
getIdToken(): Promise<string | undefined>;
|
||||
// (undocumented)
|
||||
getProfile(): ProfileInfo;
|
||||
// (undocumented)
|
||||
getProfileInfo(): Promise<ProfileInfo>;
|
||||
// (undocumented)
|
||||
getUserId(): string;
|
||||
// (undocumented)
|
||||
signOut(): Promise<void>;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "useSupportConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* 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 {
|
||||
IdentityApi,
|
||||
ProfileInfo,
|
||||
BackstageUserIdentity,
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
export class GuestUserIdentity implements IdentityApi {
|
||||
getUserId(): string {
|
||||
return 'guest';
|
||||
}
|
||||
|
||||
async getIdToken(): Promise<string | undefined> {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getProfile(): ProfileInfo {
|
||||
return {
|
||||
email: 'guest@example.com',
|
||||
displayName: 'Guest',
|
||||
};
|
||||
}
|
||||
|
||||
async getProfileInfo(): Promise<ProfileInfo> {
|
||||
return {
|
||||
email: 'guest@example.com',
|
||||
displayName: 'Guest',
|
||||
};
|
||||
}
|
||||
|
||||
async getBackstageIdentity(): Promise<BackstageUserIdentity> {
|
||||
const userEntityRef = `user:default/guest`;
|
||||
return {
|
||||
type: 'user',
|
||||
userEntityRef,
|
||||
ownershipEntityRefs: [userEntityRef],
|
||||
};
|
||||
}
|
||||
|
||||
async getCredentials(): Promise<{ token?: string | undefined }> {
|
||||
return {};
|
||||
}
|
||||
|
||||
async signOut(): Promise<void> {}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* 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 {
|
||||
BackstageUserIdentity,
|
||||
IdentityApi,
|
||||
ProfileInfo,
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
export class IdentityApiSignOutProxy implements IdentityApi {
|
||||
private constructor(
|
||||
private readonly config: {
|
||||
identityApi: IdentityApi;
|
||||
signOut: IdentityApi['signOut'];
|
||||
},
|
||||
) {}
|
||||
|
||||
static from(config: {
|
||||
identityApi: IdentityApi;
|
||||
signOut: IdentityApi['signOut'];
|
||||
}): IdentityApi {
|
||||
return new IdentityApiSignOutProxy(config);
|
||||
}
|
||||
|
||||
getUserId(): string {
|
||||
return this.config.identityApi.getUserId();
|
||||
}
|
||||
|
||||
getIdToken(): Promise<string | undefined> {
|
||||
return this.config.identityApi.getIdToken();
|
||||
}
|
||||
|
||||
getProfile(): ProfileInfo {
|
||||
return this.config.identityApi.getProfile();
|
||||
}
|
||||
|
||||
getProfileInfo(): Promise<ProfileInfo> {
|
||||
return this.config.identityApi.getProfileInfo();
|
||||
}
|
||||
|
||||
getBackstageIdentity(): Promise<BackstageUserIdentity> {
|
||||
return this.config.identityApi.getBackstageIdentity();
|
||||
}
|
||||
|
||||
getCredentials(): Promise<{ token?: string | undefined }> {
|
||||
return this.config.identityApi.getCredentials();
|
||||
}
|
||||
|
||||
signOut(): Promise<void> {
|
||||
return this.config.signOut();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* 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 {
|
||||
IdentityApi,
|
||||
ProfileInfo,
|
||||
BackstageUserIdentity,
|
||||
SignInResult,
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
function parseJwtPayload(token: string) {
|
||||
const [_header, payload, _signature] = token.split('.');
|
||||
return JSON.parse(atob(payload));
|
||||
}
|
||||
|
||||
export class LegacyUserIdentity implements IdentityApi {
|
||||
private constructor(private readonly result: SignInResult) {}
|
||||
|
||||
getUserId(): string {
|
||||
return this.result.userId;
|
||||
}
|
||||
|
||||
static fromResult(result: SignInResult): LegacyUserIdentity {
|
||||
return new LegacyUserIdentity(result);
|
||||
}
|
||||
|
||||
async getIdToken(): Promise<string | undefined> {
|
||||
return this.result.getIdToken?.();
|
||||
}
|
||||
|
||||
getProfile(): ProfileInfo {
|
||||
return this.result.profile;
|
||||
}
|
||||
|
||||
async getProfileInfo(): Promise<ProfileInfo> {
|
||||
return this.result.profile;
|
||||
}
|
||||
|
||||
async getBackstageIdentity(): Promise<BackstageUserIdentity> {
|
||||
const token = await this.getIdToken();
|
||||
|
||||
if (!token) {
|
||||
const userEntityRef = `user:default/${this.getUserId()}`;
|
||||
return {
|
||||
type: 'user',
|
||||
userEntityRef,
|
||||
ownershipEntityRefs: [userEntityRef],
|
||||
};
|
||||
}
|
||||
|
||||
const { sub, ent } = parseJwtPayload(token);
|
||||
return {
|
||||
type: 'user',
|
||||
userEntityRef: sub,
|
||||
ownershipEntityRefs: ent ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
async getCredentials(): Promise<{ token?: string | undefined }> {
|
||||
const token = await this.result.getIdToken?.();
|
||||
return { token };
|
||||
}
|
||||
|
||||
async signOut(): Promise<void> {
|
||||
return this.result.signOut?.();
|
||||
}
|
||||
}
|
||||
@@ -15,11 +15,12 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
BackstageIdentity,
|
||||
BackstageIdentityResponse,
|
||||
configApiRef,
|
||||
SignInPageProps,
|
||||
useApi,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { UserIdentity } from './UserIdentity';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
@@ -49,7 +50,7 @@ type SingleSignInPageProps = SignInPageProps & {
|
||||
export type Props = MultiSignInPageProps | SingleSignInPageProps;
|
||||
|
||||
export const MultiSignInPage = ({
|
||||
onResult,
|
||||
onSignInSuccess,
|
||||
providers = [],
|
||||
title,
|
||||
align = 'left',
|
||||
@@ -60,7 +61,7 @@ export const MultiSignInPage = ({
|
||||
const signInProviders = getSignInProviders(providers);
|
||||
const [loading, providerElements] = useSignInProviders(
|
||||
signInProviders,
|
||||
onResult,
|
||||
onSignInSuccess,
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
@@ -87,9 +88,9 @@ export const MultiSignInPage = ({
|
||||
};
|
||||
|
||||
export const SingleSignInPage = ({
|
||||
onResult,
|
||||
provider,
|
||||
auto,
|
||||
onSignInSuccess,
|
||||
}: SingleSignInPageProps) => {
|
||||
const classes = useStyles();
|
||||
const authApi = useApi(provider.apiRef);
|
||||
@@ -105,47 +106,42 @@ export const SingleSignInPage = ({
|
||||
type LoginOpts = { checkExisting?: boolean; showPopup?: boolean };
|
||||
const login = async ({ checkExisting, showPopup }: LoginOpts) => {
|
||||
try {
|
||||
let identity: BackstageIdentity | undefined;
|
||||
let identityResponse: BackstageIdentityResponse | undefined;
|
||||
if (checkExisting) {
|
||||
// Do an initial check if any logged-in session exists
|
||||
identity = await authApi.getBackstageIdentity({
|
||||
identityResponse = await authApi.getBackstageIdentity({
|
||||
optional: true,
|
||||
});
|
||||
}
|
||||
|
||||
// If no session exists, show the sign-in page
|
||||
if (!identity && (showPopup || auto)) {
|
||||
if (!identityResponse && (showPopup || auto)) {
|
||||
// Unless auto is set to true, this step should not happen.
|
||||
// When user intentionally clicks the Sign In button, autoShowPopup is set to true
|
||||
setShowLoginPage(true);
|
||||
identity = await authApi.getBackstageIdentity({
|
||||
identityResponse = await authApi.getBackstageIdentity({
|
||||
instantPopup: true,
|
||||
});
|
||||
if (!identity) {
|
||||
if (!identityResponse) {
|
||||
throw new Error(
|
||||
`The ${provider.title} provider is not configured to support sign-in`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!identity) {
|
||||
if (!identityResponse) {
|
||||
setShowLoginPage(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const profile = await authApi.getProfile();
|
||||
onResult({
|
||||
userId: identity!.id,
|
||||
profile: profile!,
|
||||
getIdToken: () => {
|
||||
return authApi
|
||||
.getBackstageIdentity()
|
||||
.then(i => i!.token ?? i!.idToken);
|
||||
},
|
||||
signOut: async () => {
|
||||
await authApi.signOut();
|
||||
},
|
||||
});
|
||||
onSignInSuccess(
|
||||
UserIdentity.create({
|
||||
identity: identityResponse.identity,
|
||||
authApi,
|
||||
profile,
|
||||
}),
|
||||
);
|
||||
} catch (err: any) {
|
||||
// User closed the sign-in modal
|
||||
setError(err);
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* 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 { BackstageUserIdentity, ProfileInfo } from '@backstage/core-plugin-api';
|
||||
import { UserIdentity } from './UserIdentity';
|
||||
|
||||
describe('UserIdentity', () => {
|
||||
it('should cache a successful response from the AuthApi for getProfile', async () => {
|
||||
const mockIdentity: BackstageUserIdentity = {
|
||||
type: 'user',
|
||||
userEntityRef: 'user:default/blam',
|
||||
ownershipEntityRefs: [],
|
||||
};
|
||||
|
||||
const mockProfileInfo: ProfileInfo = {
|
||||
displayName: 'Blam',
|
||||
email: 'blob@boop.com',
|
||||
};
|
||||
|
||||
const mockAuthApi: any = {
|
||||
getProfile: jest.fn().mockResolvedValue(mockProfileInfo),
|
||||
};
|
||||
|
||||
const userIdentity = UserIdentity.create({
|
||||
authApi: mockAuthApi,
|
||||
identity: mockIdentity,
|
||||
});
|
||||
|
||||
await userIdentity.getProfileInfo();
|
||||
await userIdentity.getProfileInfo();
|
||||
|
||||
const response = await userIdentity.getProfileInfo();
|
||||
|
||||
expect(mockAuthApi.getProfile).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(response).toEqual(mockProfileInfo);
|
||||
});
|
||||
|
||||
it('should not cache failures for the AuthApi for getProfile', async () => {
|
||||
const mockIdentity: BackstageUserIdentity = {
|
||||
type: 'user',
|
||||
userEntityRef: 'user:default/blam',
|
||||
ownershipEntityRefs: [],
|
||||
};
|
||||
|
||||
const mockProfileInfo: ProfileInfo = {
|
||||
displayName: 'Blam',
|
||||
email: 'blob@boop.com',
|
||||
};
|
||||
|
||||
const mockAuthApi: any = {
|
||||
getProfile: jest
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new Error('boop'))
|
||||
.mockResolvedValueOnce(mockProfileInfo),
|
||||
};
|
||||
|
||||
const userIdentity = UserIdentity.create({
|
||||
authApi: mockAuthApi,
|
||||
identity: mockIdentity,
|
||||
});
|
||||
|
||||
await expect(() => userIdentity.getProfileInfo()).rejects.toThrow('boop');
|
||||
const response = await userIdentity.getProfileInfo();
|
||||
|
||||
expect(mockAuthApi.getProfile).toHaveBeenCalledTimes(2);
|
||||
|
||||
expect(response).toEqual(mockProfileInfo);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* 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 {
|
||||
IdentityApi,
|
||||
ProfileInfo,
|
||||
ProfileInfoApi,
|
||||
BackstageUserIdentity,
|
||||
BackstageIdentityApi,
|
||||
SessionApi,
|
||||
SignInResult,
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
import { GuestUserIdentity } from './GuestUserIdentity';
|
||||
import { LegacyUserIdentity } from './LegacyUserIdentity';
|
||||
|
||||
/**
|
||||
* An implementation of the IdentityApi that is constructed using
|
||||
* various backstage user identity representations.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class UserIdentity implements IdentityApi {
|
||||
private profilePromise?: Promise<ProfileInfo>;
|
||||
/**
|
||||
* Creates a new IdentityApi that acts as a Guest User.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
static createGuest(): IdentityApi {
|
||||
return new GuestUserIdentity();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new IdentityApi using a legacy SignInResult object.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
static fromLegacy(result: SignInResult): IdentityApi {
|
||||
return LegacyUserIdentity.fromResult(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new IdentityApi implementation using a user identity
|
||||
* and an auth API that will be used to request backstage tokens.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
static create(options: {
|
||||
identity: BackstageUserIdentity;
|
||||
authApi: ProfileInfoApi & BackstageIdentityApi & SessionApi;
|
||||
/**
|
||||
* Passing a profile synchronously allows the deprecated `getProfile` method to be
|
||||
* called by consumers of the {@link @backstage/core-plugin-api#IdentityApi}. If you
|
||||
* do not have any consumers of that method then this is safe to leave out.
|
||||
*
|
||||
* @deprecated Only provide this if you have plugins that call the synchronous `getProfile` method, which is also deprecated.
|
||||
*/
|
||||
profile?: ProfileInfo;
|
||||
}): IdentityApi {
|
||||
return new UserIdentity(options.identity, options.authApi, options.profile);
|
||||
}
|
||||
|
||||
private constructor(
|
||||
private readonly identity: BackstageUserIdentity,
|
||||
private readonly authApi: ProfileInfoApi &
|
||||
BackstageIdentityApi &
|
||||
SessionApi,
|
||||
private readonly profile?: ProfileInfo,
|
||||
) {}
|
||||
|
||||
/** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getUserId} */
|
||||
getUserId(): string {
|
||||
const ref = this.identity.userEntityRef;
|
||||
const match = /^([^:/]+:)?([^:/]+\/)?([^:/]+)$/.exec(ref);
|
||||
if (!match) {
|
||||
throw new TypeError(`Invalid user entity reference "${ref}"`);
|
||||
}
|
||||
|
||||
return match[3];
|
||||
}
|
||||
|
||||
/** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getIdToken} */
|
||||
async getIdToken(): Promise<string | undefined> {
|
||||
const identity = await this.authApi.getBackstageIdentity();
|
||||
return identity!.token;
|
||||
}
|
||||
|
||||
/** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getProfile} */
|
||||
getProfile(): ProfileInfo {
|
||||
if (!this.profile) {
|
||||
throw new Error(
|
||||
'The identity API does not implement synchronous profile fetching, use getProfileInfo() instead',
|
||||
);
|
||||
}
|
||||
return this.profile;
|
||||
}
|
||||
|
||||
/** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getProfileInfo} */
|
||||
async getProfileInfo(): Promise<ProfileInfo> {
|
||||
if (this.profilePromise) {
|
||||
return await this.profilePromise;
|
||||
}
|
||||
|
||||
try {
|
||||
this.profilePromise = this.authApi.getProfile() as Promise<ProfileInfo>;
|
||||
return await this.profilePromise;
|
||||
} catch (ex) {
|
||||
this.profilePromise = undefined;
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
/** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getBackstageIdentity} */
|
||||
async getBackstageIdentity(): Promise<BackstageUserIdentity> {
|
||||
return this.identity;
|
||||
}
|
||||
|
||||
/** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getCredentials} */
|
||||
async getCredentials(): Promise<{ token?: string | undefined }> {
|
||||
const identity = await this.authApi.getBackstageIdentity();
|
||||
return { token: identity!.token };
|
||||
}
|
||||
|
||||
/** {@inheritdoc @backstage/core-plugin-api#IdentityApi.signOut} */
|
||||
async signOut(): Promise<void> {
|
||||
return this.authApi.signOut();
|
||||
}
|
||||
}
|
||||
@@ -26,17 +26,18 @@ import {
|
||||
errorApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { ForwardedError } from '@backstage/errors';
|
||||
import { UserIdentity } from './UserIdentity';
|
||||
|
||||
const Component: ProviderComponent = ({ onResult }) => {
|
||||
const Component: ProviderComponent = ({ onSignInSuccess }) => {
|
||||
const auth0AuthApi = useApi(auth0AuthApiRef);
|
||||
const errorApi = useApi(errorApiRef);
|
||||
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
const identity = await auth0AuthApi.getBackstageIdentity({
|
||||
const identityResponse = await auth0AuthApi.getBackstageIdentity({
|
||||
instantPopup: true,
|
||||
});
|
||||
if (!identity) {
|
||||
if (!identityResponse) {
|
||||
throw new Error(
|
||||
'The Auth0 provider is not configured to support sign-in',
|
||||
);
|
||||
@@ -44,15 +45,13 @@ const Component: ProviderComponent = ({ onResult }) => {
|
||||
|
||||
const profile = await auth0AuthApi.getProfile();
|
||||
|
||||
onResult({
|
||||
userId: identity!.id,
|
||||
profile: profile!,
|
||||
getIdToken: () =>
|
||||
auth0AuthApi.getBackstageIdentity().then(i => i!.token ?? i!.idToken),
|
||||
signOut: async () => {
|
||||
await auth0AuthApi.signOut();
|
||||
},
|
||||
});
|
||||
onSignInSuccess(
|
||||
UserIdentity.create({
|
||||
identity: identityResponse.identity,
|
||||
authApi: auth0AuthApi,
|
||||
profile,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
errorApi.post(new ForwardedError('Auth0 login failed', error));
|
||||
}
|
||||
@@ -77,25 +76,20 @@ const Component: ProviderComponent = ({ onResult }) => {
|
||||
const loader: ProviderLoader = async apis => {
|
||||
const auth0AuthApi = apis.get(auth0AuthApiRef)!;
|
||||
|
||||
const identity = await auth0AuthApi.getBackstageIdentity({
|
||||
const identityResponse = await auth0AuthApi.getBackstageIdentity({
|
||||
optional: true,
|
||||
});
|
||||
|
||||
if (!identity) {
|
||||
if (!identityResponse) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const profile = await auth0AuthApi.getProfile();
|
||||
|
||||
return {
|
||||
userId: identity.id,
|
||||
profile: profile!,
|
||||
getIdToken: () =>
|
||||
auth0AuthApi.getBackstageIdentity().then(i => i!.token ?? i!.idToken),
|
||||
signOut: async () => {
|
||||
await auth0AuthApi.signOut();
|
||||
},
|
||||
};
|
||||
return UserIdentity.create({
|
||||
identity: identityResponse.identity,
|
||||
authApi: auth0AuthApi,
|
||||
profile,
|
||||
});
|
||||
};
|
||||
|
||||
export const auth0Provider: SignInProvider = { Component, loader };
|
||||
|
||||
@@ -27,36 +27,33 @@ import {
|
||||
import { useApi, errorApiRef } from '@backstage/core-plugin-api';
|
||||
import { GridItem } from './styles';
|
||||
import { ForwardedError } from '@backstage/errors';
|
||||
import { UserIdentity } from './UserIdentity';
|
||||
|
||||
const Component: ProviderComponent = ({ config, onResult }) => {
|
||||
const Component: ProviderComponent = ({ config, onSignInSuccess }) => {
|
||||
const { apiRef, title, message } = config as SignInProviderConfig;
|
||||
const authApi = useApi(apiRef);
|
||||
const errorApi = useApi(errorApiRef);
|
||||
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
const identity = await authApi.getBackstageIdentity({
|
||||
const identityResponse = await authApi.getBackstageIdentity({
|
||||
instantPopup: true,
|
||||
});
|
||||
if (!identity) {
|
||||
if (!identityResponse) {
|
||||
throw new Error(
|
||||
`The ${title} provider is not configured to support sign-in`,
|
||||
);
|
||||
}
|
||||
|
||||
const profile = await authApi.getProfile();
|
||||
onResult({
|
||||
userId: identity!.id,
|
||||
profile: profile!,
|
||||
getIdToken: () => {
|
||||
return authApi
|
||||
.getBackstageIdentity()
|
||||
.then(i => i!.token ?? i!.idToken);
|
||||
},
|
||||
signOut: async () => {
|
||||
await authApi.signOut();
|
||||
},
|
||||
});
|
||||
|
||||
onSignInSuccess(
|
||||
UserIdentity.create({
|
||||
identity: identityResponse.identity,
|
||||
profile,
|
||||
authApi,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
errorApi.post(new ForwardedError('Login failed', error));
|
||||
}
|
||||
@@ -82,25 +79,21 @@ const Component: ProviderComponent = ({ config, onResult }) => {
|
||||
const loader: ProviderLoader = async (apis, apiRef) => {
|
||||
const authApi = apis.get(apiRef)!;
|
||||
|
||||
const identity = await authApi.getBackstageIdentity({
|
||||
const identityResponse = await authApi.getBackstageIdentity({
|
||||
optional: true,
|
||||
});
|
||||
|
||||
if (!identity) {
|
||||
if (!identityResponse) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const profile = await authApi.getProfile();
|
||||
|
||||
return {
|
||||
userId: identity.id,
|
||||
profile: profile!,
|
||||
getIdToken: () =>
|
||||
authApi.getBackstageIdentity().then(i => i!.token ?? i!.idToken),
|
||||
signOut: async () => {
|
||||
await authApi.signOut();
|
||||
},
|
||||
};
|
||||
return UserIdentity.create({
|
||||
identity: identityResponse.identity,
|
||||
profile,
|
||||
authApi,
|
||||
});
|
||||
};
|
||||
|
||||
export const commonProvider: SignInProvider = { Component, loader };
|
||||
|
||||
@@ -26,6 +26,7 @@ import isEmpty from 'lodash/isEmpty';
|
||||
import { InfoCard } from '../InfoCard/InfoCard';
|
||||
import { ProviderComponent, ProviderLoader, SignInProvider } from './types';
|
||||
import { GridItem } from './styles';
|
||||
import { UserIdentity } from './UserIdentity';
|
||||
|
||||
// accept base64url format according to RFC7515 (https://tools.ietf.org/html/rfc7515#section-3)
|
||||
const ID_TOKEN_REGEX = /^[a-z0-9_\-]+\.[a-z0-9_\-]+\.[a-z0-9_\-]+$/i;
|
||||
@@ -60,7 +61,7 @@ const asInputRef = (renderResult: UseFormRegisterReturn) => {
|
||||
};
|
||||
};
|
||||
|
||||
const Component: ProviderComponent = ({ onResult }) => {
|
||||
const Component: ProviderComponent = ({ onSignInSuccess }) => {
|
||||
const classes = useFormStyles();
|
||||
const { register, handleSubmit, formState } = useForm<Data>({
|
||||
mode: 'onChange',
|
||||
@@ -68,14 +69,15 @@ const Component: ProviderComponent = ({ onResult }) => {
|
||||
|
||||
const { errors } = formState;
|
||||
|
||||
const handleResult = ({ userId, idToken }: Data) => {
|
||||
onResult({
|
||||
userId,
|
||||
profile: {
|
||||
email: `${userId}@example.com`,
|
||||
},
|
||||
getIdToken: idToken ? async () => idToken : undefined,
|
||||
});
|
||||
const handleResult = ({ userId }: Data) => {
|
||||
onSignInSuccess(
|
||||
UserIdentity.fromLegacy({
|
||||
userId,
|
||||
profile: {
|
||||
email: `${userId}@example.com`,
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -20,16 +20,9 @@ import Button from '@material-ui/core/Button';
|
||||
import { InfoCard } from '../InfoCard/InfoCard';
|
||||
import { GridItem } from './styles';
|
||||
import { ProviderComponent, ProviderLoader, SignInProvider } from './types';
|
||||
import { GuestUserIdentity } from './GuestUserIdentity';
|
||||
|
||||
const result = {
|
||||
userId: 'guest',
|
||||
profile: {
|
||||
email: 'guest@example.com',
|
||||
displayName: 'Guest',
|
||||
},
|
||||
};
|
||||
|
||||
const Component: ProviderComponent = ({ onResult }) => (
|
||||
const Component: ProviderComponent = ({ onSignInSuccess }) => (
|
||||
<GridItem>
|
||||
<InfoCard
|
||||
title="Guest"
|
||||
@@ -38,7 +31,7 @@ const Component: ProviderComponent = ({ onResult }) => (
|
||||
<Button
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
onClick={() => onResult(result)}
|
||||
onClick={() => onSignInSuccess(new GuestUserIdentity())}
|
||||
>
|
||||
Enter
|
||||
</Button>
|
||||
@@ -56,7 +49,7 @@ const Component: ProviderComponent = ({ onResult }) => (
|
||||
);
|
||||
|
||||
const loader: ProviderLoader = async () => {
|
||||
return result;
|
||||
return new GuestUserIdentity();
|
||||
};
|
||||
|
||||
export const guestProvider: SignInProvider = { Component, loader };
|
||||
|
||||
@@ -18,3 +18,4 @@ export type { SignInProviderConfig } from './types';
|
||||
export { SignInPage } from './SignInPage';
|
||||
export type { SignInPageClassKey } from './styles';
|
||||
export type { CustomProviderClassKey } from './customProvider';
|
||||
export { UserIdentity } from './UserIdentity';
|
||||
|
||||
@@ -17,10 +17,10 @@
|
||||
import React, { useLayoutEffect, useState, useMemo, useCallback } from 'react';
|
||||
import {
|
||||
SignInPageProps,
|
||||
SignInResult,
|
||||
useApi,
|
||||
useApiHolder,
|
||||
errorApiRef,
|
||||
IdentityApi,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import {
|
||||
IdentityProviders,
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
import { commonProvider } from './commonProvider';
|
||||
import { guestProvider } from './guestProvider';
|
||||
import { customProvider } from './customProvider';
|
||||
import { IdentityApiSignOutProxy } from './IdentityApiSignOutProxy';
|
||||
|
||||
const PROVIDER_STORAGE_KEY = '@backstage/core:SignInPage:provider';
|
||||
|
||||
@@ -81,7 +82,7 @@ export function getSignInProviders(
|
||||
|
||||
export const useSignInProviders = (
|
||||
providers: SignInProviderType,
|
||||
onResult: SignInPageProps['onResult'],
|
||||
onSignInSuccess: SignInPageProps['onSignInSuccess'],
|
||||
) => {
|
||||
const errorApi = useApi(errorApiRef);
|
||||
const apiHolder = useApiHolder();
|
||||
@@ -89,16 +90,18 @@ export const useSignInProviders = (
|
||||
|
||||
// This decorates the result with sign out logic from this hook
|
||||
const handleWrappedResult = useCallback(
|
||||
(result: SignInResult) => {
|
||||
onResult({
|
||||
...result,
|
||||
signOut: async () => {
|
||||
localStorage.removeItem(PROVIDER_STORAGE_KEY);
|
||||
await result.signOut?.();
|
||||
},
|
||||
});
|
||||
(identityApi: IdentityApi) => {
|
||||
onSignInSuccess(
|
||||
IdentityApiSignOutProxy.from({
|
||||
identityApi,
|
||||
signOut: async () => {
|
||||
localStorage.removeItem(PROVIDER_STORAGE_KEY);
|
||||
await identityApi.signOut?.();
|
||||
},
|
||||
}),
|
||||
);
|
||||
},
|
||||
[onResult],
|
||||
[onSignInSuccess],
|
||||
);
|
||||
|
||||
// In this effect we check if the user has already selected an existing login
|
||||
@@ -151,7 +154,14 @@ export const useSignInProviders = (
|
||||
return () => {
|
||||
didCancel = true;
|
||||
};
|
||||
}, [loading, errorApi, onResult, apiHolder, providers, handleWrappedResult]);
|
||||
}, [
|
||||
loading,
|
||||
errorApi,
|
||||
onSignInSuccess,
|
||||
apiHolder,
|
||||
providers,
|
||||
handleWrappedResult,
|
||||
]);
|
||||
|
||||
// This renders all available sign-in providers
|
||||
const elements = useMemo(
|
||||
@@ -161,7 +171,7 @@ export const useSignInProviders = (
|
||||
|
||||
const { Component } = provider.components;
|
||||
|
||||
const handleResult = (result: SignInResult) => {
|
||||
const handleSignInSuccess = (result: IdentityApi) => {
|
||||
localStorage.setItem(PROVIDER_STORAGE_KEY, provider.id);
|
||||
|
||||
handleWrappedResult(result);
|
||||
@@ -171,7 +181,7 @@ export const useSignInProviders = (
|
||||
<Component
|
||||
key={provider.id}
|
||||
config={provider.config!}
|
||||
onResult={handleResult}
|
||||
onSignInSuccess={handleSignInSuccess}
|
||||
/>
|
||||
);
|
||||
}),
|
||||
|
||||
@@ -17,12 +17,12 @@
|
||||
import { ComponentType } from 'react';
|
||||
import {
|
||||
SignInPageProps,
|
||||
SignInResult,
|
||||
ApiHolder,
|
||||
ApiRef,
|
||||
ProfileInfoApi,
|
||||
BackstageIdentityApi,
|
||||
SessionApi,
|
||||
IdentityApi,
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
export type SignInProviderConfig = {
|
||||
@@ -41,7 +41,7 @@ export type ProviderComponent = ComponentType<
|
||||
export type ProviderLoader = (
|
||||
apis: ApiHolder,
|
||||
apiRef: ApiRef<ProfileInfoApi & BackstageIdentityApi & SessionApi>,
|
||||
) => Promise<SignInResult | undefined>;
|
||||
) => Promise<IdentityApi | undefined>;
|
||||
|
||||
export type SignInProvider = {
|
||||
Component: ProviderComponent;
|
||||
|
||||
@@ -10,6 +10,7 @@ import { BackstageTheme } from '@backstage/theme';
|
||||
import { ComponentType } from 'react';
|
||||
import { Config } from '@backstage/config';
|
||||
import { IconComponent as IconComponent_2 } from '@backstage/core-plugin-api';
|
||||
import { IdentityApi as IdentityApi_2 } from '@backstage/core-plugin-api';
|
||||
import { Observable as Observable_2 } from '@backstage/types';
|
||||
import { Observer as Observer_2 } from '@backstage/types';
|
||||
import { ProfileInfo as ProfileInfo_2 } from '@backstage/core-plugin-api';
|
||||
@@ -233,18 +234,21 @@ export type AuthRequestOptions = {
|
||||
instantPopup?: boolean;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type BackstageIdentity = {
|
||||
id: string;
|
||||
idToken: string;
|
||||
token: string;
|
||||
};
|
||||
// @public @deprecated
|
||||
export type BackstageIdentity = BackstageIdentityResponse;
|
||||
|
||||
// @public
|
||||
export type BackstageIdentityApi = {
|
||||
getBackstageIdentity(
|
||||
options?: AuthRequestOptions,
|
||||
): Promise<BackstageIdentity | undefined>;
|
||||
): Promise<BackstageIdentityResponse | undefined>;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type BackstageIdentityResponse = {
|
||||
id: string;
|
||||
token: string;
|
||||
identity: BackstageUserIdentity;
|
||||
};
|
||||
|
||||
// @public
|
||||
@@ -261,6 +265,13 @@ export type BackstagePlugin<
|
||||
externalRoutes: ExternalRoutes;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type BackstageUserIdentity = {
|
||||
type: 'user';
|
||||
userEntityRef: string;
|
||||
ownershipEntityRefs: string[];
|
||||
};
|
||||
|
||||
// @public
|
||||
export const bitbucketAuthApiRef: ApiRef<
|
||||
OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi
|
||||
@@ -528,8 +539,13 @@ export type IconComponent = ComponentType<{
|
||||
// @public
|
||||
export type IdentityApi = {
|
||||
getUserId(): string;
|
||||
getProfile(): ProfileInfo;
|
||||
getIdToken(): Promise<string | undefined>;
|
||||
getProfile(): ProfileInfo;
|
||||
getProfileInfo(): Promise<ProfileInfo>;
|
||||
getBackstageIdentity(): Promise<BackstageUserIdentity>;
|
||||
getCredentials(): Promise<{
|
||||
token?: string;
|
||||
}>;
|
||||
signOut(): Promise<void>;
|
||||
};
|
||||
|
||||
@@ -742,10 +758,10 @@ export enum SessionState {
|
||||
|
||||
// @public
|
||||
export type SignInPageProps = {
|
||||
onResult(result: SignInResult): void;
|
||||
onSignInSuccess(identityApi: IdentityApi_2): void;
|
||||
};
|
||||
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export type SignInResult = {
|
||||
userId: string;
|
||||
profile: ProfileInfo_2;
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { ApiRef, createApiRef } from '../system';
|
||||
import { ProfileInfo } from './auth';
|
||||
import { BackstageUserIdentity, ProfileInfo } from './auth';
|
||||
|
||||
/**
|
||||
* The Identity API used to identify and get information about the signed in user.
|
||||
@@ -26,25 +26,45 @@ export type IdentityApi = {
|
||||
* The ID of the signed in user. This ID is not meant to be presented to the user, but used
|
||||
* as an opaque string to pass on to backends or use in frontend logic.
|
||||
*
|
||||
* TODO: The intention of the user ID is to be able to tie the user to an identity
|
||||
* that is known by the catalog and/or identity backend. It should for example
|
||||
* be possible to fetch all owned components using this ID.
|
||||
* @deprecated use {@link IdentityApi.getBackstageIdentity} instead.
|
||||
*/
|
||||
getUserId(): string;
|
||||
|
||||
/**
|
||||
* The profile of the signed in user.
|
||||
*/
|
||||
getProfile(): ProfileInfo;
|
||||
|
||||
/**
|
||||
* An OpenID Connect ID Token which proves the identity of the signed in user.
|
||||
*
|
||||
* The ID token will be undefined if the signed in user does not have a verified
|
||||
* identity, such as a demo user or mocked user for e2e tests.
|
||||
*
|
||||
* @deprecated use {@link IdentityApi.getCredentials} instead.
|
||||
*/
|
||||
getIdToken(): Promise<string | undefined>;
|
||||
|
||||
/**
|
||||
* The profile of the signed in user.
|
||||
*
|
||||
* @deprecated use {@link IdentityApi.getProfileInfo} instead.
|
||||
*/
|
||||
getProfile(): ProfileInfo;
|
||||
|
||||
/**
|
||||
* The profile of the signed in user.
|
||||
*/
|
||||
getProfileInfo(): Promise<ProfileInfo>;
|
||||
|
||||
/**
|
||||
* User identity information within Backstage.
|
||||
*/
|
||||
getBackstageIdentity(): Promise<BackstageUserIdentity>;
|
||||
|
||||
/**
|
||||
* Provides credentials in the form of a token which proves the identity of the signed in user.
|
||||
*
|
||||
* The token will be undefined if the signed in user does not have a verified
|
||||
* identity, such as a demo user or mocked user for e2e tests.
|
||||
*/
|
||||
getCredentials(): Promise<{ token?: string }>;
|
||||
|
||||
/**
|
||||
* Sign out the current user
|
||||
*/
|
||||
|
||||
@@ -160,31 +160,65 @@ export type BackstageIdentityApi = {
|
||||
*/
|
||||
getBackstageIdentity(
|
||||
options?: AuthRequestOptions,
|
||||
): Promise<BackstageIdentity | undefined>;
|
||||
): Promise<BackstageIdentityResponse | undefined>;
|
||||
};
|
||||
|
||||
/**
|
||||
* A (user id, token) pair.
|
||||
* User identity information within Backstage.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type BackstageIdentity = {
|
||||
export type BackstageUserIdentity = {
|
||||
/**
|
||||
* The backstage user ID.
|
||||
* The type of identity that this structure represents. In the frontend app
|
||||
* this will currently always be 'user'.
|
||||
*/
|
||||
id: string;
|
||||
type: 'user';
|
||||
|
||||
/**
|
||||
* @deprecated This is deprecated, use `token` instead.
|
||||
* The entityRef of the user in the catalog.
|
||||
* For example User:default/sandra
|
||||
*/
|
||||
idToken: string;
|
||||
userEntityRef: string;
|
||||
|
||||
/**
|
||||
* The user and group entities that the user claims ownership through
|
||||
*/
|
||||
ownershipEntityRefs: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Token and Identity response, with the users claims in the Identity.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type BackstageIdentityResponse = {
|
||||
/**
|
||||
* The backstage user ID.
|
||||
*
|
||||
* @deprecated The identity is now provided via the `identity` field instead.
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* The token used to authenticate the user within Backstage.
|
||||
*/
|
||||
token: string;
|
||||
|
||||
/**
|
||||
* Identity information derived from the token.
|
||||
*/
|
||||
identity: BackstageUserIdentity;
|
||||
};
|
||||
|
||||
/**
|
||||
* The old exported symbol for {@link BackstageIdentityResponse}.
|
||||
*
|
||||
* @public
|
||||
* @deprecated use {@link BackstageIdentityResponse} instead.
|
||||
*/
|
||||
export type BackstageIdentity = BackstageIdentityResponse;
|
||||
|
||||
/**
|
||||
* Profile information of the user.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user