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