UNPKG

6.23 kBPlain TextView Raw
1import Debug from 'debug';
2import merge from 'lodash/merge';
3import { NotAuthenticated } from '@feathersjs/errors';
4import { AuthenticationBase, AuthenticationResult, AuthenticationRequest } from './core';
5import { connection, event } from './hooks';
6import '@feathersjs/transport-commons';
7import { Application, Params, ServiceMethods, ServiceAddons } from '@feathersjs/feathers';
8import jsonwebtoken from 'jsonwebtoken';
9
10const debug = Debug('@feathersjs/authentication/service');
11
12declare module '@feathersjs/feathers' {
13 interface Application<ServiceTypes = {}> {
14
15 /**
16 * Returns the default authentication service or the
17 * authentication service for a given path.
18 *
19 * @param location The service path to use (optional)
20 */
21 defaultAuthentication (location?: string): AuthenticationService;
22 }
23
24 interface Params {
25 authenticated?: boolean;
26 authentication?: AuthenticationRequest;
27 }
28}
29
30export interface AuthenticationService extends ServiceAddons<AuthenticationResult> {}
31
32export class AuthenticationService extends AuthenticationBase implements Partial<ServiceMethods<AuthenticationResult>> {
33 constructor (app: Application, configKey: string = 'authentication', options = {}) {
34 super(app, configKey, options);
35
36 if (typeof app.defaultAuthentication !== 'function') {
37 app.defaultAuthentication = function (location?: string) {
38 const configKey = app.get('defaultAuthentication');
39 const path = location || Object.keys(this.services).find(current =>
40 this.service(current).configKey === configKey
41 );
42
43 return path ? this.service(path) : null;
44 };
45 }
46 }
47 /**
48 * Return the payload for a JWT based on the authentication result.
49 * Called internally by the `create` method.
50 * @param _authResult The current authentication result
51 * @param params The service call parameters
52 */
53 async getPayload (_authResult: AuthenticationResult, params: Params) {
54 // Uses `params.payload` or returns an empty payload
55 const { payload = {} } = params;
56
57 return payload;
58 }
59
60 /**
61 * Returns the JWT options based on an authentication result.
62 * By default sets the JWT subject to the entity id.
63 * @param authResult The authentication result
64 * @param params Service call parameters
65 */
66 async getTokenOptions (authResult: AuthenticationResult, params: Params) {
67 const { service, entity, entityId } = this.configuration;
68 const jwtOptions = merge({}, params.jwtOptions, params.jwt);
69 const value = service && entity && authResult[entity];
70
71 // Set the subject to the entity id if it is available
72 if (value && !jwtOptions.subject) {
73 const idProperty = entityId || this.app.service(service).id;
74 const subject = value[idProperty];
75
76 if (subject === undefined) {
77 throw new NotAuthenticated(`Can not set subject from ${entity}.${idProperty}`);
78 }
79
80 jwtOptions.subject = `${subject}`;
81 }
82
83 return jwtOptions;
84 }
85
86 /**
87 * Create and return a new JWT for a given authentication request.
88 * Will trigger the `login` event.
89 * @param data The authentication request (should include `strategy` key)
90 * @param params Service call parameters
91 */
92 async create (data: AuthenticationRequest, params: Params) {
93 const authStrategies = params.authStrategies || this.configuration.authStrategies;
94
95 if (!authStrategies.length) {
96 throw new NotAuthenticated('No authentication strategies allowed for creating a JWT (`authStrategies`)');
97 }
98
99 const authResult = await this.authenticate(data, params, ...authStrategies);
100
101 debug('Got authentication result', authResult);
102
103 if (authResult.accessToken) {
104 return authResult;
105 }
106
107 const [ payload, jwtOptions ] = await Promise.all([
108 this.getPayload(authResult, params),
109 this.getTokenOptions(authResult, params)
110 ]);
111
112 debug('Creating JWT with', payload, jwtOptions);
113
114 const accessToken = await this.createAccessToken(payload, jwtOptions, params.secret);
115
116 return merge({ accessToken }, authResult, {
117 authentication: {
118 accessToken,
119 payload: jsonwebtoken.decode(accessToken)
120 }
121 });
122 }
123
124 /**
125 * Mark a JWT as removed. By default only verifies the JWT and returns the result.
126 * Triggers the `logout` event.
127 * @param id The JWT to remove or null
128 * @param params Service call parameters
129 */
130 async remove (id: string | null, params: Params) {
131 const { authentication } = params;
132 const { authStrategies } = this.configuration;
133
134 // When an id is passed it is expected to be the authentication `accessToken`
135 if (id !== null && id !== authentication.accessToken) {
136 throw new NotAuthenticated('Invalid access token');
137 }
138
139 debug('Verifying authentication strategy in remove');
140
141 return this.authenticate(authentication, params, ...authStrategies);
142 }
143
144 /**
145 * Validates the service configuration.
146 */
147 setup () {
148 // The setup method checks for valid settings and registers the
149 // connection and event (login, logout) hooks
150 const { secret, service, entity, entityId } = this.configuration;
151
152 if (typeof secret !== 'string') {
153 throw new Error(`A 'secret' must be provided in your authentication configuration`);
154 }
155
156 if (entity !== null) {
157 if (service === undefined) {
158 throw new Error(`The 'service' option is not set in the authentication configuration`);
159 }
160
161 if (this.app.service(service) === undefined) {
162 throw new Error(`The '${service}' entity service does not exist (set to 'null' if it is not required)`);
163 }
164
165 if (this.app.service(service).id === undefined && entityId === undefined) {
166 throw new Error(`The '${service}' service does not have an 'id' property and no 'entityId' option is set.`);
167 }
168 }
169
170 this.hooks({
171 after: {
172 create: [ connection('login'), event('login') ],
173 remove: [ connection('logout'), event('logout') ]
174 }
175 });
176
177 this.app.on('disconnect', async (connection) => {
178 await this.handleConnection('disconnect', connection);
179 });
180
181 if (typeof this.publish === 'function') {
182 this.publish(() => null);
183 }
184 }
185}