Merge pull request #1031 from spotify/rugvip/auth-options
packages/core-api: properly forward auth options to respect the instantPopup option
This commit is contained in:
@@ -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<string>;
|
||||
getAccessToken(
|
||||
scope?: OAuthScope,
|
||||
options?: AccessTokenOptions,
|
||||
): Promise<string>;
|
||||
|
||||
/**
|
||||
* Log out the user's session. This will reload the page.
|
||||
|
||||
@@ -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<GoogleSession>) {}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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<AuthSession>
|
||||
private readonly basePath: string;
|
||||
private readonly environment: string;
|
||||
private readonly provider: AuthProvider & { id: string };
|
||||
private readonly joinScopesFunc: (scopes: Set<string>) => string;
|
||||
private readonly authRequester: AuthRequester<AuthSession>;
|
||||
private readonly sessionTransform: (response: any) => Promise<AuthSession>;
|
||||
|
||||
@@ -84,18 +85,22 @@ export class DefaultAuthConnector<AuthSession>
|
||||
|
||||
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<string>): Promise<AuthSession> {
|
||||
return this.authRequester(scopes);
|
||||
async createSession(options: CreateSessionOptions): Promise<AuthSession> {
|
||||
if (options.instantPopup) {
|
||||
return this.showPopup(options.scopes);
|
||||
}
|
||||
return this.authRequester(options.scopes);
|
||||
}
|
||||
|
||||
async refreshSession(): Promise<any> {
|
||||
@@ -142,7 +147,8 @@ export class DefaultAuthConnector<AuthSession>
|
||||
}
|
||||
}
|
||||
|
||||
private async showPopup(scope: string): Promise<AuthSession> {
|
||||
private async showPopup(scopes: Set<string>): Promise<AuthSession> {
|
||||
const scope = this.joinScopesFunc(scopes);
|
||||
const popupUrl = this.buildUrl('/start', { scope });
|
||||
|
||||
const payload = await showLoginPopup({
|
||||
|
||||
@@ -14,12 +14,17 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export type CreateSessionOptions = {
|
||||
scopes: Set<string>;
|
||||
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<AuthSession> = {
|
||||
createSession(scopes: Set<string>): Promise<AuthSession>;
|
||||
createSession(options: CreateSessionOptions): Promise<AuthSession>;
|
||||
refreshSession(): Promise<AuthSession>;
|
||||
removeSession(): Promise<void>;
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
SessionManager,
|
||||
SessionScopesFunc,
|
||||
SessionShouldRefreshFunc,
|
||||
GetSessionOptions,
|
||||
} from './types';
|
||||
import { SessionScopeHelper } from './common';
|
||||
|
||||
@@ -59,18 +60,7 @@ export class AuthSessionStore<T> implements SessionManager<T> {
|
||||
});
|
||||
}
|
||||
|
||||
async getSession(options: {
|
||||
optional: false;
|
||||
scopes?: Set<string>;
|
||||
}): Promise<T>;
|
||||
async getSession(options: {
|
||||
optional?: boolean;
|
||||
scopes?: Set<string>;
|
||||
}): Promise<T | undefined>;
|
||||
async getSession(options: {
|
||||
optional?: boolean;
|
||||
scopes?: Set<string>;
|
||||
}): Promise<T | undefined> {
|
||||
async getSession(options: GetSessionOptions): Promise<T | undefined> {
|
||||
const { scopes } = options;
|
||||
const session = this.loadSession();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<T> implements SessionManager<T> {
|
||||
this.helper = new SessionScopeHelper({ sessionScopes, defaultScopes });
|
||||
}
|
||||
|
||||
async getSession(options: {
|
||||
optional: false;
|
||||
scopes?: Set<string>;
|
||||
}): Promise<T>;
|
||||
async getSession(options: {
|
||||
optional?: boolean;
|
||||
scopes?: Set<string>;
|
||||
}): Promise<T | undefined>;
|
||||
async getSession(options: {
|
||||
optional?: boolean;
|
||||
scopes?: Set<string>;
|
||||
}): Promise<T | undefined> {
|
||||
async getSession(options: GetSessionOptions): Promise<T | undefined> {
|
||||
if (
|
||||
this.helper.sessionExistsAndHasScope(this.currentSession, options.scopes)
|
||||
) {
|
||||
@@ -115,9 +105,10 @@ export class RefreshingAuthSessionManager<T> implements SessionManager<T> {
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<T> implements SessionManager<T> {
|
||||
this.helper = new SessionScopeHelper({ sessionScopes, defaultScopes });
|
||||
}
|
||||
|
||||
async getSession(options: {
|
||||
optional: false;
|
||||
scopes?: Set<string>;
|
||||
}): Promise<T>;
|
||||
async getSession(options: {
|
||||
optional?: boolean;
|
||||
scopes?: Set<string>;
|
||||
}): Promise<T | undefined>;
|
||||
async getSession(options: {
|
||||
optional?: boolean;
|
||||
scopes?: Set<string>;
|
||||
}): Promise<T | undefined> {
|
||||
async getSession(options: GetSessionOptions): Promise<T | undefined> {
|
||||
if (
|
||||
this.helper.sessionExistsAndHasScope(this.currentSession, options.scopes)
|
||||
) {
|
||||
@@ -67,9 +56,10 @@ export class StaticAuthSessionManager<T> implements SessionManager<T> {
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,17 +14,19 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export type GetSessionOptions = {
|
||||
optional?: boolean;
|
||||
instantPopup?: boolean;
|
||||
scopes?: Set<string>;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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<T> = {
|
||||
getSession(options: { optional: false; scopes?: Set<string> }): Promise<T>;
|
||||
getSession(options: {
|
||||
optional?: boolean;
|
||||
scopes?: Set<string>;
|
||||
}): Promise<T | undefined>;
|
||||
getSession(options: GetSessionOptions): Promise<T | undefined>;
|
||||
|
||||
removeSession(): Promise<void>;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user