diff --git a/.changeset/brave-pandas-beam.md b/.changeset/brave-pandas-beam.md index 3defff8aa6..8dd525d749 100644 --- a/.changeset/brave-pandas-beam.md +++ b/.changeset/brave-pandas-beam.md @@ -1,5 +1,17 @@ --- -'@backstage/core-app-api': patch +'@backstage/core-app-api': minor --- -custom AuthConnector for OAuth2 +Support custom `AuthConnector` for `OAuth2`. + +A user can pass their own `AuthConnector` implementation in `OAuth2` constructor. +In which case the session manager will use that instead of the `DefaultAuthConnector` to interact with the +authentication provider. + +A custom `AuthConnector` may call the authentication provider from the front-end, store and retrieve tokens +in the session storage, for example, and otherwise send custom requests to the authentication provider and +handle its responses. + +Note, that if the custom `AuthConnector` transforms scopes returned from the authentication provider, +the transformation must be the same as `OAuth2CreateOptions#scopeTransform` passed to `OAuth2` constructor. +See creating `DefaultAuthConnector` in `OAuth2#create(...)` for an example. diff --git a/packages/core-app-api/report.api.md b/packages/core-app-api/report.api.md index 73d940d1c4..ccec5ca4ce 100644 --- a/packages/core-app-api/report.api.md +++ b/packages/core-app-api/report.api.md @@ -296,11 +296,26 @@ export type AuthApiCreateOptions = { // @public export type AuthConnector = { - createSession(options: CreateSessionOptions): Promise; - refreshSession(scopes?: Set): Promise; + createSession( + options: AuthConnectorCreateSessionOptions, + ): Promise; + refreshSession( + options?: AuthConnectorRefreshSessionOptions, + ): Promise; removeSession(): Promise; }; +// @public (undocumented) +export type AuthConnectorCreateSessionOptions = { + scopes: Set; + instantPopup?: boolean; +}; + +// @public (undocumented) +export type AuthConnectorRefreshSessionOptions = { + scopes: Set; +}; + // @public export type BackstageApp = { getPlugins(): BackstagePlugin[]; @@ -360,12 +375,6 @@ export function createFetchApi(options: { middleware?: FetchMiddleware | FetchMiddleware[] | undefined; }): FetchApi; -// @public (undocumented) -export type CreateSessionOptions = { - scopes: Set; - instantPopup?: boolean; -}; - // @public export function createSpecializedApp(options: AppOptions): BackstageApp; @@ -491,15 +500,6 @@ export class LocalStorageFeatureFlags implements FeatureFlagsApi { save(options: FeatureFlagsSaveOptions): void; } -// @public -export type LoginPopupOptions = { - url: string; - name: string; - origin: string; - width?: number; - height?: number; -}; - // @public export class MicrosoftAuth { // (undocumented) @@ -573,9 +573,7 @@ export class OAuth2 export type OAuth2CreateOptions = OAuthApiCreateOptions & { scopeTransform?: (scopes: string[]) => string[]; popupOptions?: PopupOptions; - authConnectorFactory?: ( - opts: OAuth2CreateOptions, - ) => AuthConnector; + authConnector?: AuthConnector; }; // @public @@ -627,6 +625,19 @@ export type OneLoginAuthCreateOptions = { provider?: AuthProviderInfo; }; +// @public +export function openLoginPopup( + options: OpenLoginPopupOptions, +): Promise; + +// @public +export type OpenLoginPopupOptions = { + url: string; + name: string; + width?: number; + height?: number; +}; + // @public export type PopupOptions = { size?: @@ -662,9 +673,6 @@ export class SamlAuth signOut(): Promise; } -// @public -export function showLoginPopup(options: LoginPopupOptions): Promise; - // @public export type SignInPageProps = PropsWithChildren<{ onSignInSuccess(identityApi: IdentityApi): void; diff --git a/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts b/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts index 9162896ebe..fb3aead36c 100644 --- a/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts +++ b/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts @@ -44,9 +44,7 @@ import { OAuthApiCreateOptions } from '../types'; export type OAuth2CreateOptions = OAuthApiCreateOptions & { scopeTransform?: (scopes: string[]) => string[]; popupOptions?: PopupOptions; - authConnectorFactory?: ( - opts: OAuth2CreateOptions, - ) => AuthConnector; + authConnector?: AuthConnector; }; export type OAuth2Response = { @@ -83,72 +81,61 @@ export default class OAuth2 BackstageIdentityApi, SessionApi { - private static createDefaultAuthConnector(options: OAuth2CreateOptions) { - const { - configApi, - discoveryApi, - environment, - provider, - oauthRequestApi, - scopeTransform, - popupOptions, - } = options; - - return new DefaultAuthConnector({ - configApi, - discoveryApi, - environment: environment!, - provider: provider!, - oauthRequestApi: oauthRequestApi, - sessionTransform({ - backstageIdentity, - ...res - }: OAuth2Response): OAuth2Session { - const session: OAuth2Session = { - ...res, - providerInfo: { - idToken: res.providerInfo.idToken, - accessToken: res.providerInfo.accessToken, - scopes: OAuth2.normalizeScopes( - scopeTransform!, - res.providerInfo.scope, - ), - expiresAt: res.providerInfo.expiresInSeconds - ? new Date(Date.now() + res.providerInfo.expiresInSeconds * 1000) - : undefined, - }, - }; - if (backstageIdentity) { - session.backstageIdentity = { - token: backstageIdentity.token, - identity: backstageIdentity.identity, - expiresAt: backstageIdentity.expiresInSeconds - ? new Date(Date.now() + backstageIdentity.expiresInSeconds * 1000) - : undefined, - }; - } - return session; - }, - popupOptions, - }); - } - static create(options: OAuth2CreateOptions) { const { + configApi, + discoveryApi, environment = 'development', provider = DEFAULT_PROVIDER, + oauthRequestApi, defaultScopes = [], scopeTransform = x => x, - authConnectorFactory = (opts: OAuth2CreateOptions) => - OAuth2.createDefaultAuthConnector(opts), + popupOptions, } = options; - const connector = authConnectorFactory({ - ...options, - scopeTransform, - environment, - provider, - }); + const connector = + options.authConnector ?? + new DefaultAuthConnector({ + configApi, + discoveryApi, + environment, + provider, + oauthRequestApi: oauthRequestApi, + sessionTransform({ + backstageIdentity, + ...res + }: OAuth2Response): OAuth2Session { + const session: OAuth2Session = { + ...res, + providerInfo: { + idToken: res.providerInfo.idToken, + accessToken: res.providerInfo.accessToken, + scopes: OAuth2.normalizeScopes( + scopeTransform, + res.providerInfo.scope, + ), + expiresAt: res.providerInfo.expiresInSeconds + ? new Date( + Date.now() + res.providerInfo.expiresInSeconds * 1000, + ) + : undefined, + }, + }; + if (backstageIdentity) { + session.backstageIdentity = { + token: backstageIdentity.token, + identity: backstageIdentity.identity, + expiresAt: backstageIdentity.expiresInSeconds + ? new Date( + Date.now() + backstageIdentity.expiresInSeconds * 1000, + ) + : undefined, + }; + } + return session; + }, + popupOptions, + }); const sessionManager = new RefreshingAuthSessionManager({ connector, diff --git a/packages/core-app-api/src/index.ts b/packages/core-app-api/src/index.ts index 9033622b9d..1dc8ddec19 100644 --- a/packages/core-app-api/src/index.ts +++ b/packages/core-app-api/src/index.ts @@ -23,7 +23,10 @@ export * from './apis'; export * from './app'; export * from './routing'; -export type { AuthConnector } from './lib'; -export type { CreateSessionOptions } from './lib'; -export { showLoginPopup } from './lib'; -export type { LoginPopupOptions } from './lib'; +export type { + AuthConnector, + AuthConnectorCreateSessionOptions, + AuthConnectorRefreshSessionOptions, + openLoginPopup, + OpenLoginPopupOptions, +} from './lib'; diff --git a/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts b/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts index 60c4a3b8d9..192b1b7ac0 100644 --- a/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts +++ b/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts @@ -26,7 +26,7 @@ import { ConfigApi } from '@backstage/core-plugin-api'; jest.mock('../loginPopup', () => { return { - showLoginPopup: jest.fn(), + openLoginPopup: jest.fn(), }; }); @@ -74,7 +74,9 @@ describe('DefaultAuthConnector', () => { ); const connector = new DefaultAuthConnector(defaultOptions); - const session = await connector.refreshSession(new Set(['a', 'b', 'c'])); + const session = await connector.refreshSession({ + scopes: new Set(['a', 'b', 'c']), + }); expect(session.idToken).toBe('mock-id-token'); expect(session.accessToken).toBe('mock-access-token'); expect(session.scopes).toEqual(new Set(['a', 'b', 'c'])); @@ -118,7 +120,7 @@ describe('DefaultAuthConnector', () => { it('should create a session', async () => { const mockOauth = new MockOAuthApi(); const popupSpy = jest - .spyOn(loginPopup, 'showLoginPopup') + .spyOn(loginPopup, 'openLoginPopup') .mockResolvedValue({ idToken: 'my-id-token', accessToken: 'my-access-token', @@ -151,7 +153,7 @@ describe('DefaultAuthConnector', () => { it('should instantly show popup if option is set', async () => { const popupSpy = jest - .spyOn(loginPopup, 'showLoginPopup') + .spyOn(loginPopup, 'openLoginPopup') .mockResolvedValue('my-session'); const connector = new DefaultAuthConnector({ ...defaultOptions, @@ -169,7 +171,6 @@ describe('DefaultAuthConnector', () => { expect(popupSpy).toHaveBeenCalledTimes(1); expect(popupSpy).toHaveBeenCalledWith({ name: 'My Provider Login', - origin: 'http://my-host', url: 'http://my-host/api/auth/my-provider/start?scope=&origin=http%3A%2F%2Flocalhost&flow=popup&env=production', width: 450, height: 730, @@ -178,7 +179,7 @@ describe('DefaultAuthConnector', () => { it('should show popup fullscreen', async () => { const popupSpy = jest - .spyOn(loginPopup, 'showLoginPopup') + .spyOn(loginPopup, 'openLoginPopup') .mockResolvedValue('my-session'); jest.spyOn(window.screen, 'width', 'get').mockReturnValue(1000); @@ -205,7 +206,6 @@ describe('DefaultAuthConnector', () => { expect(popupSpy).toHaveBeenCalledWith({ height: 1000, name: 'My Provider Login', - origin: 'http://my-host', url: 'http://my-host/api/auth/my-provider/start?scope=&origin=http%3A%2F%2Flocalhost&flow=popup&env=production', width: 1000, }); @@ -213,7 +213,7 @@ describe('DefaultAuthConnector', () => { it('should show popup with special width and height', async () => { const popupSpy = jest - .spyOn(loginPopup, 'showLoginPopup') + .spyOn(loginPopup, 'openLoginPopup') .mockResolvedValue('my-session'); const connector = new DefaultAuthConnector({ ...defaultOptions, @@ -236,7 +236,6 @@ describe('DefaultAuthConnector', () => { expect(popupSpy).toHaveBeenCalledWith({ name: 'My Provider Login', - origin: 'http://my-host', url: 'http://my-host/api/auth/my-provider/start?scope=&origin=http%3A%2F%2Flocalhost&flow=popup&env=production', width: 500, height: 1000, @@ -246,7 +245,7 @@ describe('DefaultAuthConnector', () => { it('should use join func to join scopes', async () => { const mockOauth = new MockOAuthApi(); const popupSpy = jest - .spyOn(loginPopup, 'showLoginPopup') + .spyOn(loginPopup, 'openLoginPopup') .mockResolvedValue({ scopes: '' }); const connector = new DefaultAuthConnector({ ...defaultOptions, diff --git a/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.ts b/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.ts index ad1b25619d..3671ddc6e6 100644 --- a/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.ts +++ b/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.ts @@ -20,8 +20,13 @@ import { OAuthRequestApi, OAuthRequester, } from '@backstage/core-plugin-api'; -import { showLoginPopup } from '../loginPopup'; -import { AuthConnector, CreateSessionOptions, PopupOptions } from './types'; +import { openLoginPopup } from '../loginPopup'; +import { + AuthConnector, + AuthConnectorCreateSessionOptions, + PopupOptions, + AuthConnectorRefreshSessionOptions, +} from './types'; let warned = false; @@ -123,7 +128,9 @@ export class DefaultAuthConnector this.popupOptions = popupOptions; } - async createSession(options: CreateSessionOptions): Promise { + async createSession( + options: AuthConnectorCreateSessionOptions, + ): Promise { if (options.instantPopup) { if (this.enableExperimentalRedirectFlow) { return this.executeRedirect(options.scopes); @@ -133,11 +140,13 @@ export class DefaultAuthConnector return this.authRequester(options.scopes); } - async refreshSession(scopes?: Set): Promise { + async refreshSession( + options?: AuthConnectorRefreshSessionOptions, + ): Promise { const res = await fetch( await this.buildUrl('/refresh', { optional: true, - ...(scopes && { scope: this.joinScopesFunc(scopes) }), + ...(options && { scope: this.joinScopesFunc(options.scopes) }), }), { headers: { @@ -203,10 +212,9 @@ export class DefaultAuthConnector ? window.screen.height : this.popupOptions?.size?.height || 730; - const payload = await showLoginPopup({ + const payload = await openLoginPopup({ url: popupUrl, name: `${this.provider.title} Login`, - origin: new URL(popupUrl).origin, width, height, }); diff --git a/packages/core-app-api/src/lib/AuthConnector/DirectAuthConnector.ts b/packages/core-app-api/src/lib/AuthConnector/DirectAuthConnector.ts index 200ba755ac..a45bfa0af9 100644 --- a/packages/core-app-api/src/lib/AuthConnector/DirectAuthConnector.ts +++ b/packages/core-app-api/src/lib/AuthConnector/DirectAuthConnector.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { AuthProviderInfo, DiscoveryApi } from '@backstage/core-plugin-api'; -import { showLoginPopup } from '../loginPopup'; +import { openLoginPopup } from '../loginPopup'; type Options = { discoveryApi: DiscoveryApi; @@ -36,13 +36,12 @@ export class DirectAuthConnector { async createSession(): Promise { const popupUrl = await this.buildUrl('/start'); - const payload = await showLoginPopup({ + const payload = (await openLoginPopup({ url: popupUrl, name: `${this.provider.title} Login`, - origin: new URL(popupUrl).origin, width: 450, height: 730, - }); + })) as any; return { ...payload, diff --git a/packages/core-app-api/src/lib/AuthConnector/types.ts b/packages/core-app-api/src/lib/AuthConnector/types.ts index 1ba96c6cb8..035760d0aa 100644 --- a/packages/core-app-api/src/lib/AuthConnector/types.ts +++ b/packages/core-app-api/src/lib/AuthConnector/types.ts @@ -17,11 +17,18 @@ /** * @public */ -export type CreateSessionOptions = { +export type AuthConnectorCreateSessionOptions = { scopes: Set; instantPopup?: boolean; }; +/** + * @public + */ +export type AuthConnectorRefreshSessionOptions = { + scopes: Set; +}; + /** * An AuthConnector is responsible for realizing auth session actions * by for example communicating with a backend or interacting with the user. @@ -29,8 +36,12 @@ export type CreateSessionOptions = { * @public */ export type AuthConnector = { - createSession(options: CreateSessionOptions): Promise; - refreshSession(scopes?: Set): Promise; + createSession( + options: AuthConnectorCreateSessionOptions, + ): Promise; + refreshSession( + options?: AuthConnectorRefreshSessionOptions, + ): Promise; removeSession(): Promise; }; diff --git a/packages/core-app-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts b/packages/core-app-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts index 88000638e6..9afc710bdd 100644 --- a/packages/core-app-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts +++ b/packages/core-app-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts @@ -16,6 +16,7 @@ import { RefreshingAuthSessionManager } from './RefreshingAuthSessionManager'; import { SessionState } from '@backstage/core-plugin-api'; +import { AuthConnectorRefreshSessionOptions } from '../AuthConnector'; const defaultOptions = { sessionScopes: (session: { scopes: Set }) => session.scopes, @@ -47,7 +48,7 @@ describe('RefreshingAuthSessionManager', () => { await manager.getSession({}); expect(createSession).toHaveBeenCalledTimes(1); - expect(refreshSession).toHaveBeenCalledWith(new Set()); + expect(refreshSession).toHaveBeenCalledWith({ scopes: new Set() }); expect(stateSubscriber.mock.calls).toEqual([ [SessionState.SignedOut], [SessionState.SignedIn], @@ -103,7 +104,7 @@ describe('RefreshingAuthSessionManager', () => { await manager.getSession({ scopes: new Set(['a']) }); expect(createSession).toHaveBeenCalledTimes(1); - expect(refreshSession).toHaveBeenCalledWith(new Set(['a'])); + expect(refreshSession).toHaveBeenCalledWith({ scopes: new Set(['a']) }); await manager.getSession({ scopes: new Set(['a']) }); expect(createSession).toHaveBeenCalledTimes(1); @@ -134,7 +135,7 @@ describe('RefreshingAuthSessionManager', () => { expect(await manager.getSession({ optional: true })).toBe(undefined); expect(createSession).toHaveBeenCalledTimes(0); - expect(refreshSession).toHaveBeenCalledWith(new Set()); + expect(refreshSession).toHaveBeenCalledWith({ scopes: new Set() }); }); it('should forward option to instantly show auth popup and not attempt refresh', async () => { @@ -168,10 +169,12 @@ describe('RefreshingAuthSessionManager', () => { it('should handle two simultaneous session refreshes with same scopes', async () => { const createSession = jest.fn(); - const refreshSession = jest.fn(async (scopes?: Set) => ({ - scopes: scopes ?? new Set(), - expired: false, - })); + const refreshSession = jest.fn( + async (options?: AuthConnectorRefreshSessionOptions) => ({ + scopes: options?.scopes ?? new Set(), + expired: false, + }), + ); const manager = new RefreshingAuthSessionManager({ connector: { createSession, refreshSession }, ...defaultOptions, @@ -192,10 +195,12 @@ describe('RefreshingAuthSessionManager', () => { it('should handle two simultaneous session refreshes with different scopes', async () => { const createSession = jest.fn(); - const refreshSession = jest.fn(async (scopes?: Set) => ({ - scopes: scopes ?? new Set(), - expired: false, - })); + const refreshSession = jest.fn( + async (options?: AuthConnectorRefreshSessionOptions) => ({ + scopes: options?.scopes ?? new Set(), + expired: false, + }), + ); const manager = new RefreshingAuthSessionManager({ connector: { createSession, refreshSession }, ...defaultOptions, @@ -216,10 +221,12 @@ describe('RefreshingAuthSessionManager', () => { it('should handle multiple simultaneous session refreshes with different scopes', async () => { const createSession = jest.fn(); - const refreshSession = jest.fn(async (scopes?: Set) => ({ - scopes: scopes ?? new Set(), - expired: false, - })); + const refreshSession = jest.fn( + async (options?: AuthConnectorRefreshSessionOptions) => ({ + scopes: options?.scopes ?? new Set(), + expired: false, + }), + ); const manager = new RefreshingAuthSessionManager({ connector: { createSession, refreshSession }, ...defaultOptions, diff --git a/packages/core-app-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts b/packages/core-app-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts index 414743ba9d..216409a528 100644 --- a/packages/core-app-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts +++ b/packages/core-app-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts @@ -137,9 +137,9 @@ export class RefreshingAuthSessionManager implements SessionManager { return this.refreshPromise; } - this.refreshPromise = this.connector.refreshSession( - this.helper.getExtendedScope(this.currentSession, scopes), - ); + this.refreshPromise = this.connector.refreshSession({ + scopes: this.helper.getExtendedScope(this.currentSession, scopes), + }); try { const session = await this.refreshPromise; diff --git a/packages/core-app-api/src/lib/loginPopup.test.ts b/packages/core-app-api/src/lib/loginPopup.test.ts index 50193cbdd6..448f4f727c 100644 --- a/packages/core-app-api/src/lib/loginPopup.test.ts +++ b/packages/core-app-api/src/lib/loginPopup.test.ts @@ -14,14 +14,14 @@ * limitations under the License. */ -import { showLoginPopup } from './loginPopup'; +import { openLoginPopup } from './loginPopup'; -describe('showLoginPopup', () => { +describe('openLoginPopup', () => { afterEach(() => { jest.resetAllMocks(); }); - it('should show an auth popup', async () => { + it('should open an auth popup', async () => { const popupMock = { closed: false }; const openSpy = jest .spyOn(window, 'open') @@ -29,15 +29,14 @@ describe('showLoginPopup', () => { const addEventListenerSpy = jest.spyOn(window, 'addEventListener'); const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener'); - const payloadPromise = showLoginPopup({ - url: 'my-origin/api/backend/auth/start?scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fa%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fb', + const payloadPromise = openLoginPopup({ + url: 'http://my-origin/api/backend/auth/start?scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fa%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fb', name: 'test-popup', - origin: 'my-origin', }); expect(openSpy).toHaveBeenCalledTimes(1); expect(openSpy.mock.calls[0][0]).toBe( - 'my-origin/api/backend/auth/start?scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fa%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fb', + 'http://my-origin/api/backend/auth/start?scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fa%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fb', ); expect(openSpy.mock.calls[0][1]).toBe('test-popup'); expect(addEventListenerSpy).toHaveBeenCalledTimes(1); @@ -57,16 +56,16 @@ describe('showLoginPopup', () => { // None of these should be accepted listener({ source: popupMock } as MessageEvent); - listener({ origin: 'my-origin' } as MessageEvent); + listener({ origin: 'http://my-origin' } as MessageEvent); listener({ data: { type: 'authorization_response' } } as MessageEvent); listener({ source: popupMock, - origin: 'my-origin', + origin: 'http://my-origin', data: {}, } as MessageEvent); listener({ source: popupMock, - origin: 'my-origin', + origin: 'http://my-origin', data: { type: 'not-auth-result', response: {} }, } as MessageEvent); @@ -79,7 +78,7 @@ describe('showLoginPopup', () => { // This should be accepted as a valid sessions response listener({ source: popupMock, - origin: 'my-origin', + origin: 'http://my-origin', data: { type: 'authorization_response', response: myResponse, @@ -101,10 +100,9 @@ describe('showLoginPopup', () => { const addEventListenerSpy = jest.spyOn(window, 'addEventListener'); const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener'); - const payloadPromise = showLoginPopup({ - url: 'url', + const payloadPromise = openLoginPopup({ + url: 'http://my-origin', name: 'name', - origin: 'my-origin', }); expect(openSpy).toHaveBeenCalledTimes(1); @@ -115,7 +113,7 @@ describe('showLoginPopup', () => { listener({ source: popupMock, - origin: 'my-origin', + origin: 'http://my-origin', data: { type: 'authorization_response', error: { @@ -145,10 +143,9 @@ describe('showLoginPopup', () => { openSpy.mockReturnValue(popupMock as Window); - const payloadPromise = showLoginPopup({ - url: 'url', + const payloadPromise = openLoginPopup({ + url: 'http://origin', name: 'name', - origin: 'origin', }); expect(openSpy).toHaveBeenCalledTimes(1); @@ -158,7 +155,7 @@ describe('showLoginPopup', () => { const listener = addEventListenerSpy.mock.calls[0][1] as EventListener; listener({ source: popupMock, - origin: 'origin', + origin: 'http://origin', data: { type: 'config_info', targetOrigin: 'http://localhost', @@ -187,16 +184,15 @@ describe('showLoginPopup', () => { openSpy.mockReturnValue(popupMock as Window); - const payloadPromise = showLoginPopup({ - url: 'url', + const payloadPromise = openLoginPopup({ + url: 'http://origin', name: 'name', - origin: 'origin', }); const listener = addEventListenerSpy.mock.calls[0][1] as EventListener; listener({ source: popupMock, - origin: 'origin', + origin: 'http://origin', data: { type: 'config_info', targetOrigin: 'http://differenthost', diff --git a/packages/core-app-api/src/lib/loginPopup.ts b/packages/core-app-api/src/lib/loginPopup.ts index 9ab75501b0..e7ed84b0cf 100644 --- a/packages/core-app-api/src/lib/loginPopup.ts +++ b/packages/core-app-api/src/lib/loginPopup.ts @@ -19,7 +19,7 @@ * * @public */ -export type LoginPopupOptions = { +export type OpenLoginPopupOptions = { /** * The URL that the auth popup should point to */ @@ -30,11 +30,6 @@ export type LoginPopupOptions = { */ name: string; - /** - * The origin of the final popup page that will post a message to this window. - */ - origin: string; - /** * The width of the popup in pixels, defaults to 500 */ @@ -70,13 +65,17 @@ type AuthResult = * * @public */ -export function showLoginPopup(options: LoginPopupOptions): Promise { +export function openLoginPopup( + options: OpenLoginPopupOptions, +): Promise { return new Promise((resolve, reject) => { const width = options.width || 500; const height = options.height || 700; const left = window.screen.width / 2 - width / 2; const top = window.screen.height / 2 - height / 2; + const origin = new URL(options.url).origin; + const popup = window.open( options.url, options.name, @@ -96,7 +95,7 @@ export function showLoginPopup(options: LoginPopupOptions): Promise { if (event.source !== popup) { return; } - if (event.origin !== options.origin) { + if (event.origin !== origin) { return; } const { data } = event;