diff --git a/packages/core/src/api/apis/definitions/OAuthRequestApi.ts b/packages/core/src/api/apis/definitions/OAuthRequestApi.ts index a6441e9d1a..52d30a763e 100644 --- a/packages/core/src/api/apis/definitions/OAuthRequestApi.ts +++ b/packages/core/src/api/apis/definitions/OAuthRequestApi.ts @@ -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; - 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 = { * Implementation of the auth flow, which will be called synchronously when * trigger() is called on an auth requests. */ - onAuthRequest(scope: OAuthScopes): Promise; + onAuthRequest(scopes: Set): Promise; }; /** @@ -76,7 +64,7 @@ export type AuthRequesterOptions = { * union of all requested scopes. */ export type AuthRequester = ( - scope: OAuthScopes, + scopes: Set, ) => Promise; /** diff --git a/packages/core/src/api/apis/implementations/OAuthRequestManager/BasicOAuthScopes.ts b/packages/core/src/api/apis/implementations/OAuthRequestManager/BasicOAuthScopes.ts deleted file mode 100644 index fb022c5385..0000000000 --- a/packages/core/src/api/apis/implementations/OAuthRequestManager/BasicOAuthScopes.ts +++ /dev/null @@ -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, - 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 { - 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; - } -} diff --git a/packages/core/src/api/apis/implementations/OAuthRequestManager/MockOAuthApi.test.ts b/packages/core/src/api/apis/implementations/OAuthRequestManager/MockOAuthApi.test.ts index 61e466f200..32170acc6f 100644 --- a/packages/core/src/api/apis/implementations/OAuthRequestManager/MockOAuthApi.test.ts +++ b/packages/core/src/api/apis/implementations/OAuthRequestManager/MockOAuthApi.test.ts @@ -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( diff --git a/packages/core/src/api/apis/implementations/OAuthRequestManager/OAuthPendingRequests.test.ts b/packages/core/src/api/apis/implementations/OAuthRequestManager/OAuthPendingRequests.test.ts index be762ca936..36378798d4 100644 --- a/packages/core/src/api/apis/implementations/OAuthRequestManager/OAuthPendingRequests.test.ts +++ b/packages/core/src/api/apis/implementations/OAuthRequestManager/OAuthPendingRequests.test.ts @@ -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(); - 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(); 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); diff --git a/packages/core/src/api/apis/implementations/OAuthRequestManager/OAuthPendingRequests.ts b/packages/core/src/api/apis/implementations/OAuthRequestManager/OAuthPendingRequests.ts index e8dd0e12ef..6ee1674b39 100644 --- a/packages/core/src/api/apis/implementations/OAuthRequestManager/OAuthPendingRequests.ts +++ b/packages/core/src/api/apis/implementations/OAuthRequestManager/OAuthPendingRequests.ts @@ -14,22 +14,50 @@ * limitations under the License. */ -import { OAuthScopes } from '../../definitions'; import { BehaviorSubject } from '../lib'; import { Observable } from '../../../types'; type RequestQueueEntry = { - scopes: OAuthScopes; + scopes: Set; resolve: (value?: ResultType | PromiseLike | undefined) => void; reject: (reason: Error) => void; }; export type PendingRequest = { - scopes: OAuthScopes | undefined; + scopes: Set | undefined; resolve: (value: ResultType) => void; reject: (reason: Error) => void; }; +export function hasScopes( + searched: Set, + searchFor: Set, +): boolean { + for (const scope of searchFor) { + if (!searched.has(scope)) { + return false; + } + } + return true; +} + +export function joinScopes( + scopes: Set, + ...moreScopess: Set[] +): Set { + 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 { this.getCurrentPending(), ); - request(scopes: OAuthScopes): Promise { + request(scopes: Set): Promise { return new Promise((resolve, reject) => { this.requests.push({ scopes, resolve, reject }); @@ -49,9 +77,9 @@ export class OAuthPendingRequests { }); } - resolve(scopes: OAuthScopes, result: ResultType): void { - this.requests = this.requests.filter((request) => { - if (scopes.hasScopes(request.scopes)) { + resolve(scopes: Set, 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 { } 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 { : this.requests .slice(1) .reduce( - (acc, current) => acc.extend(current.scopes), + (acc, current) => joinScopes(acc, current.scopes), this.requests[0].scopes, ); diff --git a/packages/core/src/api/apis/implementations/OAuthRequestManager/OAuthRequestManager.test.ts b/packages/core/src/api/apis/implementations/OAuthRequestManager/OAuthRequestManager.test.ts index 922cb20a25..46a5362f35 100644 --- a/packages/core/src/api/apis/implementations/OAuthRequestManager/OAuthRequestManager.test.ts +++ b/packages/core/src/api/apis/implementations/OAuthRequestManager/OAuthRequestManager.test.ts @@ -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([ diff --git a/packages/core/src/api/apis/implementations/OAuthRequestManager/OAuthRequestManager.ts b/packages/core/src/api/apis/implementations/OAuthRequestManager/OAuthRequestManager.ts index 8b3b2d94c2..1168d6abd4 100644 --- a/packages/core/src/api/apis/implementations/OAuthRequestManager/OAuthRequestManager.ts +++ b/packages/core/src/api/apis/implementations/OAuthRequestManager/OAuthRequestManager.ts @@ -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); }; } diff --git a/packages/core/src/api/apis/implementations/auth/google/GoogleAuth.test.ts b/packages/core/src/api/apis/implementations/auth/google/GoogleAuth.test.ts index a6d2b082b2..b386806636 100644 --- a/packages/core/src/api/apis/implementations/auth/google/GoogleAuth.test.ts +++ b/packages/core/src/api/apis/implementations/auth/google/GoogleAuth.test.ts @@ -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); diff --git a/packages/core/src/api/apis/implementations/auth/google/GoogleAuth.ts b/packages/core/src/api/apis/implementations/auth/google/GoogleAuth.ts index efa5ef7079..1ae94ae9c6 100644 --- a/packages/core/src/api/apis/implementations/auth/google/GoogleAuth.ts +++ b/packages/core/src/api/apis/implementations/auth/google/GoogleAuth.ts @@ -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 { - 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, ): 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) { + 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 { + 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; diff --git a/packages/core/src/api/apis/implementations/auth/google/GoogleScopes.test.ts b/packages/core/src/api/apis/implementations/auth/google/GoogleScopes.test.ts deleted file mode 100644 index b4eec497bb..0000000000 --- a/packages/core/src/api/apis/implementations/auth/google/GoogleScopes.test.ts +++ /dev/null @@ -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()); - }); -}); diff --git a/packages/core/src/api/apis/implementations/auth/google/GoogleScopes.ts b/packages/core/src/api/apis/implementations/auth/google/GoogleScopes.ts deleted file mode 100644 index a29d392488..0000000000 --- a/packages/core/src/api/apis/implementations/auth/google/GoogleScopes.ts +++ /dev/null @@ -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) { - 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}`; - } -} diff --git a/packages/core/src/api/apis/implementations/auth/google/types.ts b/packages/core/src/api/apis/implementations/auth/google/types.ts index 55d9a7ecf2..96c69c5d4f 100644 --- a/packages/core/src/api/apis/implementations/auth/google/types.ts +++ b/packages/core/src/api/apis/implementations/auth/google/types.ts @@ -14,11 +14,9 @@ * limitations under the License. */ -import GoogleScopes from './GoogleScopes'; - export type GoogleSession = { idToken: string; accessToken: string; - scopes: GoogleScopes; + scopes: Set; expiresAt: Date; }; diff --git a/packages/core/src/api/apis/implementations/lib/AuthHelper/AuthHelper.test.ts b/packages/core/src/api/apis/implementations/lib/AuthHelper/AuthHelper.test.ts index 5ca95cb600..d62226679d 100644 --- a/packages/core/src/api/apis/implementations/lib/AuthHelper/AuthHelper.test.ts +++ b/packages/core/src/api/apis/implementations/lib/AuthHelper/AuthHelper.test.ts @@ -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), }); }); diff --git a/packages/core/src/api/apis/implementations/lib/AuthHelper/AuthHelper.ts b/packages/core/src/api/apis/implementations/lib/AuthHelper/AuthHelper.ts index dc79641e70..e1d9d12b31 100644 --- a/packages/core/src/api/apis/implementations/lib/AuthHelper/AuthHelper.ts +++ b/packages/core/src/api/apis/implementations/lib/AuthHelper/AuthHelper.ts @@ -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 = { export type GenericAuthHelper = { refreshSession(): Promise; removeSession(): Promise; - createSession(scope: OAuthScopes): Promise; + createSession(scopes: Set): Promise; }; export class AuthHelper implements AuthHelper { @@ -59,12 +55,12 @@ export class AuthHelper implements AuthHelper { 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 implements AuthHelper { '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 implements AuthHelper { } } - async createSession(scope: OAuthScopes): Promise { - return this.authRequester(scope); + async createSession(scopes: Set): Promise { + return this.authRequester(scopes); } private async showPopup(scope: string): Promise {