diff --git a/packages/core-api/src/apis/definitions/auth.ts b/packages/core-api/src/apis/definitions/auth.ts index 15c9e5d632..c066c83b73 100644 --- a/packages/core-api/src/apis/definitions/auth.ts +++ b/packages/core-api/src/apis/definitions/auth.ts @@ -85,7 +85,10 @@ export type OAuthApi = { * will be prompted 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. */ - getAccessToken(scope?: OAuthScope): Promise; + getAccessToken( + scope?: OAuthScope, + options?: AccessTokenOptions, + ): Promise; /** * Log out the user's session. This will reload the page. 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 e52e8f0397..582afbcfea 100644 --- a/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts @@ -21,6 +21,7 @@ import { OAuthApi, OpenIdConnectApi, IdTokenOptions, + AccessTokenOptions, } from '../../../definitions/auth'; import { OAuthRequestApi, AuthProvider } from '../../../definitions'; import { SessionManager } from '../../../../lib/AuthSessionManager/types'; @@ -95,19 +96,23 @@ class GoogleAuth implements OAuthApi, OpenIdConnectApi { constructor(private readonly sessionManager: SessionManager) {} - async getAccessToken(scope?: string | string[]) { + async getAccessToken( + scope?: string | string[], + options?: AccessTokenOptions, + ) { const normalizedScopes = GoogleAuth.normalizeScopes(scope); const session = await this.sessionManager.getSession({ - optional: false, + ...options, scopes: normalizedScopes, }); - return session.accessToken; + if (session) { + return session.accessToken; + } + return ''; } - async getIdToken({ optional }: IdTokenOptions = {}) { - const session = await this.sessionManager.getSession({ - optional: optional || false, - }); + async getIdToken(options: IdTokenOptions = {}) { + const session = await this.sessionManager.getSession(options); if (session) { return session.idToken; } diff --git a/packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts b/packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts index 9dde49aab8..3d8f7abeb9 100644 --- a/packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts +++ b/packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts @@ -86,7 +86,7 @@ describe('DefaultAuthConnector', () => { ...defaultOptions, oauthRequestApi: mockOauth, }); - const promise = helper.createSession(new Set(['a', 'b'])); + const promise = helper.createSession({ scopes: new Set(['a', 'b']) }); await mockOauth.rejectAll(); await expect(promise).rejects.toMatchObject({ name: 'RejectedError' }); }); @@ -106,7 +106,9 @@ describe('DefaultAuthConnector', () => { oauthRequestApi: mockOauth, }); - const sessionPromise = helper.createSession(new Set(['a', 'b'])); + const sessionPromise = helper.createSession({ + scopes: new Set(['a', 'b']), + }); await mockOauth.triggerAll(); @@ -123,6 +125,26 @@ describe('DefaultAuthConnector', () => { }); }); + it('should instantly show popup if option is set', async () => { + const popupSpy = jest + .spyOn(loginPopup, 'showLoginPopup') + .mockResolvedValue('my-session'); + const helper = new DefaultAuthConnector({ + ...defaultOptions, + oauthRequestApi: new MockOAuthApi(), + sessionTransform: str => str, + }); + + const sessionPromise = helper.createSession({ + scopes: new Set(), + instantPopup: true, + }); + + expect(popupSpy).toBeCalledTimes(1); + + await expect(sessionPromise).resolves.toBe('my-session'); + }); + it('should use join func to join scopes', async () => { const mockOauth = new MockOAuthApi(); const popupSpy = jest @@ -134,7 +156,7 @@ describe('DefaultAuthConnector', () => { oauthRequestApi: mockOauth, }); - helper.createSession(new Set(['a', 'b'])); + helper.createSession({ scopes: new Set(['a', 'b']) }); await mockOauth.triggerAll(); diff --git a/packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.ts b/packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.ts index 43bca21e90..21d2530c5a 100644 --- a/packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.ts +++ b/packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.ts @@ -17,7 +17,7 @@ import { AuthRequester } from '../../apis'; import { OAuthRequestApi, AuthProvider } from '../../apis/definitions'; import { showLoginPopup } from '../loginPopup'; -import { AuthConnector } from './types'; +import { AuthConnector, CreateSessionOptions } from './types'; const DEFAULT_BASE_PATH = '/api/auth/'; @@ -68,6 +68,7 @@ export class DefaultAuthConnector private readonly basePath: string; private readonly environment: string; private readonly provider: AuthProvider & { id: string }; + private readonly joinScopesFunc: (scopes: Set) => string; private readonly authRequester: AuthRequester; private readonly sessionTransform: (response: any) => Promise; @@ -84,18 +85,22 @@ export class DefaultAuthConnector this.authRequester = oauthRequestApi.createAuthRequester({ provider, - onAuthRequest: scopes => this.showPopup(joinScopes(scopes)), + onAuthRequest: scopes => this.showPopup(scopes), }); this.apiOrigin = apiOrigin; this.basePath = basePath; this.environment = environment; this.provider = provider; + this.joinScopesFunc = joinScopes; this.sessionTransform = sessionTransform; } - async createSession(scopes: Set): Promise { - return this.authRequester(scopes); + async createSession(options: CreateSessionOptions): Promise { + if (options.instantPopup) { + return this.showPopup(options.scopes); + } + return this.authRequester(options.scopes); } async refreshSession(): Promise { @@ -142,7 +147,8 @@ export class DefaultAuthConnector } } - private async showPopup(scope: string): Promise { + private async showPopup(scopes: Set): Promise { + const scope = this.joinScopesFunc(scopes); const popupUrl = this.buildUrl('/start', { scope }); const payload = await showLoginPopup({ diff --git a/packages/core-api/src/lib/AuthConnector/types.ts b/packages/core-api/src/lib/AuthConnector/types.ts index 146c31cbe1..46175a265f 100644 --- a/packages/core-api/src/lib/AuthConnector/types.ts +++ b/packages/core-api/src/lib/AuthConnector/types.ts @@ -14,12 +14,17 @@ * limitations under the License. */ +export type CreateSessionOptions = { + scopes: Set; + instantPopup?: boolean; +}; + /** * An AuthConnector is responsible for realizing auth session actions * by for example communicating with a backend or interacting with the user. */ export type AuthConnector = { - createSession(scopes: Set): Promise; + createSession(options: CreateSessionOptions): Promise; refreshSession(): Promise; removeSession(): Promise; }; diff --git a/packages/core-api/src/lib/AuthSessionManager/AuthSessionStore.ts b/packages/core-api/src/lib/AuthSessionManager/AuthSessionStore.ts index 14796c6e11..8d318f5bff 100644 --- a/packages/core-api/src/lib/AuthSessionManager/AuthSessionStore.ts +++ b/packages/core-api/src/lib/AuthSessionManager/AuthSessionStore.ts @@ -18,6 +18,7 @@ import { SessionManager, SessionScopesFunc, SessionShouldRefreshFunc, + GetSessionOptions, } from './types'; import { SessionScopeHelper } from './common'; @@ -59,18 +60,7 @@ export class AuthSessionStore implements SessionManager { }); } - async getSession(options: { - optional: false; - scopes?: Set; - }): Promise; - async getSession(options: { - optional?: boolean; - scopes?: Set; - }): Promise; - async getSession(options: { - optional?: boolean; - scopes?: Set; - }): Promise { + async getSession(options: GetSessionOptions): Promise { const { scopes } = options; const session = this.loadSession(); diff --git a/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts b/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts index 8684c932b2..ee7cbab2ae 100644 --- a/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts +++ b/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts @@ -113,6 +113,23 @@ describe('RefreshingAuthSessionManager', () => { expect(refreshSession).toBeCalledTimes(1); }); + it('should forward option to instantly show auth popup', async () => { + const createSession = jest.fn(); + const refreshSession = jest.fn().mockRejectedValue(new Error('NOPE')); + const manager = new RefreshingAuthSessionManager({ + connector: { createSession, refreshSession }, + ...defaultOptions, + } as any); + + expect(await manager.getSession({ instantPopup: true })).toBe(undefined); + expect(createSession).toBeCalledTimes(1); + expect(createSession).toHaveBeenCalledWith({ + scopes: new Set(), + instantPopup: true, + }); + expect(refreshSession).toBeCalledTimes(1); + }); + it('should remove session and reload', async () => { // This is a workaround that is used by Facebook and the Jest core team // It is a limitation with the newest versions of JSDOM, and newer browser standards diff --git a/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts b/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts index cbd6d5ca46..64df3deca4 100644 --- a/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts +++ b/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts @@ -18,6 +18,7 @@ import { SessionManager, SessionScopesFunc, SessionShouldRefreshFunc, + GetSessionOptions, } from './types'; import { AuthConnector } from '../AuthConnector'; import { SessionScopeHelper, hasScopes } from './common'; @@ -60,18 +61,7 @@ export class RefreshingAuthSessionManager implements SessionManager { this.helper = new SessionScopeHelper({ sessionScopes, defaultScopes }); } - async getSession(options: { - optional: false; - scopes?: Set; - }): Promise; - async getSession(options: { - optional?: boolean; - scopes?: Set; - }): Promise; - async getSession(options: { - optional?: boolean; - scopes?: Set; - }): Promise { + async getSession(options: GetSessionOptions): Promise { if ( this.helper.sessionExistsAndHasScope(this.currentSession, options.scopes) ) { @@ -115,9 +105,10 @@ export class RefreshingAuthSessionManager implements SessionManager { } // We can call authRequester multiple times, the returned session will contain all requested scopes. - this.currentSession = await this.connector.createSession( - this.helper.getExtendedScope(this.currentSession, options.scopes), - ); + this.currentSession = await this.connector.createSession({ + ...options, + scopes: this.helper.getExtendedScope(this.currentSession, options.scopes), + }); return this.currentSession; } diff --git a/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.test.ts b/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.test.ts index dc13aec0c2..7d47fa91df 100644 --- a/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.test.ts +++ b/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.test.ts @@ -62,7 +62,7 @@ describe('StaticAuthSessionManager', () => { it('should only request auth once for same scopes', async () => { const createSession = jest .fn() - .mockImplementation(scopes => [...scopes].join(' ')); + .mockImplementation(({ scopes }) => [...scopes].join(' ')); const manager = new StaticAuthSessionManager({ connector: { createSession, ...baseConnector }, ...defaultOptions, diff --git a/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.ts b/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.ts index 8d057ecc58..6e6db47a99 100644 --- a/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.ts +++ b/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { SessionManager } from './types'; +import { SessionManager, GetSessionOptions } from './types'; import { AuthConnector } from '../AuthConnector'; import { SessionScopeHelper } from './common'; @@ -43,18 +43,7 @@ export class StaticAuthSessionManager implements SessionManager { this.helper = new SessionScopeHelper({ sessionScopes, defaultScopes }); } - async getSession(options: { - optional: false; - scopes?: Set; - }): Promise; - async getSession(options: { - optional?: boolean; - scopes?: Set; - }): Promise; - async getSession(options: { - optional?: boolean; - scopes?: Set; - }): Promise { + async getSession(options: GetSessionOptions): Promise { if ( this.helper.sessionExistsAndHasScope(this.currentSession, options.scopes) ) { @@ -67,9 +56,10 @@ export class StaticAuthSessionManager implements SessionManager { } // We can call authRequester multiple times, the returned session will contain all requested scopes. - this.currentSession = await this.connector.createSession( - this.helper.getExtendedScope(this.currentSession, options.scopes), - ); + this.currentSession = await this.connector.createSession({ + ...options, + scopes: this.helper.getExtendedScope(this.currentSession, options.scopes), + }); return this.currentSession; } diff --git a/packages/core-api/src/lib/AuthSessionManager/types.ts b/packages/core-api/src/lib/AuthSessionManager/types.ts index 5ebc8643e1..d28a751fbf 100644 --- a/packages/core-api/src/lib/AuthSessionManager/types.ts +++ b/packages/core-api/src/lib/AuthSessionManager/types.ts @@ -14,17 +14,19 @@ * limitations under the License. */ +export type GetSessionOptions = { + optional?: boolean; + instantPopup?: boolean; + scopes?: Set; +}; + /** * A sessions manager keeps track of the current session and makes sure that * multiple simultaneous requests for sessions with different scope are handled * in a correct way. */ export type SessionManager = { - getSession(options: { optional: false; scopes?: Set }): Promise; - getSession(options: { - optional?: boolean; - scopes?: Set; - }): Promise; + getSession(options: GetSessionOptions): Promise; removeSession(): Promise; };