Skip to content

Commit

Permalink
feat: Add authentication through oAuth redirect to authentication cli…
Browse files Browse the repository at this point in the history
…ent (#1301)
  • Loading branch information
daffl committed Apr 21, 2019
1 parent 656bae7 commit 35d8043
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 59 deletions.
90 changes: 50 additions & 40 deletions 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 {
Expand All @@ -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);
Expand All @@ -58,7 +41,7 @@ export class AuthenticationClient {
}

get storage () {
return this.app.get('storage');
return this.app.get('storage') as Storage;
}

handleSocket (socket: any) {
Expand All @@ -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<string|null> {
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<string|null> {
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 () {
Expand All @@ -94,7 +100,7 @@ export class AuthenticationClient {
return Promise.resolve(null);
}

reauthenticate (force: boolean = false): Promise<AuthenticationResult> {
reAuthenticate (force: boolean = false): Promise<AuthenticationResult> {
// Either returns the authentication state or
// tries to re-authenticate with the stored JWT and strategy
const authPromise = this.app.get('authentication');
Expand All @@ -109,15 +115,17 @@ 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;
}

authenticate (authentication: AuthenticationRequest): Promise<AuthenticationResult> {
if (!authentication) {
return this.reauthenticate();
return this.reAuthenticate();
}

const promise = this.service.create(authentication)
Expand All @@ -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);

Expand Down
23 changes: 14 additions & 9 deletions 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<ServiceTypes = any> {
Expand All @@ -10,34 +11,39 @@ declare module '@feathersjs/feathers' {
primus?: any;
authentication: AuthenticationClient;
authenticate (authentication?: AuthenticationRequest): Promise<AuthenticationResult>;
reauthenticate (force: boolean): Promise<AuthenticationResult>;
reAuthenticate (force: boolean): Promise<AuthenticationResult>;
logout (): Promise<AuthenticationResult>;
}
}

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<AuthenticationClientOptions> = {}) => {
const options: AuthenticationClientOptions = Object.assign({}, defaults, _options);
const { Authentication } = options;

return (app: Application) => {
const authentication = new Authentication(app, options);

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({
Expand All @@ -51,7 +57,6 @@ const init = (_options: AuthenticationClientOptions = {}) => {
};
};

export { AuthenticationClient, AuthenticationClientOptions, Storage, hooks };
export default init;

if (typeof module !== 'undefined') {
Expand Down
49 changes: 49 additions & 0 deletions packages/authentication-client/src/storage.ts
@@ -0,0 +1,49 @@
export interface Storage {
getItem (key: string): Promise<any>;
setItem? (key: string, value: any): Promise<any>;
removeItem? (key: string): Promise<any>;
}

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));
}
}
45 changes: 35 additions & 10 deletions packages/authentication-client/test/index.test.ts
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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');
Expand All @@ -120,15 +145,15 @@ describe('@feathersjs/authentication-client', () => {
});

return result;
}).then(() => app.authentication.reauthenticate())
}).then(() => app.authentication.reAuthenticate())
.then(() =>
app.authentication.reset()
).then(() => {
return Promise.resolve(app.get('storage').getItem('feathers-jwt'));
}).then(at => {
assert.strictEqual(at, accessToken, 'Set accessToken in storage');

return app.authentication.reauthenticate();
return app.authentication.reAuthenticate();
}).then(at => {
assert.deepStrictEqual(at, {
accessToken,
Expand Down

0 comments on commit 35d8043

Please sign in to comment.