UNPKG

5.37 kBPlain TextView Raw
1import Debug from 'debug';
2import omit from 'lodash/omit';
3import { IncomingMessage } from 'http';
4import { NotAuthenticated } from '@feathersjs/errors';
5import { Params } from '@feathersjs/feathers';
6// @ts-ignore
7import lt from 'long-timeout';
8
9import { AuthenticationBaseStrategy } from './strategy';
10import { AuthenticationRequest, AuthenticationResult, ConnectionEvent } from './core';
11
12const debug = Debug('@feathersjs/authentication/jwt');
13const SPLIT_HEADER = /(\S+)\s+(\S+)/;
14
15export class JWTStrategy extends AuthenticationBaseStrategy {
16 expirationTimers = new WeakMap();
17
18 get configuration () {
19 const authConfig = this.authentication.configuration;
20 const config = super.configuration;
21
22 return {
23 service: authConfig.service,
24 entity: authConfig.entity,
25 entityId: authConfig.entityId,
26 header: 'Authorization',
27 schemes: [ 'Bearer', 'JWT' ],
28 ...config
29 };
30 }
31
32 async handleConnection (event: ConnectionEvent, connection: any, authResult?: AuthenticationResult): Promise<void> {
33 const isValidLogout = event === 'logout' && connection.authentication && authResult &&
34 connection.authentication.accessToken === authResult.accessToken;
35
36 const { accessToken } = authResult || {};
37
38 if (accessToken && event === 'login') {
39 debug('Adding authentication information to connection');
40 const { exp } = await this.authentication.verifyAccessToken(accessToken);
41 // The time (in ms) until the token expires
42 const duration = (exp * 1000) - Date.now();
43 // This may have to be a `logout` event but right now we don't want
44 // the whole context object lingering around until the timer is gone
45 const timer = lt.setTimeout(() => this.app.emit('disconnect', connection), duration);
46
47 debug(`Registering connection expiration timer for ${duration}ms`);
48 lt.clearTimeout(this.expirationTimers.get(connection));
49 this.expirationTimers.set(connection, timer);
50
51 debug('Adding authentication information to connection');
52 connection.authentication = {
53 strategy: this.name,
54 accessToken
55 };
56 } else if (event === 'disconnect' || isValidLogout) {
57 debug('Removing authentication information and expiration timer from connection');
58
59 const { entity } = this.configuration;
60
61 delete connection[entity];
62 delete connection.authentication;
63
64 lt.clearTimeout(this.expirationTimers.get(connection));
65 this.expirationTimers.delete(connection);
66 }
67 }
68
69 verifyConfiguration () {
70 const allowedKeys = [ 'entity', 'entityId', 'service', 'header', 'schemes' ];
71
72 for (const key of Object.keys(this.configuration)) {
73 if (!allowedKeys.includes(key)) {
74 throw new Error(`Invalid JwtStrategy option 'authentication.${this.name}.${key}'. Did you mean to set it in 'authentication.jwtOptions'?`);
75 }
76 }
77
78 if (typeof this.configuration.header !== 'string') {
79 throw new Error(`The 'header' option for the ${this.name} strategy must be a string`);
80 }
81 }
82
83 async getEntityQuery (_params: Params) {
84 return {};
85 }
86
87 /**
88 * Return the entity for a given id
89 * @param id The id to use
90 * @param params Service call parameters
91 */
92 async getEntity (id: string, params: Params) {
93 const entityService = this.entityService;
94 const { entity } = this.configuration;
95
96 debug('Getting entity', id);
97
98 if (entityService === null) {
99 throw new NotAuthenticated(`Could not find entity service`);
100 }
101
102 const query = await this.getEntityQuery(params);
103 const getParams = Object.assign({}, omit(params, 'provider'), { query });
104 const result = await entityService.get(id, getParams);
105
106 if (!params.provider) {
107 return result;
108 }
109
110 return entityService.get(id, { ...params, [entity]: result });
111 }
112
113 async getEntityId (authResult: AuthenticationResult, _params: Params) {
114 return authResult.authentication.payload.sub;
115 }
116
117 async authenticate (authentication: AuthenticationRequest, params: Params) {
118 const { accessToken } = authentication;
119 const { entity } = this.configuration;
120
121 if (!accessToken) {
122 throw new NotAuthenticated('No access token');
123 }
124
125 const payload = await this.authentication.verifyAccessToken(accessToken, params.jwt);
126 const result = {
127 accessToken,
128 authentication: {
129 strategy: 'jwt',
130 accessToken,
131 payload
132 }
133 };
134
135 if (entity === null) {
136 return result;
137 }
138
139 const entityId = await this.getEntityId(result, params);
140 const value = await this.getEntity(entityId, params);
141
142 return {
143 ...result,
144 [entity]: value
145 };
146 }
147
148 async parse (req: IncomingMessage) {
149 const { header, schemes }: { header: string, schemes: string[] } = this.configuration;
150 const headerValue = req.headers && req.headers[header.toLowerCase()];
151
152 if (!headerValue || typeof headerValue !== 'string') {
153 return null;
154 }
155
156 debug('Found parsed header value');
157
158 const [ , scheme, schemeValue ] = headerValue.match(SPLIT_HEADER) || [];
159 const hasScheme = scheme && schemes.some(
160 current => new RegExp(current, 'i').test(scheme)
161 );
162
163 if (scheme && !hasScheme) {
164 return null;
165 }
166
167 return {
168 strategy: this.name,
169 accessToken: hasScheme ? schemeValue : headerValue
170 };
171 }
172}