UNPKG

@feathersjs/authentication

Version:

Add Authentication to your FeathersJS app.

186 lines (150 loc) 6.23 kB
import Debug from 'debug'; import merge from 'lodash/merge'; import { NotAuthenticated } from '@feathersjs/errors'; import { AuthenticationBase, AuthenticationResult, AuthenticationRequest } from './core'; import { connection, event } from './hooks'; import '@feathersjs/transport-commons'; import { Application, Params, ServiceMethods, ServiceAddons } from '@feathersjs/feathers'; import jsonwebtoken from 'jsonwebtoken'; const debug = Debug('@feathersjs/authentication/service'); declare module '@feathersjs/feathers' { interface Application<ServiceTypes = {}> { /** * Returns the default authentication service or the * authentication service for a given path. * * @param location The service path to use (optional) */ defaultAuthentication (location?: string): AuthenticationService; } interface Params { authenticated?: boolean; authentication?: AuthenticationRequest; } } export interface AuthenticationService extends ServiceAddons<AuthenticationResult> {} export class AuthenticationService extends AuthenticationBase implements Partial<ServiceMethods<AuthenticationResult>> { constructor (app: Application, configKey: string = 'authentication', options = {}) { super(app, configKey, options); if (typeof app.defaultAuthentication !== 'function') { app.defaultAuthentication = function (location?: string) { const configKey = app.get('defaultAuthentication'); const path = location || Object.keys(this.services).find(current => this.service(current).configKey === configKey ); return path ? this.service(path) : null; }; } } /** * Return the payload for a JWT based on the authentication result. * Called internally by the `create` method. * @param _authResult The current authentication result * @param params The service call parameters */ async getPayload (_authResult: AuthenticationResult, params: Params) { // Uses `params.payload` or returns an empty payload const { payload = {} } = params; return payload; } /** * Returns the JWT options based on an authentication result. * By default sets the JWT subject to the entity id. * @param authResult The authentication result * @param params Service call parameters */ async getTokenOptions (authResult: AuthenticationResult, params: Params) { const { service, entity, entityId } = this.configuration; const jwtOptions = merge({}, params.jwtOptions, params.jwt); const value = service && entity && authResult[entity]; // Set the subject to the entity id if it is available if (value && !jwtOptions.subject) { const idProperty = entityId || this.app.service(service).id; const subject = value[idProperty]; if (subject === undefined) { throw new NotAuthenticated(`Can not set subject from ${entity}.${idProperty}`); } jwtOptions.subject = `${subject}`; } return jwtOptions; } /** * Create and return a new JWT for a given authentication request. * Will trigger the `login` event. * @param data The authentication request (should include `strategy` key) * @param params Service call parameters */ async create (data: AuthenticationRequest, params: Params) { const authStrategies = params.authStrategies || this.configuration.authStrategies; if (!authStrategies.length) { throw new NotAuthenticated('No authentication strategies allowed for creating a JWT (`authStrategies`)'); } const authResult = await this.authenticate(data, params, ...authStrategies); debug('Got authentication result', authResult); if (authResult.accessToken) { return authResult; } const [ payload, jwtOptions ] = await Promise.all([ this.getPayload(authResult, params), this.getTokenOptions(authResult, params) ]); debug('Creating JWT with', payload, jwtOptions); const accessToken = await this.createAccessToken(payload, jwtOptions, params.secret); return merge({ accessToken }, authResult, { authentication: { accessToken, payload: jsonwebtoken.decode(accessToken) } }); } /** * Mark a JWT as removed. By default only verifies the JWT and returns the result. * Triggers the `logout` event. * @param id The JWT to remove or null * @param params Service call parameters */ async remove (id: string | null, params: Params) { const { authentication } = params; const { authStrategies } = this.configuration; // When an id is passed it is expected to be the authentication `accessToken` if (id !== null && id !== authentication.accessToken) { throw new NotAuthenticated('Invalid access token'); } debug('Verifying authentication strategy in remove'); return this.authenticate(authentication, params, ...authStrategies); } /** * Validates the service configuration. */ setup () { // The setup method checks for valid settings and registers the // connection and event (login, logout) hooks const { secret, service, entity, entityId } = this.configuration; if (typeof secret !== 'string') { throw new Error(`A 'secret' must be provided in your authentication configuration`); } if (entity !== null) { if (service === undefined) { throw new Error(`The 'service' option is not set in the authentication configuration`); } if (this.app.service(service) === undefined) { throw new Error(`The '${service}' entity service does not exist (set to 'null' if it is not required)`); } if (this.app.service(service).id === undefined && entityId === undefined) { throw new Error(`The '${service}' service does not have an 'id' property and no 'entityId' option is set.`); } } this.hooks({ after: { create: [ connection('login'), event('login') ], remove: [ connection('logout'), event('logout') ] } }); this.app.on('disconnect', async (connection) => { await this.handleConnection('disconnect', connection); }); if (typeof this.publish === 'function') { this.publish(() => null); } } }