From 35d8043591279842a62d38781cfe7b916f9a2593 Mon Sep 17 00:00:00 2001 From: David Luecke Date: Sun, 21 Apr 2019 16:08:29 -0700 Subject: [PATCH] feat: Add authentication through oAuth redirect to authentication client (#1301) --- packages/authentication-client/src/core.ts | 90 ++++++++++--------- packages/authentication-client/src/index.ts | 23 +++-- packages/authentication-client/src/storage.ts | 49 ++++++++++ .../authentication-client/test/index.test.ts | 45 +++++++--- 4 files changed, 148 insertions(+), 59 deletions(-) create mode 100644 packages/authentication-client/src/storage.ts diff --git a/packages/authentication-client/src/core.ts b/packages/authentication-client/src/core.ts index 279cd984c1..2f7cb7ba76 100644 --- a/packages/authentication-client/src/core.ts +++ b/packages/authentication-client/src/core.ts @@ -1,38 +1,20 @@ import { NotAuthenticated } from '@feathersjs/errors'; import { Application } from '@feathersjs/feathers'; import { AuthenticationRequest, AuthenticationResult } from '@feathersjs/authentication'; +import { Storage, StorageWrapper } from './storage'; -export class Storage { - store: { [key: string]: any }; - - constructor () { - this.store = {}; - } - - getItem (key: string) { - return this.store[key]; - } - - setItem (key: string, value: any) { - return (this.store[key] = value); - } - - removeItem (key: string) { - delete this.store[key]; - return this; - } -} - -export type ClientConstructor = new (app: Application, options: AuthenticationClientOptions) => AuthenticationClient; +export type ClientConstructor = new (app: Application, options: AuthenticationClientOptions) + => AuthenticationClient; export interface AuthenticationClientOptions { - storage?: Storage; - header?: string; - scheme?: string; - storageKey?: string; - jwtStrategy?: string; - path?: string; - Authentication?: ClientConstructor; + storage: Storage; + header: string; + scheme: string; + storageKey: string; + locationKey: string; + jwtStrategy: string; + path: string; + Authentication: ClientConstructor; } export class AuthenticationClient { @@ -42,11 +24,12 @@ export class AuthenticationClient { constructor (app: Application, options: AuthenticationClientOptions) { const socket = app.io || app.primus; + const storage = new StorageWrapper(app.get('storage') || options.storage); this.app = app; - this.app.set('storage', this.app.get('storage') || options.storage); this.options = options; this.authenticated = false; + this.app.set('storage', storage); if (socket) { this.handleSocket(socket); @@ -58,7 +41,7 @@ export class AuthenticationClient { } get storage () { - return this.app.get('storage'); + return this.app.get('storage') as Storage; } handleSocket (socket: any) { @@ -70,21 +53,44 @@ export class AuthenticationClient { // has been called explicitly first if (this.authenticated) { // Force reauthentication with the server - this.reauthenticate(true); + this.reAuthenticate(true); } }); } setJwt (accessToken: string) { - return Promise.resolve(this.storage.setItem(this.options.storageKey, accessToken)); + return this.storage.setItem(this.options.storageKey, accessToken); + } + + getFromLocation (location: Location): Promise { + const regex = new RegExp(`(?:\&?)${this.options.locationKey}=([^&]*)`); + const type = location.hash ? 'hash' : 'search'; + const match = location[type] ? location[type].match(regex) : null; + + if (match !== null) { + const [ , value ] = match; + + location[type] = location[type].replace(regex, ''); + + return Promise.resolve(value); + } + + return Promise.resolve(null); } - getJwt () { - return Promise.resolve(this.storage.getItem(this.options.storageKey)); + getJwt (): Promise { + return this.storage.getItem(this.options.storageKey) + .then((accessToken: string) => { + if (!accessToken && typeof window !== 'undefined' && window.location) { + return this.getFromLocation(window.location); + } + + return accessToken || null; + }); } removeJwt () { - return Promise.resolve(this.storage.removeItem(this.options.storageKey)); + return this.storage.removeItem(this.options.storageKey); } reset () { @@ -94,7 +100,7 @@ export class AuthenticationClient { return Promise.resolve(null); } - reauthenticate (force: boolean = false): Promise { + reAuthenticate (force: boolean = false): Promise { // Either returns the authentication state or // tries to re-authenticate with the stored JWT and strategy const authPromise = this.app.get('authentication'); @@ -109,7 +115,9 @@ export class AuthenticationClient { strategy: this.options.jwtStrategy, accessToken }); - }).catch(error => this.removeJwt().then(() => Promise.reject(error))); + }).catch((error: Error) => + this.removeJwt().then(() => Promise.reject(error)) + ); } return authPromise; @@ -117,7 +125,7 @@ export class AuthenticationClient { authenticate (authentication: AuthenticationRequest): Promise { if (!authentication) { - return this.reauthenticate(); + return this.reAuthenticate(); } const promise = this.service.create(authentication) @@ -129,7 +137,9 @@ export class AuthenticationClient { this.app.emit('authenticated', authResult); return this.setJwt(accessToken).then(() => authResult); - }).catch((error: any) => this.reset().then(() => Promise.reject(error))); + }).catch((error: Error) => + this.reset().then(() => Promise.reject(error)) + ); this.app.set('authentication', promise); diff --git a/packages/authentication-client/src/index.ts b/packages/authentication-client/src/index.ts index b72025be4b..0e7496c328 100644 --- a/packages/authentication-client/src/index.ts +++ b/packages/authentication-client/src/index.ts @@ -1,7 +1,8 @@ -import { AuthenticationClient, Storage, AuthenticationClientOptions } from './core'; +import { AuthenticationClient, AuthenticationClientOptions } from './core'; import * as hooks from './hooks'; import { Application } from '@feathersjs/feathers'; import { AuthenticationResult, AuthenticationRequest } from '@feathersjs/authentication'; +import { Storage, MemoryStorage, StorageWrapper } from './storage'; declare module '@feathersjs/feathers' { interface Application { @@ -10,26 +11,31 @@ declare module '@feathersjs/feathers' { primus?: any; authentication: AuthenticationClient; authenticate (authentication?: AuthenticationRequest): Promise; - reauthenticate (force: boolean): Promise; + reAuthenticate (force: boolean): Promise; logout (): Promise; } } +export { AuthenticationClient, AuthenticationClientOptions, Storage, MemoryStorage, hooks }; + export type ClientConstructor = new (app: Application, options: AuthenticationClientOptions) => AuthenticationClient; +export const defaultStorage: Storage = typeof window !== 'undefined' && window.localStorage ? + new StorageWrapper(window.localStorage) : new MemoryStorage(); + export const defaults: AuthenticationClientOptions = { header: 'Authorization', scheme: 'Bearer', storageKey: 'feathers-jwt', + locationKey: 'access_token', jwtStrategy: 'jwt', path: '/authentication', - Authentication: AuthenticationClient + Authentication: AuthenticationClient, + storage: defaultStorage }; -const init = (_options: AuthenticationClientOptions = {}) => { - const options: AuthenticationClientOptions = Object.assign({}, { - storage: new Storage() - }, defaults, _options); +const init = (_options: Partial = {}) => { + const options: AuthenticationClientOptions = Object.assign({}, defaults, _options); const { Authentication } = options; return (app: Application) => { @@ -37,7 +43,7 @@ const init = (_options: AuthenticationClientOptions = {}) => { app.authentication = authentication; app.authenticate = authentication.authenticate.bind(authentication); - app.reauthenticate = authentication.reauthenticate.bind(authentication); + app.reAuthenticate = authentication.reAuthenticate.bind(authentication); app.logout = authentication.logout.bind(authentication); app.hooks({ @@ -51,7 +57,6 @@ const init = (_options: AuthenticationClientOptions = {}) => { }; }; -export { AuthenticationClient, AuthenticationClientOptions, Storage, hooks }; export default init; if (typeof module !== 'undefined') { diff --git a/packages/authentication-client/src/storage.ts b/packages/authentication-client/src/storage.ts new file mode 100644 index 0000000000..23c995c1b7 --- /dev/null +++ b/packages/authentication-client/src/storage.ts @@ -0,0 +1,49 @@ +export interface Storage { + getItem (key: string): Promise; + setItem? (key: string, value: any): Promise; + removeItem? (key: string): Promise; +} + +export class MemoryStorage implements Storage { + store: { [key: string]: any }; + + constructor () { + this.store = {}; + } + + getItem (key: string) { + return Promise.resolve(this.store[key]); + } + + setItem (key: string, value: any) { + return Promise.resolve(this.store[key] = value); + } + + removeItem (key: string) { + const value = this.store[key]; + + delete this.store[key]; + + return Promise.resolve(value); + } +} + +export class StorageWrapper implements Storage { + storage: any; + + constructor (storage: any) { + this.storage = storage; + } + + getItem (key: string) { + return Promise.resolve(this.storage.getItem(key)); + } + + setItem (key: string, value: any) { + return Promise.resolve(this.storage.setItem(key, value)); + } + + removeItem (key: string) { + return Promise.resolve(this.storage.removeItem(key)); + } +} diff --git a/packages/authentication-client/test/index.test.ts b/packages/authentication-client/test/index.test.ts index 72eafd232b..152756ad48 100644 --- a/packages/authentication-client/test/index.test.ts +++ b/packages/authentication-client/test/index.test.ts @@ -41,16 +41,41 @@ describe('@feathersjs/authentication-client', () => { assert.strictEqual(typeof app.logout, 'function'); }); - it('setJwt, getJwt, removeJwt', () => { + it('setJwt, getJwt, removeJwt', async () => { const auth = app.authentication; const token = 'hi'; - return auth.setJwt(token) - .then(() => auth.getJwt()) - .then(res => assert.strictEqual(res, token)) - .then(() => auth.removeJwt()) - .then(() => auth.getJwt()) - .then(res => assert.strictEqual(res, undefined)); + await auth.setJwt(token); + + const res = await auth.getJwt(); + + assert.strictEqual(res, token); + + await auth.removeJwt(); + assert.strictEqual(await auth.getJwt(), null); + }); + + it('getFromLocation', async () => { + const auth = app.authentication; + let dummyLocation = { hash: 'access_token=testing' } as Location; + + let token = await auth.getFromLocation(dummyLocation); + + assert.strictEqual(token, 'testing'); + assert.strictEqual(dummyLocation.hash, ''); + + dummyLocation.hash = 'a=b&access_token=otherTest&c=d'; + token = await auth.getFromLocation(dummyLocation); + + assert.strictEqual(token, 'otherTest'); + assert.strictEqual(dummyLocation.hash, 'a=b&c=d'); + + dummyLocation = { search: 'access_token=testing' } as Location; + token = await auth.getFromLocation(dummyLocation); + + assert.strictEqual(token, 'testing'); + assert.strictEqual(dummyLocation.search, ''); + assert.strictEqual(await auth.getFromLocation({} as Location), null); }); it('authenticate, authentication hook, login event', () => { @@ -100,7 +125,7 @@ describe('@feathersjs/authentication-client', () => { describe('reauthenticate', () => { it('fails when no token in storage', () => { - return app.authentication.reauthenticate().then(() => { + return app.authentication.reAuthenticate().then(() => { assert.fail('Should never get here'); }).catch(error => { assert.strictEqual(error.message, 'No accessToken found in storage'); @@ -120,7 +145,7 @@ describe('@feathersjs/authentication-client', () => { }); return result; - }).then(() => app.authentication.reauthenticate()) + }).then(() => app.authentication.reAuthenticate()) .then(() => app.authentication.reset() ).then(() => { @@ -128,7 +153,7 @@ describe('@feathersjs/authentication-client', () => { }).then(at => { assert.strictEqual(at, accessToken, 'Set accessToken in storage'); - return app.authentication.reauthenticate(); + return app.authentication.reAuthenticate(); }).then(at => { assert.deepStrictEqual(at, { accessToken,