packages/core: refactor GoogleAuthHelper into generic helper and move to lib
This commit is contained in:
@@ -14,7 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { AuthHelper } from './GoogleAuthHelper';
|
||||
import GoogleIcon from '@material-ui/icons/AcUnit';
|
||||
import { AuthHelper } from '../../lib/AuthHelper';
|
||||
import GoogleScopes from './GoogleScopes';
|
||||
import { GoogleSession } from './types';
|
||||
import { OAuthScopes } from '../../..';
|
||||
@@ -23,11 +24,42 @@ import {
|
||||
OpenIdConnectApi,
|
||||
IdTokenOptions,
|
||||
} from '../../../definitions/auth';
|
||||
import { OAuthRequestApi } from '../../../definitions';
|
||||
import { GenericAuthHelper } from '../../lib/AuthHelper/AuthHelper';
|
||||
|
||||
export type GoogleAuthResponse = {
|
||||
accessToken: string;
|
||||
idToken: string;
|
||||
scopes: string;
|
||||
expiresInSeconds: number;
|
||||
};
|
||||
|
||||
class GoogleAuth implements OAuthApi, OpenIdConnectApi {
|
||||
private currentSession: GoogleSession | undefined;
|
||||
|
||||
constructor(private readonly helper: AuthHelper) {}
|
||||
static create(oauthRequestApi: OAuthRequestApi) {
|
||||
const helper = new AuthHelper({
|
||||
providerPath: 'google/',
|
||||
environment: 'dev',
|
||||
provider: {
|
||||
title: 'Google',
|
||||
icon: GoogleIcon,
|
||||
},
|
||||
oauthRequestApi: oauthRequestApi,
|
||||
sessionTransform(res: GoogleAuthResponse): GoogleSession {
|
||||
return {
|
||||
idToken: res.idToken,
|
||||
accessToken: res.accessToken,
|
||||
scopes: GoogleScopes.from(res.scopes),
|
||||
expiresAt: new Date(Date.now() + res.expiresInSeconds * 1000),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
return new GoogleAuth(helper);
|
||||
}
|
||||
|
||||
constructor(private readonly helper: GenericAuthHelper<GoogleSession>) {}
|
||||
|
||||
async getAccessToken(scope?: string | string[]) {
|
||||
const session = await this.getSession({ optional: false, scope });
|
||||
@@ -129,7 +161,7 @@ class GoogleAuth implements OAuthApi, OpenIdConnectApi {
|
||||
if (scope) {
|
||||
newScope = newScope.extend(scope);
|
||||
}
|
||||
return newScope.toString();
|
||||
return newScope;
|
||||
}
|
||||
}
|
||||
export default GoogleAuth;
|
||||
|
||||
+35
-28
@@ -14,13 +14,30 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import GoogleAuthHelper from './GoogleAuthHelper';
|
||||
import GoogleScopes from './GoogleScopes';
|
||||
import ProviderIcon from '@material-ui/icons/AcUnit';
|
||||
import { AuthHelper } from './AuthHelper';
|
||||
import MockOAuthApi from '../../OAuthRequestManager/MockOAuthApi';
|
||||
import { BasicOAuthScopes } from '../../OAuthRequestManager/BasicOAuthScopes';
|
||||
|
||||
const anyFetch = fetch as any;
|
||||
|
||||
describe('GoogleAuthHelper', () => {
|
||||
const defaultOptions = {
|
||||
apiOrigin: 'my-origin',
|
||||
providerPath: 'my-provider',
|
||||
environment: 'production',
|
||||
provider: {
|
||||
title: 'My Provider',
|
||||
icon: ProviderIcon,
|
||||
},
|
||||
oauthRequestApi: new MockOAuthApi(),
|
||||
sessionTransform: ({ expiresInSeconds, ...res }: any) => ({
|
||||
...res,
|
||||
scopes: BasicOAuthScopes.from(res.scopes),
|
||||
expiresAt: new Date(Date.now() + expiresInSeconds * 1000),
|
||||
}),
|
||||
};
|
||||
|
||||
describe('AuthHelper', () => {
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
anyFetch.resetMocks();
|
||||
@@ -36,10 +53,7 @@ describe('GoogleAuthHelper', () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const helper = new GoogleAuthHelper(
|
||||
{ apiOrigin: 'my-origin', dev: false },
|
||||
new MockOAuthApi(),
|
||||
);
|
||||
const helper = new AuthHelper<any>(defaultOptions);
|
||||
const session = await helper.refreshSession();
|
||||
expect(session.idToken).toBe('mock-id-token');
|
||||
expect(session.accessToken).toBe('mock-access-token');
|
||||
@@ -51,10 +65,7 @@ describe('GoogleAuthHelper', () => {
|
||||
it('should handle failure to refresh session', async () => {
|
||||
anyFetch.mockRejectOnce(new Error('Network NOPE'));
|
||||
|
||||
const helper = new GoogleAuthHelper(
|
||||
{ apiOrigin: 'my-origin', dev: true },
|
||||
new MockOAuthApi(),
|
||||
);
|
||||
const helper = new AuthHelper(defaultOptions);
|
||||
await expect(helper.refreshSession()).rejects.toThrow(
|
||||
'Auth refresh request failed, Error: Network NOPE',
|
||||
);
|
||||
@@ -63,10 +74,7 @@ describe('GoogleAuthHelper', () => {
|
||||
it('should handle failure response when refreshing session', async () => {
|
||||
anyFetch.mockResponseOnce({}, { status: 401, statusText: 'NOPE' });
|
||||
|
||||
const helper = new GoogleAuthHelper(
|
||||
{ apiOrigin: 'my-origin', dev: false },
|
||||
new MockOAuthApi(),
|
||||
);
|
||||
const helper = new AuthHelper(defaultOptions);
|
||||
await expect(helper.refreshSession()).rejects.toThrow(
|
||||
'Auth refresh request failed with status NOPE',
|
||||
);
|
||||
@@ -74,11 +82,11 @@ describe('GoogleAuthHelper', () => {
|
||||
|
||||
it('should fail if popup was rejected', async () => {
|
||||
const mockOauth = new MockOAuthApi();
|
||||
const helper = new GoogleAuthHelper(
|
||||
{ apiOrigin: 'my-origin', dev: false },
|
||||
mockOauth,
|
||||
);
|
||||
const promise = helper.createSession('a b');
|
||||
const helper = new AuthHelper({
|
||||
...defaultOptions,
|
||||
oauthRequestApi: mockOauth,
|
||||
});
|
||||
const promise = helper.createSession(BasicOAuthScopes.from('a b'));
|
||||
await mockOauth.rejectAll();
|
||||
await expect(promise).rejects.toMatchObject({ name: 'RejectedError' });
|
||||
});
|
||||
@@ -91,25 +99,24 @@ describe('GoogleAuthHelper', () => {
|
||||
expiresInSeconds: 3600,
|
||||
});
|
||||
const popupSpy = jest.spyOn(mockOauth, 'showLoginPopup');
|
||||
const helper = new GoogleAuthHelper(
|
||||
{ apiOrigin: 'my-origin', dev: false },
|
||||
mockOauth,
|
||||
);
|
||||
const helper = new AuthHelper({
|
||||
...defaultOptions,
|
||||
oauthRequestApi: mockOauth,
|
||||
});
|
||||
|
||||
const sessionPromise = helper.createSession('a b');
|
||||
const sessionPromise = helper.createSession(BasicOAuthScopes.from('a b'));
|
||||
|
||||
await mockOauth.triggerAll();
|
||||
|
||||
expect(popupSpy).toBeCalledTimes(1);
|
||||
expect(popupSpy.mock.calls[0][0]).toMatchObject({
|
||||
url:
|
||||
'my-origin/api/backend/auth/start?scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fa%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fb',
|
||||
url: 'my-origin/api/auth/my-provider/start?scope=a%20b&env=production',
|
||||
});
|
||||
|
||||
await expect(sessionPromise).resolves.toEqual({
|
||||
idToken: 'my-id-token',
|
||||
accessToken: 'my-access-token',
|
||||
scopes: expect.any(GoogleScopes),
|
||||
scopes: expect.any(BasicOAuthScopes),
|
||||
expiresAt: expect.any(Date),
|
||||
});
|
||||
});
|
||||
+60
-61
@@ -14,59 +14,69 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import GoogleIcon from '@material-ui/icons/AcUnit';
|
||||
import GoogleScopes from './GoogleScopes';
|
||||
import { GoogleSession } from './types';
|
||||
import { AuthRequester, OAuthRequestApi } from '../../..';
|
||||
import { AuthRequester } from '../../..';
|
||||
import {
|
||||
OAuthRequestApi,
|
||||
AuthProvider,
|
||||
OAuthScopes,
|
||||
} from '../../../definitions';
|
||||
|
||||
const API_PATH = '/api/backend/auth';
|
||||
const DEFAULT_BASE_PATH = '/api/auth/';
|
||||
|
||||
type Options = {
|
||||
apiOrigin: string;
|
||||
dev: boolean;
|
||||
type Options<AuthSession> = {
|
||||
apiOrigin?: string;
|
||||
basePath?: string;
|
||||
providerPath: string;
|
||||
environment: string;
|
||||
provider: AuthProvider;
|
||||
oauthRequestApi: OAuthRequestApi;
|
||||
sessionTransform?(response: any): AuthSession | Promise<AuthSession>;
|
||||
};
|
||||
|
||||
export type GoogleAuthResponse = {
|
||||
accessToken: string;
|
||||
idToken: string;
|
||||
scopes: string;
|
||||
expiresInSeconds: number;
|
||||
};
|
||||
|
||||
export type AuthHelper = {
|
||||
refreshSession(): Promise<GoogleSession>;
|
||||
export type GenericAuthHelper<AuthSession> = {
|
||||
refreshSession(): Promise<AuthSession>;
|
||||
removeSession(): Promise<void>;
|
||||
createSession(scope: string): Promise<GoogleSession>;
|
||||
createSession(scope: OAuthScopes): Promise<AuthSession>;
|
||||
};
|
||||
|
||||
class GoogleAuthHelper implements AuthHelper {
|
||||
private readonly authRequester: AuthRequester<GoogleSession>;
|
||||
private refreshPromise?: Promise<GoogleSession>;
|
||||
export class AuthHelper<AuthSession> implements AuthHelper<AuthSession> {
|
||||
private readonly apiOrigin: string;
|
||||
private readonly basePath: string;
|
||||
private readonly providerPath: string;
|
||||
private readonly environment: string;
|
||||
private readonly provider: AuthProvider;
|
||||
private readonly oauthRequestApi: OAuthRequestApi;
|
||||
private readonly authRequester: AuthRequester<AuthSession>;
|
||||
private readonly sessionTransform: (response: any) => Promise<AuthSession>;
|
||||
|
||||
static create(oauthRequest: OAuthRequestApi) {
|
||||
return new GoogleAuthHelper(
|
||||
{
|
||||
apiOrigin: window.location.origin,
|
||||
dev: process.env.NODE_ENV === 'development',
|
||||
},
|
||||
oauthRequest,
|
||||
);
|
||||
}
|
||||
private refreshPromise?: Promise<AuthSession>;
|
||||
|
||||
constructor(
|
||||
private readonly options: Options,
|
||||
private readonly oauth: OAuthRequestApi,
|
||||
) {
|
||||
this.authRequester = oauth.createAuthRequester({
|
||||
provider: {
|
||||
title: 'Google',
|
||||
icon: GoogleIcon,
|
||||
},
|
||||
constructor(options: Options<AuthSession>) {
|
||||
const {
|
||||
apiOrigin = window.location.origin,
|
||||
basePath = DEFAULT_BASE_PATH,
|
||||
providerPath,
|
||||
environment,
|
||||
provider,
|
||||
oauthRequestApi,
|
||||
sessionTransform = (id) => id,
|
||||
} = options;
|
||||
|
||||
this.authRequester = oauthRequestApi.createAuthRequester({
|
||||
provider,
|
||||
onAuthRequest: (scopes) => this.showPopup(scopes.toString()),
|
||||
});
|
||||
|
||||
this.apiOrigin = apiOrigin;
|
||||
this.basePath = basePath;
|
||||
this.providerPath = providerPath;
|
||||
this.environment = environment;
|
||||
this.provider = provider;
|
||||
this.oauthRequestApi = oauthRequestApi;
|
||||
this.sessionTransform = sessionTransform;
|
||||
}
|
||||
|
||||
async refreshSession(): Promise<GoogleSession> {
|
||||
async refreshSession(): Promise<AuthSession> {
|
||||
if (this.refreshPromise) {
|
||||
return this.refreshPromise;
|
||||
}
|
||||
@@ -107,7 +117,7 @@ class GoogleAuthHelper implements AuthHelper {
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
return GoogleAuthHelper.convertAuthInfo(authInfo);
|
||||
return await this.sessionTransform(authInfo);
|
||||
}
|
||||
|
||||
async removeSession(): Promise<void> {
|
||||
@@ -124,22 +134,22 @@ class GoogleAuthHelper implements AuthHelper {
|
||||
}
|
||||
}
|
||||
|
||||
async createSession(scope: string): Promise<GoogleSession> {
|
||||
return this.authRequester(GoogleScopes.from(scope));
|
||||
async createSession(scope: OAuthScopes): Promise<AuthSession> {
|
||||
return this.authRequester(scope);
|
||||
}
|
||||
|
||||
private async showPopup(scope: string): Promise<GoogleSession> {
|
||||
private async showPopup(scope: string): Promise<AuthSession> {
|
||||
const popupUrl = this.buildUrl('/start', { scope });
|
||||
|
||||
const payload = await this.oauth.showLoginPopup({
|
||||
const payload = await this.oauthRequestApi.showLoginPopup({
|
||||
url: popupUrl,
|
||||
name: 'google-login',
|
||||
origin: this.options.apiOrigin,
|
||||
name: `${this.provider.title} Login`,
|
||||
origin: this.apiOrigin,
|
||||
width: 450,
|
||||
height: 730,
|
||||
});
|
||||
|
||||
return GoogleAuthHelper.convertAuthInfo(payload);
|
||||
return await this.sessionTransform(payload);
|
||||
}
|
||||
|
||||
private buildUrl(
|
||||
@@ -148,10 +158,10 @@ class GoogleAuthHelper implements AuthHelper {
|
||||
): string {
|
||||
const queryString = this.buildQueryString({
|
||||
...query,
|
||||
dev: this.options.dev,
|
||||
env: this.environment,
|
||||
});
|
||||
|
||||
return `${this.options.apiOrigin}${API_PATH}${path}${queryString}`;
|
||||
return `${this.apiOrigin}${this.basePath}${this.providerPath}${path}${queryString}`;
|
||||
}
|
||||
|
||||
private buildQueryString(query?: {
|
||||
@@ -178,15 +188,4 @@ class GoogleAuthHelper implements AuthHelper {
|
||||
}
|
||||
return `?${queryString}`;
|
||||
}
|
||||
|
||||
private static convertAuthInfo(authInfo: GoogleAuthResponse): GoogleSession {
|
||||
return {
|
||||
idToken: authInfo.idToken,
|
||||
accessToken: authInfo.accessToken,
|
||||
scopes: GoogleScopes.from(authInfo.scopes),
|
||||
expiresAt: new Date(Date.now() + authInfo.expiresInSeconds * 1000),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default GoogleAuthHelper;
|
||||
+5
-6
@@ -14,23 +14,22 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import GoogleScopes from './GoogleScopes';
|
||||
import MockAuthHelper, { mockIdToken, mockAccessToken } from './MockAuthHelper';
|
||||
import MockAuthHelper, { mockAccessToken } from './MockAuthHelper';
|
||||
|
||||
describe('MockAuthHelper', () => {
|
||||
it('should return mock tokens', async () => {
|
||||
const helper = new MockAuthHelper();
|
||||
|
||||
await expect(helper.createSession()).resolves.toEqual({
|
||||
idToken: mockIdToken,
|
||||
accessToken: mockAccessToken,
|
||||
expiresAt: expect.any(Date),
|
||||
scopes: expect.any(GoogleScopes),
|
||||
scopes: expect.any(String),
|
||||
});
|
||||
|
||||
await expect(helper.refreshSession()).resolves.toEqual({
|
||||
idToken: mockIdToken,
|
||||
accessToken: mockAccessToken,
|
||||
expiresAt: expect.any(Date),
|
||||
scopes: expect.any(GoogleScopes),
|
||||
scopes: expect.any(String),
|
||||
});
|
||||
});
|
||||
});
|
||||
+13
-22
@@ -14,24 +14,24 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import GoogleScopes from './GoogleScopes';
|
||||
import { GoogleSession } from './types';
|
||||
import { AuthHelper } from './GoogleAuthHelper';
|
||||
import { GenericAuthHelper } from './AuthHelper';
|
||||
|
||||
export const mockIdToken = 'mock-id-token';
|
||||
export const mockAccessToken = 'mock-access-token';
|
||||
|
||||
const defaultMockSession: GoogleSession = {
|
||||
idToken: mockIdToken,
|
||||
accessToken: mockAccessToken,
|
||||
expiresAt: new Date(),
|
||||
scopes: GoogleScopes.default(),
|
||||
type MockSession = {
|
||||
accessToken: string;
|
||||
expiresAt: Date;
|
||||
scopes: string;
|
||||
};
|
||||
|
||||
export default class MockAuthHelper implements AuthHelper {
|
||||
constructor(
|
||||
private readonly mockSession: GoogleSession = defaultMockSession,
|
||||
) {}
|
||||
const defaultMockSession: MockSession = {
|
||||
accessToken: mockAccessToken,
|
||||
expiresAt: new Date(),
|
||||
scopes: 'profile email',
|
||||
};
|
||||
|
||||
export default class MockAuthHelper implements GenericAuthHelper<MockSession> {
|
||||
constructor(private readonly mockSession: MockSession = defaultMockSession) {}
|
||||
|
||||
async refreshSession() {
|
||||
return this.mockSession;
|
||||
@@ -42,13 +42,4 @@ export default class MockAuthHelper implements AuthHelper {
|
||||
async createSession() {
|
||||
return this.mockSession;
|
||||
}
|
||||
|
||||
async showPopup(scope: string) {
|
||||
return {
|
||||
scopes: GoogleScopes.from(scope),
|
||||
idToken: 'i',
|
||||
accessToken: 'a',
|
||||
expiresAt: new Date(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { AuthHelper } from './AuthHelper';
|
||||
Reference in New Issue
Block a user