diff --git a/packages/core-api/src/apis/definitions/IdentityApi.ts b/packages/core-api/src/apis/definitions/IdentityApi.ts index dbc36ad967..13f4cd24bc 100644 --- a/packages/core-api/src/apis/definitions/IdentityApi.ts +++ b/packages/core-api/src/apis/definitions/IdentityApi.ts @@ -14,6 +14,7 @@ * limitations under the License. */ import { createApiRef } from '../ApiRef'; +import { ProfileInfo } from './auth'; /** * The Identity API used to identify and get information about the signed in user. @@ -29,6 +30,11 @@ export type IdentityApi = { */ 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. * diff --git a/packages/core-api/src/apis/definitions/auth.ts b/packages/core-api/src/apis/definitions/auth.ts index c884bf975b..ccd40e66ce 100644 --- a/packages/core-api/src/apis/definitions/auth.ts +++ b/packages/core-api/src/apis/definitions/auth.ts @@ -37,13 +37,13 @@ import { Observable } from '../..'; */ export type OAuthScope = string | string[]; -export type AccessTokenOptions = { +export type AuthRequestOptions = { /** * If this is set to true, the user will not be prompted to log in, - * and an empty access token will be returned if there is no existing session. + * and an empty response will be returned if there is no existing session. * - * This can be used to perform a check whether the user is logged in with a set of scopes, - * or if you don't want to force a user to be logged in, but provide functionality if they already are. + * This can be used to perform a check whether the user is logged in, or if you don't + * want to force a user to be logged in, but provide functionality if they already are. * * @default false */ @@ -88,7 +88,7 @@ export type OAuthApi = { */ getAccessToken( scope?: OAuthScope, - options?: AccessTokenOptions, + options?: AuthRequestOptions, ): Promise; /** @@ -97,29 +97,6 @@ export type OAuthApi = { logout(): Promise; }; -export type IdTokenOptions = { - /** - * If this is set to true, the user will not be prompted to log in, - * and an empty id token will be returned if there is no existing session. - * - * This can be used to perform a check whether the user is logged in, or if you don't - * want to force a user to be logged in, but provide functionality if they already are. - * - * @default false - */ - optional?: boolean; - - /** - * If this is set to true, the request will bypass the regular oauth login modal - * and open the login popup directly. - * - * The method must be called synchronously from a user action for this to work in all browsers. - * - * @default false - */ - instantPopup?: boolean; -}; - /** * This API provides access to OpenID Connect credentials. It lets you request ID tokens, * which can be passed to backend services to prove the user's identity. @@ -136,7 +113,7 @@ export type OpenIdConnectApi = { * to log in. The returned promise will not resolve until the user has successfully logged in. * The returned promise can be rejected, but only if the user rejects the login request. */ - getIdToken(options?: IdTokenOptions): Promise; + getIdToken(options?: AuthRequestOptions): Promise; /** * Log out the user's session. This will reload the page. @@ -144,38 +121,65 @@ export type OpenIdConnectApi = { logout(): Promise; }; -export type ProfileInfoOptions = { - /** - * If this is set to true, the user will not be prompted to log in, - * and an empty profile will be returned if there is no existing session. - * - * This can be used to perform a check whether the user is logged in, or if you don't - * want to force a user to be logged in, but provide functionality if they already are. - * - * @default false - */ - optional?: boolean; -}; - /** * This API provides access to profile information of the user from an auth provider. */ export type ProfileInfoApi = { - getProfile(options?: ProfileInfoOptions): Promise; + /** + * Get profile information for the user as supplied by this auth provider. + * + * If the optional flag is not set, a session is guaranteed to be returned, while if + * the optional flag is set, the session may be undefined. See @AuthRequestOptions for more details. + */ + getProfile(options?: AuthRequestOptions): Promise; }; /** - * Profile information of the user from an auth provider. + * This API provides access to the user's identity within Backstage. + * + * An auth provider that implements this interface can be used to sign-in to backstage. It is + * not intended to be used directly from a plugin, but instead serves as a connection between + * this authentication method and the app's @IdentityApi + */ +export type BackstageIdentityApi = { + /** + * Get the user's identity within Backstage. This should normally not be called directly, + * use the @IdentityApi instead. + * + * If the optional flag is not set, a session is guaranteed to be returned, while if + * the optional flag is set, the session may be undefined. See @AuthRequestOptions for more details. + */ + getBackstageIdentity( + options?: AuthRequestOptions, + ): Promise; +}; + +export type BackstageIdentity = { + /** + * The backstage user ID. + */ + id: string; + + /** + * An ID token that can be used to authenticate the user within Backstage. + */ + idToken: string; +}; + +/** + * Profile information of the user. */ export type ProfileInfo = { /** * Email ID. */ email: string; + /** * Display name that can be presented to the user. */ - name?: string; + displayName?: string; + /** * URL to an avatar image of the user. */ @@ -207,7 +211,11 @@ export type SessionStateApi = { * email and expiration information. Do not rely on any other fields, as they might not be present. */ export const googleAuthApiRef = createApiRef< - OAuthApi & OpenIdConnectApi & ProfileInfoApi & SessionStateApi + OAuthApi & + OpenIdConnectApi & + ProfileInfoApi & + BackstageIdentityApi & + SessionStateApi >({ id: 'core.auth.google', description: 'Provides authentication towards Google APIs and identities', @@ -219,7 +227,9 @@ export const googleAuthApiRef = createApiRef< * See https://developer.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps/ * for a full list of supported scopes. */ -export const githubAuthApiRef = createApiRef({ +export const githubAuthApiRef = createApiRef< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionStateApi +>({ id: 'core.auth.github', description: 'Provides authentication towards Github APIs', }); diff --git a/packages/core-api/src/apis/implementations/auth/github/GithubAuth.test.ts b/packages/core-api/src/apis/implementations/auth/github/GithubAuth.test.ts index 7c8e6ce9f0..3ff29cc6ef 100644 --- a/packages/core-api/src/apis/implementations/auth/github/GithubAuth.test.ts +++ b/packages/core-api/src/apis/implementations/auth/github/GithubAuth.test.ts @@ -20,7 +20,7 @@ describe('GithubAuth', () => { it('should get access token', async () => { const getSession = jest .fn() - .mockResolvedValue({ accessToken: 'access-token' }); + .mockResolvedValue({ providerInfo: { accessToken: 'access-token' } }); const githubAuth = new GithubAuth({ getSession } as any); expect(await githubAuth.getAccessToken()).toBe('access-token'); 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 f4a7092f79..4641dd7b50 100644 --- a/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts @@ -19,9 +19,11 @@ import { DefaultAuthConnector } from '../../../../lib/AuthConnector'; import { GithubSession } from './types'; import { OAuthApi, - AccessTokenOptions, SessionStateApi, SessionState, + ProfileInfo, + BackstageIdentity, + AuthRequestOptions, } from '../../../definitions/auth'; import { OAuthRequestApi, AuthProvider } from '../../../definitions'; import { SessionManager } from '../../../../lib/AuthSessionManager/types'; @@ -41,10 +43,13 @@ type CreateOptions = { }; export type GithubAuthResponse = { - accessToken: string; - idToken: string; - scope: string; - expiresInSeconds: number; + providerInfo: { + accessToken: string; + scope: string; + expiresInSeconds: number; + }; + profile: ProfileInfo; + backstageIdentity: BackstageIdentity; }; const DEFAULT_PROVIDER = { @@ -69,9 +74,14 @@ class GithubAuth implements OAuthApi, SessionStateApi { oauthRequestApi: oauthRequestApi, sessionTransform(res: GithubAuthResponse): GithubSession { return { - accessToken: res.accessToken, - scopes: GithubAuth.normalizeScope(res.scope), - expiresAt: new Date(Date.now() + res.expiresInSeconds * 1000), + ...res, + providerInfo: { + accessToken: res.providerInfo.accessToken, + scopes: GithubAuth.normalizeScope(res.providerInfo.scope), + expiresAt: new Date( + Date.now() + res.providerInfo.expiresInSeconds * 1000, + ), + }, }; }, }); @@ -79,7 +89,7 @@ class GithubAuth implements OAuthApi, SessionStateApi { const sessionManager = new StaticAuthSessionManager({ connector, defaultScopes: new Set(['user']), - sessionScopes: session => session.scopes, + sessionScopes: (session: GithubSession) => session.providerInfo.scopes, }); return new GithubAuth(sessionManager); @@ -93,7 +103,7 @@ class GithubAuth implements OAuthApi, SessionStateApi { constructor(private readonly sessionManager: SessionManager) {} - async getAccessToken(scope?: string, options?: AccessTokenOptions) { + async getAccessToken(scope?: string, options?: AuthRequestOptions) { const normalizedScopes = GithubAuth.normalizeScope(scope); const session = await this.sessionManager.getSession({ ...options, @@ -101,11 +111,25 @@ class GithubAuth implements OAuthApi, SessionStateApi { }); this.sessionStateTracker.setIsSignedId(!!session); if (session) { - return session.accessToken; + return session.providerInfo.accessToken; } return ''; } + async getBackstageIdentity( + options: AuthRequestOptions = {}, + ): Promise { + const session = await this.sessionManager.getSession(options); + this.sessionStateTracker.setIsSignedId(!!session); + return session?.backstageIdentity; + } + + async getProfile(options: AuthRequestOptions = {}) { + const session = await this.sessionManager.getSession(options); + this.sessionStateTracker.setIsSignedId(!!session); + return session?.profile; + } + async logout() { await this.sessionManager.removeSession(); this.sessionStateTracker.setIsSignedId(false); diff --git a/packages/core-api/src/apis/implementations/auth/github/types.ts b/packages/core-api/src/apis/implementations/auth/github/types.ts index 282017b80d..54c427423c 100644 --- a/packages/core-api/src/apis/implementations/auth/github/types.ts +++ b/packages/core-api/src/apis/implementations/auth/github/types.ts @@ -14,8 +14,15 @@ * limitations under the License. */ +import { ProfileInfo } from '../../..'; +import { BackstageIdentity } from '../../../definitions'; + export type GithubSession = { - accessToken: string; - scopes: Set; - expiresAt: Date; + providerInfo: { + accessToken: string; + scopes: Set; + expiresAt: Date; + }; + profile: ProfileInfo; + backstageIdentity: BackstageIdentity; }; diff --git a/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.test.ts b/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.test.ts index 1ee3575b89..5290ee798b 100644 --- a/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.test.ts +++ b/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.test.ts @@ -23,9 +23,9 @@ const PREFIX = 'https://www.googleapis.com/auth/'; describe('GoogleAuth', () => { it('should get refreshed access token', async () => { - const getSession = jest - .fn() - .mockResolvedValue({ accessToken: 'access-token', expiresAt: theFuture }); + const getSession = jest.fn().mockResolvedValue({ + providerInfo: { accessToken: 'access-token', expiresAt: theFuture }, + }); const googleAuth = new GoogleAuth({ getSession } as any); expect(await googleAuth.getAccessToken()).toBe('access-token'); @@ -33,9 +33,9 @@ describe('GoogleAuth', () => { }); it('should get refreshed id token', async () => { - const getSession = jest - .fn() - .mockResolvedValue({ idToken: 'id-token', expiresAt: theFuture }); + const getSession = jest.fn().mockResolvedValue({ + providerInfo: { idToken: 'id-token', expiresAt: theFuture }, + }); const googleAuth = new GoogleAuth({ getSession } as any); expect(await googleAuth.getIdToken()).toBe('id-token'); @@ -43,9 +43,9 @@ describe('GoogleAuth', () => { }); it('should get optional id token', async () => { - const getSession = jest - .fn() - .mockResolvedValue({ idToken: 'id-token', expiresAt: theFuture }); + const getSession = jest.fn().mockResolvedValue({ + providerInfo: { idToken: 'id-token', expiresAt: theFuture }, + }); const googleAuth = new GoogleAuth({ getSession } as any); expect(await googleAuth.getIdToken({ optional: true })).toBe('id-token'); @@ -58,9 +58,11 @@ describe('GoogleAuth', () => { const getSession = jest .fn() .mockResolvedValueOnce({ - accessToken: 'access-token', - expiresAt: theFuture, - scopes: new Set([`${PREFIX}not-enough`]), + providerInfo: { + accessToken: 'access-token', + expiresAt: theFuture, + scopes: new Set([`${PREFIX}not-enough`]), + }, }) .mockRejectedValue(error); const googleAuth = new GoogleAuth({ getSession } as any); @@ -77,17 +79,21 @@ describe('GoogleAuth', () => { it('should wait for all session refreshes', async () => { const initialSession = { - idToken: 'token1', - expiresAt: theFuture, - scopes: new Set(), + providerInfo: { + idToken: 'token1', + expiresAt: theFuture, + scopes: new Set(), + }, }; const getSession = jest .fn() .mockResolvedValueOnce(initialSession) .mockResolvedValue({ - idToken: 'token2', - expiresAt: theFuture, - scopes: new Set(), + providerInfo: { + idToken: 'token2', + expiresAt: theFuture, + scopes: new Set(), + }, }); const googleAuth = new GoogleAuth({ getSession } as any); @@ -95,7 +101,7 @@ describe('GoogleAuth', () => { await expect(googleAuth.getIdToken()).resolves.toBe('token1'); expect(getSession).toBeCalledTimes(1); - initialSession.expiresAt = thePast; + initialSession.providerInfo.expiresAt = thePast; const promise1 = googleAuth.getIdToken(); const promise2 = googleAuth.getIdToken(); diff --git a/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts b/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts index 183ea592b4..db26d2d9a4 100644 --- a/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts @@ -20,13 +20,13 @@ import { GoogleSession } from './types'; import { OAuthApi, OpenIdConnectApi, - IdTokenOptions, - AccessTokenOptions, ProfileInfoApi, - ProfileInfoOptions, ProfileInfo, SessionStateApi, SessionState, + BackstageIdentityApi, + AuthRequestOptions, + BackstageIdentity, } from '../../../definitions/auth'; import { OAuthRequestApi, AuthProvider } from '../../../definitions'; import { SessionManager } from '../../../../lib/AuthSessionManager/types'; @@ -46,11 +46,14 @@ type CreateOptions = { }; export type GoogleAuthResponse = { + providerInfo: { + accessToken: string; + idToken: string; + scope: string; + expiresInSeconds: number; + }; profile: ProfileInfo; - accessToken: string; - idToken: string; - scope: string; - expiresInSeconds: number; + backstageIdentity: BackstageIdentity; }; const DEFAULT_PROVIDER = { @@ -62,7 +65,12 @@ const DEFAULT_PROVIDER = { const SCOPE_PREFIX = 'https://www.googleapis.com/auth/'; class GoogleAuth - implements OAuthApi, OpenIdConnectApi, ProfileInfoApi, SessionStateApi { + implements + OAuthApi, + OpenIdConnectApi, + ProfileInfoApi, + BackstageIdentityApi, + SessionStateApi { static create({ apiOrigin, basePath, @@ -78,11 +86,15 @@ class GoogleAuth oauthRequestApi: oauthRequestApi, sessionTransform(res: GoogleAuthResponse): GoogleSession { return { - profile: res.profile, - idToken: res.idToken, - accessToken: res.accessToken, - scopes: GoogleAuth.normalizeScopes(res.scope), - expiresAt: new Date(Date.now() + res.expiresInSeconds * 1000), + ...res, + providerInfo: { + idToken: res.providerInfo.idToken, + accessToken: res.providerInfo.accessToken, + scopes: GoogleAuth.normalizeScopes(res.providerInfo.scope), + expiresAt: new Date( + Date.now() + res.providerInfo.expiresInSeconds * 1000, + ), + }, }; }, }); @@ -94,9 +106,10 @@ class GoogleAuth `${SCOPE_PREFIX}userinfo.email`, `${SCOPE_PREFIX}userinfo.profile`, ]), - sessionScopes: session => session.scopes, - sessionShouldRefresh: session => { - const expiresInSec = (session.expiresAt.getTime() - Date.now()) / 1000; + sessionScopes: (session: GoogleSession) => session.providerInfo.scopes, + sessionShouldRefresh: (session: GoogleSession) => { + const expiresInSec = + (session.providerInfo.expiresAt.getTime() - Date.now()) / 1000; return expiresInSec < 60 * 5; }, }); @@ -114,7 +127,7 @@ class GoogleAuth async getAccessToken( scope?: string | string[], - options?: AccessTokenOptions, + options?: AuthRequestOptions, ) { const normalizedScopes = GoogleAuth.normalizeScopes(scope); const session = await this.sessionManager.getSession({ @@ -123,16 +136,16 @@ class GoogleAuth }); this.sessionStateTracker.setIsSignedId(!!session); if (session) { - return session.accessToken; + return session.providerInfo.accessToken; } return ''; } - async getIdToken(options: IdTokenOptions = {}) { + async getIdToken(options: AuthRequestOptions = {}) { const session = await this.sessionManager.getSession(options); this.sessionStateTracker.setIsSignedId(!!session); if (session) { - return session.idToken; + return session.providerInfo.idToken; } return ''; } @@ -142,13 +155,18 @@ class GoogleAuth this.sessionStateTracker.setIsSignedId(false); } - async getProfile(options: ProfileInfoOptions = {}) { + async getBackstageIdentity( + options: AuthRequestOptions = {}, + ): Promise { const session = await this.sessionManager.getSession(options); this.sessionStateTracker.setIsSignedId(!!session); - if (!session) { - return undefined; - } - return session.profile; + return session?.backstageIdentity; + } + + async getProfile(options: AuthRequestOptions = {}) { + const session = await this.sessionManager.getSession(options); + this.sessionStateTracker.setIsSignedId(!!session); + return session?.profile; } static normalizeScopes(scopes?: string | string[]): Set { diff --git a/packages/core-api/src/apis/implementations/auth/google/types.ts b/packages/core-api/src/apis/implementations/auth/google/types.ts index ea251c7006..3b40932980 100644 --- a/packages/core-api/src/apis/implementations/auth/google/types.ts +++ b/packages/core-api/src/apis/implementations/auth/google/types.ts @@ -14,12 +14,15 @@ * limitations under the License. */ -import { ProfileInfo } from '../../../definitions'; +import { ProfileInfo, BackstageIdentity } from '../../../definitions'; export type GoogleSession = { + providerInfo: { + idToken: string; + accessToken: string; + scopes: Set; + expiresAt: Date; + }; profile: ProfileInfo; - idToken: string; - accessToken: string; - scopes: Set; - expiresAt: Date; + backstageIdentity: BackstageIdentity; }; diff --git a/packages/core-api/src/app/App.tsx b/packages/core-api/src/app/App.tsx index a20a9b27f8..dc2136a806 100644 --- a/packages/core-api/src/app/App.tsx +++ b/packages/core-api/src/app/App.tsx @@ -293,6 +293,10 @@ export class PrivateAppImpl implements BackstageApp { if (!SignInPageComponent) { this.identityApi.setSignInResult({ userId: 'guest', + profile: { + email: 'guest@example.com', + displayName: 'Guest', + }, }); return ( diff --git a/packages/core-api/src/app/AppIdentity.ts b/packages/core-api/src/app/AppIdentity.ts index c38ece53df..69ee5d28ac 100644 --- a/packages/core-api/src/app/AppIdentity.ts +++ b/packages/core-api/src/app/AppIdentity.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { IdentityApi } from '../apis'; +import { IdentityApi, ProfileInfo } from '../apis'; import { SignInResult } from './types'; /** @@ -24,6 +24,7 @@ import { SignInResult } from './types'; export class AppIdentity implements IdentityApi { private hasIdentity = false; private userId?: string; + private profile?: ProfileInfo; private idTokenFunc?: () => Promise; private logoutFunc?: () => Promise; @@ -36,6 +37,15 @@ export class AppIdentity implements IdentityApi { 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 { if (!this.hasIdentity) { throw new Error( @@ -55,6 +65,7 @@ export class AppIdentity implements IdentityApi { location.reload(); } + // This is indirectly called by the sign-in page to continue into the app. setSignInResult(result: SignInResult) { if (this.hasIdentity) { return; @@ -62,8 +73,12 @@ export class AppIdentity implements IdentityApi { 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.logoutFunc = result.logout; } diff --git a/packages/core-api/src/app/types.ts b/packages/core-api/src/app/types.ts index 33b884fd58..6b022c2736 100644 --- a/packages/core-api/src/app/types.ts +++ b/packages/core-api/src/app/types.ts @@ -18,7 +18,7 @@ import { ComponentType } from 'react'; import { IconComponent, SystemIconKey, SystemIcons } from '../icons'; import { BackstagePlugin } from '../plugin'; import { ApiHolder } from '../apis'; -import { AppTheme, ConfigApi } from '../apis/definitions'; +import { AppTheme, ConfigApi, ProfileInfo } from '../apis/definitions'; import { AppConfig } from '@backstage/config'; export type BootErrorPageProps = { @@ -31,6 +31,9 @@ export type SignInResult = { * User ID that will be returned by the IdentityApi */ userId: string; + + profile: ProfileInfo; + /** * Function used to retrieve an ID token for the signed in user. */ diff --git a/packages/core/src/layout/Sidebar/Settings/UserProfile.tsx b/packages/core/src/layout/Sidebar/Settings/UserProfile.tsx index 06c36bf9ce..101ae7010b 100644 --- a/packages/core/src/layout/Sidebar/Settings/UserProfile.tsx +++ b/packages/core/src/layout/Sidebar/Settings/UserProfile.tsx @@ -14,19 +14,12 @@ * limitations under the License. */ -import React, { FC, useState, useRef, useEffect } from 'react'; +import React, { FC, useRef } from 'react'; import { makeStyles, Avatar, Divider } from '@material-ui/core'; -import { - ProfileInfo, - useApi, - googleAuthApiRef, - Subscription, - SessionState, -} from '@backstage/core-api'; +import { useApi, identityApiRef } from '@backstage/core-api'; import { SidebarItem } from '../Items'; import ExpandLess from '@material-ui/icons/ExpandLess'; import ExpandMore from '@material-ui/icons/ExpandMore'; -import AccountCircleIcon from '@material-ui/icons/AccountCircle'; const useStyles = makeStyles({ avatar: { @@ -39,71 +32,26 @@ export const UserProfile: FC<{ open: boolean; setOpen: Function }> = ({ open, setOpen, }) => { - const [profile, setProfile] = useState(); const ref = useRef(); // for scrolling down when collapse item opens - const googleAuth = useApi(googleAuthApiRef); const classes = useStyles(); + const profile = useApi(identityApiRef).getProfile(); const handleClick = () => { setOpen(!open); setTimeout(() => ref.current?.scrollIntoView({ behavior: 'smooth' }), 300); }; - useEffect(() => { - const fetchProfile = async () => { - await googleAuth - .getProfile({ optional: true }) - .then((userProfile?: ProfileInfo) => { - setProfile(userProfile); - }); - }; - - let subscription: Subscription; - const observeSession = () => { - subscription = googleAuth - .sessionState$() - .subscribe(async (sessionState: SessionState) => { - if (sessionState === SessionState.SignedIn) { - await fetchProfile(); - } else { - setProfile(undefined); - } - }); - }; - - fetchProfile(); - observeSession(); - return () => { - subscription.unsubscribe(); - }; - }, [googleAuth]); - - // Handle main auth info that is shown on the collapsible SidebarItem - let avatar; - let displayName = 'Guest'; - if (profile) { - const email = profile.email; - const name = profile.name; - const imageUrl = profile.picture; - const emailTrimmed = email.split('@')[0]; - const displayEmail = - emailTrimmed.charAt(0).toUpperCase() + emailTrimmed.slice(1); - displayName = name ?? displayEmail; - avatar = imageUrl - ? () => ( - - ) - : () => ; - } + const displayName = profile.displayName ?? profile.email; + const SignInAvatar = () => ( + + {displayName[0]} + + ); return ( <> - + {open ? : } diff --git a/packages/core/src/layout/SignInPage/customProvider.tsx b/packages/core/src/layout/SignInPage/customProvider.tsx index d24b054b16..c6cb9b789d 100644 --- a/packages/core/src/layout/SignInPage/customProvider.tsx +++ b/packages/core/src/layout/SignInPage/customProvider.tsx @@ -56,6 +56,9 @@ const Component: ProviderComponent = ({ onResult }) => { const handleResult = ({ userId, idToken }: Data) => { onResult({ userId, + profile: { + email: `${userId}@example.com`, + }, getIdToken: idToken ? async () => idToken : undefined, }); }; diff --git a/packages/core/src/layout/SignInPage/googleProvider.tsx b/packages/core/src/layout/SignInPage/googleProvider.tsx index 9613437af8..632b81ad17 100644 --- a/packages/core/src/layout/SignInPage/googleProvider.tsx +++ b/packages/core/src/layout/SignInPage/googleProvider.tsx @@ -18,16 +18,7 @@ import React from 'react'; import { Grid, Typography, Button } from '@material-ui/core'; import { InfoCard } from '../InfoCard/InfoCard'; import { ProviderComponent, ProviderLoader, SignInProvider } from './types'; -import { - useApi, - googleAuthApiRef, - errorApiRef, - ProfileInfo, -} from '@backstage/core-api'; - -function parseUserId(profile: ProfileInfo) { - return profile!.email.replace(/@.*/, ''); -} +import { useApi, googleAuthApiRef, errorApiRef } from '@backstage/core-api'; const Component: ProviderComponent = ({ onResult }) => { const googleAuthApi = useApi(googleAuthApiRef); @@ -35,12 +26,17 @@ const Component: ProviderComponent = ({ onResult }) => { const handleLogin = async () => { try { - await googleAuthApi.getIdToken({ instantPopup: true }); + const identity = await googleAuthApi.getBackstageIdentity({ + instantPopup: true, + }); + const profile = await googleAuthApi.getProfile(); onResult({ - userId: parseUserId(profile!), - getIdToken: () => googleAuthApi.getIdToken(), + userId: identity!.id, + profile: profile!, + getIdToken: () => + googleAuthApi.getBackstageIdentity().then(i => i!.idToken), logout: async () => { await googleAuthApi.logout(); }, @@ -69,11 +65,21 @@ const Component: ProviderComponent = ({ onResult }) => { const loader: ProviderLoader = async apis => { const googleAuthApi = apis.get(googleAuthApiRef)!; - const profile = await googleAuthApi.getProfile({ optional: true }); + const identity = await googleAuthApi.getBackstageIdentity({ + optional: true, + }); + + if (!identity) { + return undefined; + } + + const profile = await googleAuthApi.getProfile(); return { - userId: parseUserId(profile!), - getIdToken: () => googleAuthApi.getIdToken(), + userId: identity.id, + profile: profile!, + getIdToken: () => + googleAuthApi.getBackstageIdentity().then(i => i!.idToken), logout: async () => { await googleAuthApi.logout(); }, diff --git a/packages/core/src/layout/SignInPage/guestProvider.tsx b/packages/core/src/layout/SignInPage/guestProvider.tsx index 78d0191ba9..85784eb205 100644 --- a/packages/core/src/layout/SignInPage/guestProvider.tsx +++ b/packages/core/src/layout/SignInPage/guestProvider.tsx @@ -19,6 +19,14 @@ import { Grid, Typography, Button } from '@material-ui/core'; import { InfoCard } from '../InfoCard/InfoCard'; import { ProviderComponent, ProviderLoader, SignInProvider } from './types'; +const result = { + userId: 'guest', + profile: { + email: 'guest@example.com', + displayName: 'Guest', + }, +}; + const Component: ProviderComponent = ({ onResult }) => ( ( @@ -45,7 +53,7 @@ const Component: ProviderComponent = ({ onResult }) => ( ); const loader: ProviderLoader = async () => { - return { userId: 'guest' }; + return result; }; export const guestProvider: SignInProvider = { Component, loader }; diff --git a/plugins/auth-backend/src/lib/OAuthProvider.test.ts b/plugins/auth-backend/src/lib/OAuthProvider.test.ts index d84226214d..f92875fd48 100644 --- a/plugins/auth-backend/src/lib/OAuthProvider.test.ts +++ b/plugins/auth-backend/src/lib/OAuthProvider.test.ts @@ -116,7 +116,10 @@ describe('OAuthProvider Utils', () => { profile: { email: 'foo@bar.com', }, - userIdToken: 'a.b.c', + backstageIdentity: { + id: 'a', + idToken: 'a.b.c', + }, }, }; const jsonData = JSON.stringify(data); @@ -346,7 +349,10 @@ describe('OAuthProvider', () => { expect(mockResponse.send).toHaveBeenCalledTimes(1); expect(mockResponse.send).toHaveBeenCalledWith({ ...mockResponseData, - userIdToken: 'my-id-token', + backstageIdentity: { + id: mockResponseData.profile.email, + idToken: 'my-id-token', + }, }); }); diff --git a/plugins/auth-backend/src/lib/OAuthProvider.ts b/plugins/auth-backend/src/lib/OAuthProvider.ts index 1c88dae693..dac31af451 100644 --- a/plugins/auth-backend/src/lib/OAuthProvider.ts +++ b/plugins/auth-backend/src/lib/OAuthProvider.ts @@ -147,12 +147,13 @@ export class OAuthProvider implements AuthProviderRouteHandlers { this.setRefreshTokenCookie(res, refreshToken); } - const userIdToken = await this.options.tokenIssuer.issueToken({ - claims: { sub: response.profile.email }, + const id = response.profile.email; + const idToken = await this.options.tokenIssuer.issueToken({ + claims: { sub: id }, }); const fullResponse: AuthResponse = { ...response, - userIdToken, + backstageIdentity: { id, idToken }, }; // post message back to popup if successful @@ -212,12 +213,13 @@ export class OAuthProvider implements AuthProviderRouteHandlers { // get new access_token const response = await this.providerHandlers.refresh(refreshToken, scope); - const userIdToken = await this.options.tokenIssuer.issueToken({ - claims: { sub: response.profile.email }, + const id = response.profile.email; + const idToken = await this.options.tokenIssuer.issueToken({ + claims: { sub: id }, }); const fullResponse: AuthResponse = { ...response, - userIdToken, + backstageIdentity: { id, idToken }, }; res.send(fullResponse); diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index 867bd280a2..68b84c7e33 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -84,8 +84,9 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { response: { userId, profile }, } = await executeFrameHandlerStrategy(req, this.strategy); - const userIdToken = await this.tokenIssuer.issueToken({ - claims: { sub: userId }, + const id = userId; + const idToken = await this.tokenIssuer.issueToken({ + claims: { sub: id }, }); return postMessageResponse(res, 'http://localhost:3000', { @@ -93,7 +94,7 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { response: { providerInfo: {}, profile, - userIdToken, + backstageIdentity: { id, idToken }, }, }); } catch (error) { diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 2101fc19bb..87ea88230c 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -192,14 +192,26 @@ export type AuthProviderFactory = ( export type AuthResponse = { providerInfo: ProviderInfo; profile: ProfileInfo; - userIdToken: string; + backstageIdentity: BackstageIdentity; }; export type OAuthResponse = Omit< AuthResponse, - 'userIdToken' + 'backstageIdentity' >; +export type BackstageIdentity = { + /** + * The backstage user ID. + */ + id: string; + + /** + * An ID token that can be used to authenticate the user within Backstage. + */ + idToken: string; +}; + export type OAuthProviderInfo = { /** * An access token issued for the signed in user. diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 68b2d31032..28718e85a1 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -67,12 +67,6 @@ export async function createRouter( clientId: process.env.AUTH_GOOGLE_CLIENT_ID!, clientSecret: process.env.AUTH_GOOGLE_CLIENT_SECRET!, }, - production: { - appOrigin: 'http://localhost:3000', - secure: false, - clientId: '', - clientSecret: '', - }, }, github: { development: {