@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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 express from 'express';
|
||||
import passport from 'passport';
|
||||
import { AuthProvider, AuthProviderRouteHandlers } from './types';
|
||||
import { ProviderFactories } from './factories';
|
||||
|
||||
class MyAuthProvider implements AuthProvider, AuthProviderRouteHandlers {
|
||||
strategy(): passport.Strategy {
|
||||
return new passport.Strategy();
|
||||
}
|
||||
async start(_: express.Request, res: express.Response): Promise<any> {
|
||||
res.send('start');
|
||||
}
|
||||
async frameHandler(_: express.Request, res: express.Response): Promise<any> {
|
||||
res.send('frameHandler');
|
||||
}
|
||||
async logout(_: express.Request, res: express.Response): Promise<any> {
|
||||
res.send('logout');
|
||||
}
|
||||
}
|
||||
|
||||
describe('getProviderFactory', () => {
|
||||
it('makes a provider for MyAuthProvider', () => {
|
||||
jest
|
||||
.spyOn(ProviderFactories, 'getProviderFactory')
|
||||
.mockReturnValueOnce(MyAuthProvider);
|
||||
const provider = ProviderFactories.getProviderFactory('a');
|
||||
expect(provider).toBeDefined();
|
||||
});
|
||||
|
||||
it('throws an error when provider implementation does not exist', () => {
|
||||
expect(() => {
|
||||
ProviderFactories.getProviderFactory('b');
|
||||
}).toThrow('Provider Implementation missing for : b auth provider');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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 { AuthProviderFactories, AuthProviderFactory } from './types';
|
||||
import { GoogleAuthProvider } from './google/provider';
|
||||
|
||||
export class ProviderFactories {
|
||||
private static readonly providerFactories: AuthProviderFactories = {
|
||||
google: GoogleAuthProvider,
|
||||
};
|
||||
|
||||
public static getProviderFactory(providerId: string): AuthProviderFactory {
|
||||
const ProviderImpl = ProviderFactories.providerFactories[providerId];
|
||||
if (!ProviderImpl) {
|
||||
throw Error(
|
||||
`Provider Implementation missing for : ${providerId} auth provider`,
|
||||
);
|
||||
}
|
||||
return ProviderImpl;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* 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 { GoogleAuthProvider } from './provider';
|
||||
import passport from 'passport';
|
||||
import express from 'express';
|
||||
import * as utils from './../utils';
|
||||
|
||||
const googleAuthProviderConfig = {
|
||||
provider: 'google',
|
||||
options: {
|
||||
clientID: 'a',
|
||||
clientSecret: 'b',
|
||||
callbackURL: 'c',
|
||||
},
|
||||
};
|
||||
|
||||
const googleAuthProviderConfigInvalidOptions = {
|
||||
provider: 'google',
|
||||
options: {},
|
||||
};
|
||||
|
||||
describe('GoogleAuthProvider', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('create a new provider', () => {
|
||||
it('should succeed with valid config', () => {
|
||||
const googleAuthProvider = new GoogleAuthProvider(
|
||||
googleAuthProviderConfig,
|
||||
);
|
||||
expect(googleAuthProvider).toBeDefined();
|
||||
expect(googleAuthProvider.start).toBeDefined();
|
||||
expect(googleAuthProvider.logout).toBeDefined();
|
||||
expect(googleAuthProvider.frameHandler).toBeDefined();
|
||||
expect(googleAuthProvider.strategy).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('start authentication handler', () => {
|
||||
const mockResponse: any = ({} as unknown) as express.Response;
|
||||
const mockNext: express.NextFunction = jest.fn();
|
||||
|
||||
it('should initiate authenticate request with provided scopes', () => {
|
||||
const mockRequest = ({
|
||||
query: {
|
||||
scope: 'a,b',
|
||||
},
|
||||
} as unknown) as express.Request;
|
||||
|
||||
const spyPassport = jest
|
||||
.spyOn(passport, 'authenticate')
|
||||
.mockImplementation(() => jest.fn());
|
||||
|
||||
const googleAuthProvider = new GoogleAuthProvider(
|
||||
googleAuthProviderConfig,
|
||||
);
|
||||
googleAuthProvider.start(mockRequest, mockResponse, mockNext);
|
||||
expect(spyPassport).toBeCalledTimes(1);
|
||||
expect(spyPassport).toBeCalledWith('google', {
|
||||
scope: 'a,b',
|
||||
accessType: 'offline',
|
||||
prompt: 'consent',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw error if no scopes provided', () => {
|
||||
const mockRequest = ({
|
||||
query: {},
|
||||
} as unknown) as express.Request;
|
||||
|
||||
const googleAuthProvider = new GoogleAuthProvider(
|
||||
googleAuthProviderConfig,
|
||||
);
|
||||
expect(() => {
|
||||
googleAuthProvider.start(mockRequest, mockResponse, mockNext);
|
||||
}).toThrowError('missing scope parameter');
|
||||
});
|
||||
});
|
||||
|
||||
describe('logout handler', () => {
|
||||
const mockRequest = ({} as unknown) as express.Request;
|
||||
|
||||
it('should perform logout and respond with 200', () => {
|
||||
const mockResponse: any = ({
|
||||
send: jest.fn(),
|
||||
} as unknown) as express.Response;
|
||||
|
||||
const googleAuthProvider = new GoogleAuthProvider(
|
||||
googleAuthProviderConfig,
|
||||
);
|
||||
|
||||
const spyResponse = jest
|
||||
.spyOn(mockResponse, 'send')
|
||||
.mockImplementation(() => jest.fn());
|
||||
|
||||
googleAuthProvider.logout(mockRequest, mockResponse);
|
||||
expect(spyResponse).toBeCalledTimes(1);
|
||||
expect(spyResponse).toBeCalledWith('logout!');
|
||||
});
|
||||
});
|
||||
|
||||
describe('redirect frame handler', () => {
|
||||
const mockRequest = ({} as unknown) as express.Request;
|
||||
const mockResponse: any = ({
|
||||
send: jest.fn(),
|
||||
} as unknown) as express.Response;
|
||||
const mockNext: express.NextFunction = jest.fn();
|
||||
|
||||
it('should call authenticate and post a response ', () => {
|
||||
const spyPostMessage = jest
|
||||
.spyOn(utils, 'postMessageResponse')
|
||||
.mockImplementation(() => jest.fn());
|
||||
|
||||
const spyPassport = jest
|
||||
.spyOn(passport, 'authenticate')
|
||||
.mockImplementation(() => jest.fn());
|
||||
|
||||
const googleAuthProvider = new GoogleAuthProvider(
|
||||
googleAuthProviderConfig,
|
||||
);
|
||||
|
||||
googleAuthProvider.frameHandler(mockRequest, mockResponse, mockNext);
|
||||
const callbackFunc = spyPassport.mock.calls[0][1] as Function;
|
||||
callbackFunc();
|
||||
expect(spyPassport).toBeCalledTimes(1);
|
||||
expect(spyPostMessage).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('strategy handler', () => {
|
||||
it('should return a valid passport strategy', () => {
|
||||
const googleAuthProvider = new GoogleAuthProvider(
|
||||
googleAuthProviderConfig,
|
||||
);
|
||||
|
||||
expect(googleAuthProvider.strategy()).toBeInstanceOf(passport.Strategy);
|
||||
});
|
||||
|
||||
it('should throw an error for invalid options', () => {
|
||||
expect(() => {
|
||||
const googleAuthProvider = new GoogleAuthProvider(
|
||||
googleAuthProviderConfigInvalidOptions,
|
||||
);
|
||||
googleAuthProvider.strategy();
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -61,14 +61,12 @@ export class GoogleAuthProvider
|
||||
})(req, res, next);
|
||||
}
|
||||
|
||||
logout(_req: express.Request, res: express.Response) {
|
||||
return new Promise((resolve) => {
|
||||
res.send('logout!');
|
||||
resolve();
|
||||
});
|
||||
async logout(_req: express.Request, res: express.Response) {
|
||||
res.send('logout!');
|
||||
}
|
||||
|
||||
strategy(): passport.Strategy {
|
||||
// TODO: throw error if env variables not set?
|
||||
return new GoogleStrategy(
|
||||
{ ...this.providerConfig.options },
|
||||
(
|
||||
@@ -76,9 +74,9 @@ export class GoogleAuthProvider
|
||||
refreshToken: any,
|
||||
params: any,
|
||||
profile: any,
|
||||
cb: any,
|
||||
done: any,
|
||||
) => {
|
||||
cb(undefined, {
|
||||
done(undefined, {
|
||||
profile,
|
||||
idToken: params.id_token,
|
||||
accessToken,
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* 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 passport from 'passport';
|
||||
import express from 'express';
|
||||
import { makeProvider, defaultRouter } from '.';
|
||||
import {
|
||||
AuthProvider,
|
||||
AuthProviderRouteHandlers,
|
||||
AuthProviderConfig,
|
||||
} from './types';
|
||||
import * as passportGoogleOAuth20 from 'passport-google-oauth20';
|
||||
import { ProviderFactories } from './factories';
|
||||
|
||||
class MyAuthProvider implements AuthProvider, AuthProviderRouteHandlers {
|
||||
private readonly providerConfig: AuthProviderConfig;
|
||||
constructor(providerConfig: AuthProviderConfig) {
|
||||
this.providerConfig = providerConfig;
|
||||
}
|
||||
|
||||
strategy(): passport.Strategy {
|
||||
return new passportGoogleOAuth20.Strategy(
|
||||
this.providerConfig.options,
|
||||
() => {},
|
||||
);
|
||||
}
|
||||
async start(_: express.Request, res: express.Response): Promise<any> {
|
||||
res.send('start');
|
||||
}
|
||||
async frameHandler(_: express.Request, res: express.Response): Promise<any> {
|
||||
res.send('frameHandler');
|
||||
}
|
||||
async logout(_: express.Request, res: express.Response): Promise<any> {
|
||||
res.send('logout');
|
||||
}
|
||||
}
|
||||
|
||||
class MyAuthProviderWithRefresh extends MyAuthProvider {
|
||||
async refresh(_: express.Request, res: express.Response): Promise<any> {
|
||||
res.send('logout');
|
||||
}
|
||||
}
|
||||
|
||||
const providerConfig = {
|
||||
provider: 'a',
|
||||
options: {
|
||||
clientID: 'somevalue',
|
||||
},
|
||||
};
|
||||
|
||||
const providerConfigInvalid = {
|
||||
provider: 'b',
|
||||
options: {
|
||||
clientID: 'somevalue',
|
||||
},
|
||||
};
|
||||
|
||||
describe('makeProvider', () => {
|
||||
it('makes a provider for Myauthprovider', () => {
|
||||
jest
|
||||
.spyOn(ProviderFactories, 'getProviderFactory')
|
||||
.mockReturnValueOnce(MyAuthProvider);
|
||||
const provider = makeProvider(providerConfig);
|
||||
expect(provider.providerId).toEqual('a');
|
||||
expect(provider.strategy).toBeDefined();
|
||||
expect(provider.providerRouter).toBeDefined();
|
||||
});
|
||||
|
||||
it('throws an error when provider implementation does not exist', () => {
|
||||
expect(() => {
|
||||
makeProvider(providerConfigInvalid);
|
||||
}).toThrow('Provider Implementation missing for : b auth provider');
|
||||
});
|
||||
});
|
||||
|
||||
describe('defaultRouter', () => {
|
||||
it('make router for auth provider without refresh', () => {
|
||||
expect(
|
||||
defaultRouter(new MyAuthProvider({ provider: 'a', options: {} })),
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
it('make router for auth provider with refresh', () => {
|
||||
expect(
|
||||
defaultRouter(
|
||||
new MyAuthProviderWithRefresh({ provider: 'b', options: {} }),
|
||||
),
|
||||
).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -15,17 +15,8 @@
|
||||
*/
|
||||
|
||||
import Router from 'express-promise-router';
|
||||
import {
|
||||
AuthProviderRouteHandlers,
|
||||
AuthProviderFactories,
|
||||
AuthProviderConfig,
|
||||
} from './types';
|
||||
|
||||
import { GoogleAuthProvider } from './google/provider';
|
||||
|
||||
const providerFactories: AuthProviderFactories = {
|
||||
google: GoogleAuthProvider,
|
||||
};
|
||||
import { AuthProviderRouteHandlers, AuthProviderConfig } from './types';
|
||||
import { ProviderFactories } from './factories';
|
||||
|
||||
export const defaultRouter = (provider: AuthProviderRouteHandlers) => {
|
||||
const router = Router();
|
||||
@@ -40,10 +31,7 @@ export const defaultRouter = (provider: AuthProviderRouteHandlers) => {
|
||||
|
||||
export const makeProvider = (config: AuthProviderConfig) => {
|
||||
const providerId = config.provider;
|
||||
const ProviderImpl = providerFactories[providerId];
|
||||
if (!ProviderImpl) {
|
||||
throw Error(`Provider Implementation missing for provider: ${providerId}`);
|
||||
}
|
||||
const ProviderImpl = ProviderFactories.getProviderFactory(providerId);
|
||||
const providerInstance = new ProviderImpl(config);
|
||||
const strategy = providerInstance.strategy();
|
||||
const providerRouter = defaultRouter(providerInstance);
|
||||
|
||||
@@ -51,9 +51,11 @@ export interface AuthProviderRouteHandlers {
|
||||
}
|
||||
|
||||
export type AuthProviderFactories = {
|
||||
[key: string]: {
|
||||
new (providerConfig: any): AuthProvider & AuthProviderRouteHandlers;
|
||||
};
|
||||
[key: string]: AuthProviderFactory;
|
||||
};
|
||||
|
||||
export type AuthProviderFactory = {
|
||||
new (providerConfig: any): AuthProvider & AuthProviderRouteHandlers;
|
||||
};
|
||||
|
||||
export type AuthInfo = {
|
||||
|
||||
Reference in New Issue
Block a user