packages/core: remove custom OAuth scope classes in favor for early transform functions

This commit is contained in:
Patrik Oldsberg
2020-05-19 17:22:05 +02:00
parent bf6c807d6d
commit 2d65c9a38f
14 changed files with 138 additions and 320 deletions
@@ -18,18 +18,6 @@ import { IconComponent } from '../../../icons';
import { Observable } from '../../types';
import { ApiRef } from '../ApiRef';
export type OAuthScopes = {
extend(scopes: OAuthScopeLike): OAuthScopes;
hasScopes(scopes: OAuthScopeLike): boolean;
toSet(): Set<string>;
toString(): string;
};
export type OAuthScopeLike =
| string /** Space separated scope strings */
| string[] /** Array of individual scope strings */
| OAuthScopes;
/**
* Information about the auth provider that we're requesting a login towards.
*
@@ -62,7 +50,7 @@ export type AuthRequesterOptions<AuthResponse> = {
* Implementation of the auth flow, which will be called synchronously when
* trigger() is called on an auth requests.
*/
onAuthRequest(scope: OAuthScopes): Promise<AuthResponse>;
onAuthRequest(scopes: Set<string>): Promise<AuthResponse>;
};
/**
@@ -76,7 +64,7 @@ export type AuthRequesterOptions<AuthResponse> = {
* union of all requested scopes.
*/
export type AuthRequester<AuthResponse> = (
scope: OAuthScopes,
scopes: Set<string>,
) => Promise<AuthResponse>;
/**
@@ -1,77 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* 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 { OAuthScopes, OAuthScopeLike } from '../../definitions';
/**
* The BasicOAuthScopes class is an implementation of OAuthScopes that
* works for any simple comma- or space-separated format of scope.
*/
export class BasicOAuthScopes implements OAuthScopes {
static from(scopes: OAuthScopeLike, normalizer?: (scope: string) => string) {
const normalized = BasicOAuthScopes.asStrings(scopes, normalizer);
return new BasicOAuthScopes(new Set(normalized), normalizer);
}
constructor(
private readonly scopes: Set<string>,
private readonly normalizer?: (scope: string) => string,
) {}
extend(requestedScopes: OAuthScopeLike): BasicOAuthScopes {
const newScopes = new Set(this.scopes);
BasicOAuthScopes.asStrings(requestedScopes, this.normalizer).forEach((s) =>
newScopes.add(s),
);
return new BasicOAuthScopes(newScopes, this.normalizer);
}
hasScopes(scopes: OAuthScopeLike): boolean {
return BasicOAuthScopes.asStrings(scopes, this.normalizer).every((s) =>
this.scopes.has(s),
);
}
toSet(): Set<string> {
return this.scopes;
}
toString(): string {
return Array.from(this.scopes).join(' ');
}
toJSON() {
return Array.from(this.scopes);
}
static asStrings(
input: OAuthScopeLike,
normalizer?: (scope: string) => string,
): string[] {
let scopeArray: string[];
if (typeof input === 'string') {
scopeArray = input.split(/[,\s]/).filter(Boolean);
} else if (Array.isArray(input)) {
scopeArray = input;
} else {
scopeArray = Array.from(input.toSet());
}
if (normalizer) {
scopeArray = scopeArray.map((x) => normalizer(x));
}
return scopeArray;
}
}
@@ -16,7 +16,6 @@
import MockOAuthApi from './MockOAuthApi';
import PowerIcon from '@material-ui/icons/Power';
import { BasicOAuthScopes } from './BasicOAuthScopes';
describe('MockOAuthApi', () => {
it('should trigger all requests', async () => {
@@ -36,11 +35,11 @@ describe('MockOAuthApi', () => {
});
const promises = [
requester1(BasicOAuthScopes.from('a')),
requester1(BasicOAuthScopes.from('b')),
requester2(BasicOAuthScopes.from('a b')),
requester2(BasicOAuthScopes.from('b c')),
requester2(BasicOAuthScopes.from('c a')),
requester1(new Set(['a'])),
requester1(new Set(['b'])),
requester2(new Set(['a', 'b'])),
requester2(new Set(['b', 'c'])),
requester2(new Set(['c', 'a'])),
];
await expect(
@@ -58,9 +57,9 @@ describe('MockOAuthApi', () => {
]);
expect(authHandler1).toHaveBeenCalledTimes(1);
expect(authHandler1).toHaveBeenCalledWith(BasicOAuthScopes.from('a b'));
expect(authHandler1).toHaveBeenCalledWith(new Set(['a', 'b']));
expect(authHandler2).toHaveBeenCalledTimes(1);
expect(authHandler2).toHaveBeenCalledWith(BasicOAuthScopes.from('a b c'));
expect(authHandler2).toHaveBeenCalledWith(new Set(['a', 'b', 'c']));
});
it('should reject all requests', async () => {
@@ -79,11 +78,11 @@ describe('MockOAuthApi', () => {
});
const promises = [
requester1(BasicOAuthScopes.from('a')),
requester1(BasicOAuthScopes.from('b')),
requester2(BasicOAuthScopes.from('a b')),
requester2(BasicOAuthScopes.from('b c')),
requester2(BasicOAuthScopes.from('c a')),
requester1(new Set(['a'])),
requester1(new Set(['b'])),
requester2(new Set(['a', 'b'])),
requester2(new Set(['b', 'c'])),
requester2(new Set(['c', 'a'])),
];
await expect(
@@ -16,7 +16,6 @@
import { wait } from '@testing-library/react';
import { OAuthPendingRequests } from './OAuthPendingRequests';
import { BasicOAuthScopes } from './BasicOAuthScopes';
describe('OAuthPendingRequests', () => {
it('notifies new observers about current state', async () => {
@@ -24,7 +23,7 @@ describe('OAuthPendingRequests', () => {
const next = jest.fn();
const error = jest.fn();
const input = BasicOAuthScopes.from('a b');
const input = new Set(['a', 'b']);
target.pending().subscribe({ next, error });
target.request(input);
@@ -39,11 +38,11 @@ describe('OAuthPendingRequests', () => {
const next = jest.fn();
const error = jest.fn();
const request1 = target.request(BasicOAuthScopes.from('a'));
const request2 = target.request(BasicOAuthScopes.from('a'));
const request1 = target.request(new Set(['a']));
const request2 = target.request(new Set(['a']));
target.pending().subscribe({ next, error });
target.resolve(BasicOAuthScopes.from('a'), 'session1');
target.resolve(BasicOAuthScopes.from('a'), 'session2');
target.resolve(new Set(['a']), 'session1');
target.resolve(new Set(['a']), 'session2');
await expect(request1).resolves.toBe('session1');
await expect(request2).resolves.toBe('session1');
@@ -53,10 +52,10 @@ describe('OAuthPendingRequests', () => {
it('can resolve through the observable', async () => {
const target = new OAuthPendingRequests<string>();
const next = jest.fn((pendingRequest) => pendingRequest.resolve('done'));
const next = jest.fn(pendingRequest => pendingRequest.resolve('done'));
const error = jest.fn();
const request1 = target.request(BasicOAuthScopes.from('a'));
const request1 = target.request(new Set(['a']));
target.pending().subscribe({ next, error });
await expect(request1).resolves.toBe('done');
@@ -70,11 +69,11 @@ describe('OAuthPendingRequests', () => {
const error = jest.fn();
const rejection = new Error('eek');
const request1 = target.request(BasicOAuthScopes.from('a'));
const request2 = target.request(BasicOAuthScopes.from('a'));
const request1 = target.request(new Set(['a']));
const request2 = target.request(new Set(['a']));
target.pending().subscribe({ next, error });
target.reject(rejection);
target.resolve(BasicOAuthScopes.from('a'), 'session');
target.resolve(new Set(['a']), 'session');
await expect(request1).rejects.toBe(rejection);
await expect(request2).rejects.toBe(rejection);
@@ -85,10 +84,10 @@ describe('OAuthPendingRequests', () => {
it('can reject through the observable', async () => {
const target = new OAuthPendingRequests<string>();
const rejection = new Error('nope');
const next = jest.fn((pendingRequest) => pendingRequest.reject(rejection));
const next = jest.fn(pendingRequest => pendingRequest.reject(rejection));
const error = jest.fn();
const request1 = target.request(BasicOAuthScopes.from('a'));
const request1 = target.request(new Set(['a']));
target.pending().subscribe({ next, error });
await expect(request1).rejects.toBe(rejection);
@@ -14,22 +14,50 @@
* limitations under the License.
*/
import { OAuthScopes } from '../../definitions';
import { BehaviorSubject } from '../lib';
import { Observable } from '../../../types';
type RequestQueueEntry<ResultType> = {
scopes: OAuthScopes;
scopes: Set<string>;
resolve: (value?: ResultType | PromiseLike<ResultType> | undefined) => void;
reject: (reason: Error) => void;
};
export type PendingRequest<ResultType> = {
scopes: OAuthScopes | undefined;
scopes: Set<string> | undefined;
resolve: (value: ResultType) => void;
reject: (reason: Error) => void;
};
export function hasScopes(
searched: Set<string>,
searchFor: Set<string>,
): boolean {
for (const scope of searchFor) {
if (!searched.has(scope)) {
return false;
}
}
return true;
}
export function joinScopes(
scopes: Set<string>,
...moreScopess: Set<string>[]
): Set<string> {
const result = new Set(scopes);
for (const moreScopes of moreScopess) {
for (const scope of moreScopes) {
if (!result.has(scope)) {
result.add(scope);
}
}
}
return result;
}
/**
* The OAuthPendingRequests class is a utility for managing and observing
* a stream of requests for oauth scopes for a single provider, and resolving
@@ -41,7 +69,7 @@ export class OAuthPendingRequests<ResultType> {
this.getCurrentPending(),
);
request(scopes: OAuthScopes): Promise<ResultType> {
request(scopes: Set<string>): Promise<ResultType> {
return new Promise((resolve, reject) => {
this.requests.push({ scopes, resolve, reject });
@@ -49,9 +77,9 @@ export class OAuthPendingRequests<ResultType> {
});
}
resolve(scopes: OAuthScopes, result: ResultType): void {
this.requests = this.requests.filter((request) => {
if (scopes.hasScopes(request.scopes)) {
resolve(scopes: Set<string>, result: ResultType): void {
this.requests = this.requests.filter(request => {
if (hasScopes(scopes, request.scopes)) {
request.resolve(result);
return false;
}
@@ -62,7 +90,7 @@ export class OAuthPendingRequests<ResultType> {
}
reject(error: Error) {
this.requests.forEach((request) => request.reject(error));
this.requests.forEach(request => request.reject(error));
this.requests = [];
this.subject.next(this.getCurrentPending());
@@ -79,7 +107,7 @@ export class OAuthPendingRequests<ResultType> {
: this.requests
.slice(1)
.reduce(
(acc, current) => acc.extend(current.scopes),
(acc, current) => joinScopes(acc, current.scopes),
this.requests[0].scopes,
);
@@ -16,7 +16,6 @@
import ProviderIcon from '@material-ui/icons/AcUnit';
import { OAuthRequestManager } from './OAuthRequestManager';
import { BasicOAuthScopes } from './BasicOAuthScopes';
describe('OAuthRequestManager', () => {
it('should forward a requests', async () => {
@@ -38,7 +37,7 @@ describe('OAuthRequestManager', () => {
expect(reqSpy).toHaveBeenCalledTimes(2);
expect(reqSpy).toHaveBeenLastCalledWith([]);
const req = requester(BasicOAuthScopes.from('my-scope'));
const req = requester(new Set(['my-scope']));
expect(reqSpy).toHaveBeenCalledTimes(3);
expect(reqSpy).toHaveBeenLastCalledWith([
@@ -43,7 +43,7 @@ export class OAuthRequestManager implements OAuthRequestApi {
this.handlerCount++;
handler.pending().subscribe({
next: (scopeRequest) => {
next: scopeRequest => {
const newRequests = this.currentRequests.slice();
const request = this.makeAuthRequest(scopeRequest, options);
if (!request) {
@@ -57,7 +57,7 @@ export class OAuthRequestManager implements OAuthRequestApi {
},
});
return (scopes) => {
return scopes => {
return handler.request(scopes);
};
}
@@ -15,11 +15,12 @@
*/
import GoogleAuth from './GoogleAuth';
import GoogleScopes from './GoogleScopes';
const theFuture = new Date(Date.now() + 3600000);
const thePast = new Date(Date.now() - 10);
const PREFIX = 'https://www.googleapis.com/auth/';
describe('GoogleAuth', () => {
it('should save result form createSession', async () => {
const createSession = jest.fn().mockResolvedValue({ expiresAt: theFuture });
@@ -41,7 +42,7 @@ describe('GoogleAuth', () => {
const googleAuth = new GoogleAuth({ createSession, refreshSession } as any);
createSession.mockResolvedValue({
scopes: GoogleScopes.from('a'),
scopes: new Set([`${PREFIX}a`]),
expiresAt: theFuture,
});
await googleAuth.getSession({ scope: 'a' });
@@ -59,11 +60,11 @@ describe('GoogleAuth', () => {
const refreshSession = jest
.fn()
.mockRejectedValueOnce(new Error('NOPE'))
.mockResolvedValue({ scopes: GoogleScopes.from('a') });
.mockResolvedValue({ scopes: new Set([`${PREFIX}a`]) });
const googleAuth = new GoogleAuth({ createSession, refreshSession } as any);
createSession.mockResolvedValue({
scopes: GoogleScopes.from('a'),
scopes: new Set([`${PREFIX}a`]),
expiresAt: thePast,
});
@@ -150,7 +151,7 @@ describe('GoogleAuth', () => {
const refreshSession = jest.fn().mockResolvedValue({
accessToken: 'access-token',
expiresAt: theFuture,
scopes: GoogleScopes.from('not-enough'),
scopes: new Set([`${PREFIX}not-enough`]),
});
const googleAuth = new GoogleAuth({ createSession, refreshSession } as any);
@@ -169,7 +170,7 @@ describe('GoogleAuth', () => {
const initialSession = {
idToken: 'token1',
expiresAt: theFuture,
scopes: GoogleScopes.empty(),
scopes: new Set(),
};
const refreshSession = jest
.fn()
@@ -177,7 +178,7 @@ describe('GoogleAuth', () => {
.mockResolvedValue({
idToken: 'token2',
expiresAt: theFuture,
scopes: GoogleScopes.empty(),
scopes: new Set(),
});
const googleAuth = new GoogleAuth({ refreshSession } as any);
@@ -16,9 +16,7 @@
import GoogleIcon from '@material-ui/icons/AcUnit';
import { AuthHelper } from '../../lib/AuthHelper';
import GoogleScopes from './GoogleScopes';
import { GoogleSession } from './types';
import { OAuthScopes } from '../../..';
import {
OAuthApi,
OpenIdConnectApi,
@@ -26,6 +24,7 @@ import {
} from '../../../definitions/auth';
import { OAuthRequestApi } from '../../../definitions';
import { GenericAuthHelper } from '../../lib/AuthHelper/AuthHelper';
import { hasScopes } from '../../OAuthRequestManager/OAuthPendingRequests';
export type GoogleAuthResponse = {
accessToken: string;
@@ -34,6 +33,13 @@ export type GoogleAuthResponse = {
expiresInSeconds: number;
};
const SCOPE_PREFIX = 'https://www.googleapis.com/auth/';
const DEFAULT_SCOPES = [
'openid',
`${SCOPE_PREFIX}userinfo.email`,
`${SCOPE_PREFIX}userinfo.profile`,
];
class GoogleAuth implements OAuthApi, OpenIdConnectApi {
private currentSession: GoogleSession | undefined;
@@ -50,7 +56,7 @@ class GoogleAuth implements OAuthApi, OpenIdConnectApi {
return {
idToken: res.idToken,
accessToken: res.accessToken,
scopes: GoogleScopes.from(res.scopes),
scopes: GoogleAuth.normalizeScopes(res.scopes),
expiresAt: new Date(Date.now() + res.expiresInSeconds * 1000),
};
},
@@ -86,14 +92,16 @@ class GoogleAuth implements OAuthApi, OpenIdConnectApi {
optional?: boolean;
scope?: string | string[];
}): Promise<GoogleSession | undefined> {
if (this.sessionExistsAndHasScope(this.currentSession, options.scope)) {
const normalizedScope = GoogleAuth.normalizeScopes(options.scope);
if (this.sessionExistsAndHasScope(this.currentSession, normalizedScope)) {
if (!this.sessionWillExpire(this.currentSession!)) {
return this.currentSession!;
}
try {
const refreshedSession = await this.helper.refreshSession();
if (refreshedSession.scopes.hasScopes(this.currentSession!.scopes)) {
if (hasScopes(refreshedSession.scopes, this.currentSession!.scopes)) {
this.currentSession = refreshedSession;
}
return refreshedSession;
@@ -125,7 +133,7 @@ class GoogleAuth implements OAuthApi, OpenIdConnectApi {
// We can call authRequester multiple times, the returned session will contain all requested scopes.
this.currentSession = await this.helper.createSession(
this.getExtendedScope(options.scope),
this.getExtendedScope(normalizedScope),
);
return this.currentSession;
}
@@ -137,7 +145,7 @@ class GoogleAuth implements OAuthApi, OpenIdConnectApi {
private sessionExistsAndHasScope(
session: GoogleSession | undefined,
scope?: string | string[],
scope?: Set<string>,
): boolean {
if (!session) {
return false;
@@ -145,7 +153,7 @@ class GoogleAuth implements OAuthApi, OpenIdConnectApi {
if (!scope) {
return true;
}
return session.scopes.hasScopes(scope);
return hasScopes(session.scopes, scope);
}
private sessionWillExpire(session: GoogleSession) {
@@ -153,15 +161,45 @@ class GoogleAuth implements OAuthApi, OpenIdConnectApi {
return expiresInSec < 60 * 5;
}
private getExtendedScope(scope?: string | string[]) {
let newScope: OAuthScopes = GoogleScopes.default();
private getExtendedScope(scopes: Set<string>) {
const newScope = new Set(DEFAULT_SCOPES);
if (this.currentSession) {
newScope = this.currentSession.scopes;
for (const scope of this.currentSession.scopes) {
newScope.add(scope);
}
}
if (scope) {
newScope = newScope.extend(scope);
for (const scope of scopes) {
newScope.add(scope);
}
return newScope;
}
private static normalizeScopes(scopes?: string | string[]): Set<string> {
if (!scopes) {
return new Set();
}
const scopeList = Array.isArray(scopes)
? scopes
: scopes.split(' ').filter(Boolean);
const normalizedScopes = scopeList.map(scope => {
if (scope === 'openid') {
return scope;
}
if (scope === 'profile' || scope === 'email') {
return `${SCOPE_PREFIX}userinfo.${scope}`;
}
if (scope.startsWith(SCOPE_PREFIX)) {
return scope;
}
return `${SCOPE_PREFIX}${scope}`;
});
return new Set(normalizedScopes);
}
}
export default GoogleAuth;
@@ -1,88 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* 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 GoogleScopes from './GoogleScopes';
const PREFIX = 'https://www.googleapis.com/auth/';
describe('GoogleScopes', () => {
it('should be created from scopes', () => {
const scopes = GoogleScopes.from('a openid b profile');
expect(scopes.toString()).toBe(
`${PREFIX}a openid ${PREFIX}b ${PREFIX}userinfo.profile`,
);
});
it('should be created with default scopes', () => {
expect(GoogleScopes.default().toString()).toBe(
`openid ${PREFIX}userinfo.email ${PREFIX}userinfo.profile`,
);
});
it('should have or not have scopes', () => {
const scopes = GoogleScopes.from(`a b ${PREFIX}c`);
expect(scopes.hasScopes('a')).toBe(true);
expect(scopes.hasScopes('a b')).toBe(true);
expect(scopes.hasScopes('b')).toBe(true);
expect(scopes.hasScopes('b c')).toBe(true);
expect(scopes.hasScopes('a b c')).toBe(true);
expect(scopes.hasScopes(`a b ${PREFIX}c`)).toBe(true);
expect(scopes.hasScopes(`a ${PREFIX}b c`)).toBe(true);
expect(scopes.hasScopes('a b c d')).toBe(false);
expect(scopes.hasScopes('d')).toBe(false);
expect(scopes.hasScopes('')).toBe(true);
expect(scopes.hasScopes('abc')).toBe(false);
expect(scopes.hasScopes(`${PREFIX}a`)).toBe(true);
});
it('should handle scope shorthands correctly', () => {
const scopes = GoogleScopes.default();
expect(scopes.hasScopes('email')).toBe(true);
expect(scopes.hasScopes('profile')).toBe(true);
expect(scopes.hasScopes('openid')).toBe(true);
expect(scopes.hasScopes('userinfo.email')).toBe(true);
expect(scopes.hasScopes('userinfo.profile')).toBe(true);
expect(scopes.hasScopes('userinfo.openid')).toBe(false);
expect(scopes.hasScopes(`${PREFIX}userinfo.email`)).toBe(true);
expect(scopes.hasScopes(`${PREFIX}userinfo.profile`)).toBe(true);
expect(scopes.hasScopes(`${PREFIX}userinfo.openid`)).toBe(false);
expect(scopes.hasScopes(`${PREFIX}email`)).toBe(false);
expect(scopes.hasScopes(`${PREFIX}profile`)).toBe(false);
expect(scopes.hasScopes(`${PREFIX}openid`)).toBe(false);
});
it('should be extended', () => {
const scopes = GoogleScopes.from('a b');
expect(scopes.extend('')).not.toBe(scopes);
expect(scopes.extend('d').toString()).toBe(
`${PREFIX}a ${PREFIX}b ${PREFIX}d`,
);
expect(scopes.extend('profile').toString()).toBe(
`${PREFIX}a ${PREFIX}b ${PREFIX}userinfo.profile`,
);
expect(scopes.extend('d profile').toString()).toBe(
`${PREFIX}a ${PREFIX}b ${PREFIX}d ${PREFIX}userinfo.profile`,
);
expect(scopes.extend(`${PREFIX}d profile`).toString()).toBe(
`${PREFIX}a ${PREFIX}b ${PREFIX}d ${PREFIX}userinfo.profile`,
);
expect(scopes.extend('a').toString()).toBe(scopes.toString());
expect(scopes.extend('').toString()).toBe(scopes.toString());
expect(scopes.extend('b').toString()).toBe(scopes.toString());
expect(scopes.extend('b a').toString()).toBe(scopes.toString());
expect(scopes.extend('b a b a a').toString()).toBe(scopes.toString());
});
});
@@ -1,62 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* 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 { BasicOAuthScopes } from '../../OAuthRequestManager/BasicOAuthScopes';
import { OAuthScopeLike } from '../../..';
const SCOPE_PREFIX = 'https://www.googleapis.com/auth/';
export default class GoogleScopes extends BasicOAuthScopes {
static from(scope: OAuthScopeLike): GoogleScopes {
return new GoogleScopes(
new Set(BasicOAuthScopes.asStrings(scope, GoogleScopes.canonicalScope)),
);
}
static default(): GoogleScopes {
return new GoogleScopes(
new Set([
'openid',
`${SCOPE_PREFIX}userinfo.email`,
`${SCOPE_PREFIX}userinfo.profile`,
]),
);
}
static empty(): GoogleScopes {
return new GoogleScopes(new Set());
}
constructor(scopes: Set<string>) {
super(scopes, GoogleScopes.canonicalScope);
}
private static canonicalScope(scope: string): string {
if (scope === 'openid') {
return scope;
}
if (scope === 'profile' || scope === 'email') {
return `${SCOPE_PREFIX}userinfo.${scope}`;
}
if (scope.startsWith(SCOPE_PREFIX)) {
return scope;
}
return `${SCOPE_PREFIX}${scope}`;
}
}
@@ -14,11 +14,9 @@
* limitations under the License.
*/
import GoogleScopes from './GoogleScopes';
export type GoogleSession = {
idToken: string;
accessToken: string;
scopes: GoogleScopes;
scopes: Set<string>;
expiresAt: Date;
};
@@ -17,7 +17,6 @@
import ProviderIcon from '@material-ui/icons/AcUnit';
import { AuthHelper } from './AuthHelper';
import MockOAuthApi from '../../OAuthRequestManager/MockOAuthApi';
import { BasicOAuthScopes } from '../../OAuthRequestManager/BasicOAuthScopes';
import * as loginPopup from '../loginPopup';
const anyFetch = fetch as any;
@@ -33,7 +32,7 @@ const defaultOptions = {
oauthRequestApi: new MockOAuthApi(),
sessionTransform: ({ expiresInSeconds, ...res }: any) => ({
...res,
scopes: BasicOAuthScopes.from(res.scopes),
scopes: new Set(res.scopes.split(' ')),
expiresAt: new Date(Date.now() + expiresInSeconds * 1000),
}),
};
@@ -58,7 +57,7 @@ describe('AuthHelper', () => {
const session = await helper.refreshSession();
expect(session.idToken).toBe('mock-id-token');
expect(session.accessToken).toBe('mock-access-token');
expect(session.scopes.hasScopes('a b c')).toBe(true);
expect(session.scopes).toEqual(new Set(['a', 'b', 'c']));
expect(session.expiresAt.getTime()).toBeLessThan(Date.now() + 70000);
expect(session.expiresAt.getTime()).toBeGreaterThan(Date.now() + 50000);
});
@@ -87,7 +86,7 @@ describe('AuthHelper', () => {
...defaultOptions,
oauthRequestApi: mockOauth,
});
const promise = helper.createSession(BasicOAuthScopes.from('a b'));
const promise = helper.createSession(new Set(['a', 'b']));
await mockOauth.rejectAll();
await expect(promise).rejects.toMatchObject({ name: 'RejectedError' });
});
@@ -107,7 +106,7 @@ describe('AuthHelper', () => {
oauthRequestApi: mockOauth,
});
const sessionPromise = helper.createSession(BasicOAuthScopes.from('a b'));
const sessionPromise = helper.createSession(new Set(['a', 'b']));
await mockOauth.triggerAll();
@@ -119,7 +118,7 @@ describe('AuthHelper', () => {
await expect(sessionPromise).resolves.toEqual({
idToken: 'my-id-token',
accessToken: 'my-access-token',
scopes: expect.any(BasicOAuthScopes),
scopes: expect.any(Set),
expiresAt: expect.any(Date),
});
});
@@ -15,11 +15,7 @@
*/
import { AuthRequester } from '../../..';
import {
OAuthRequestApi,
AuthProvider,
OAuthScopes,
} from '../../../definitions';
import { OAuthRequestApi, AuthProvider } from '../../../definitions';
import { showLoginPopup } from '../loginPopup';
const DEFAULT_BASE_PATH = '/api/auth/';
@@ -37,7 +33,7 @@ type Options<AuthSession> = {
export type GenericAuthHelper<AuthSession> = {
refreshSession(): Promise<AuthSession>;
removeSession(): Promise<void>;
createSession(scope: OAuthScopes): Promise<AuthSession>;
createSession(scopes: Set<string>): Promise<AuthSession>;
};
export class AuthHelper<AuthSession> implements AuthHelper<AuthSession> {
@@ -59,12 +55,12 @@ export class AuthHelper<AuthSession> implements AuthHelper<AuthSession> {
environment,
provider,
oauthRequestApi,
sessionTransform = (id) => id,
sessionTransform = id => id,
} = options;
this.authRequester = oauthRequestApi.createAuthRequester({
provider,
onAuthRequest: (scopes) => this.showPopup(scopes.toString()),
onAuthRequest: scopes => this.showPopup([...scopes].join(' ')),
});
this.apiOrigin = apiOrigin;
@@ -95,7 +91,7 @@ export class AuthHelper<AuthSession> implements AuthHelper<AuthSession> {
'x-requested-with': 'XMLHttpRequest',
},
credentials: 'include',
}).catch((error) => {
}).catch(error => {
throw new Error(`Auth refresh request failed, ${error}`);
});
@@ -133,8 +129,8 @@ export class AuthHelper<AuthSession> implements AuthHelper<AuthSession> {
}
}
async createSession(scope: OAuthScopes): Promise<AuthSession> {
return this.authRequester(scope);
async createSession(scopes: Set<string>): Promise<AuthSession> {
return this.authRequester(scopes);
}
private async showPopup(scope: string): Promise<AuthSession> {