UNPKG

5.63 kBPlain TextView Raw
1/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/ban-ts-comment */
2import { IncomingMessage } from 'http'
3import { NotAuthenticated } from '@feathersjs/errors'
4import { Params } from '@feathersjs/feathers'
5import { createDebug } from '@feathersjs/commons'
6// @ts-ignore
7import lt from 'long-timeout'
8
9import { AuthenticationBaseStrategy } from './strategy'
10import { AuthenticationParams, AuthenticationRequest, AuthenticationResult, ConnectionEvent } from './core'
11
12const debug = createDebug('@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(
33 event: ConnectionEvent,
34 connection: any,
35 authResult?: AuthenticationResult
36 ): Promise<void> {
37 const isValidLogout =
38 event === 'logout' &&
39 connection.authentication &&
40 authResult &&
41 connection.authentication.accessToken === authResult.accessToken
42
43 const { accessToken } = authResult || {}
44 const { entity } = this.configuration
45
46 if (accessToken && event === 'login') {
47 debug('Adding authentication information to connection')
48 const { exp } =
49 authResult?.authentication?.payload || (await this.authentication.verifyAccessToken(accessToken))
50 // The time (in ms) until the token expires
51 const duration = exp * 1000 - Date.now()
52 const timer = lt.setTimeout(() => this.app.emit('disconnect', connection), duration)
53
54 debug(`Registering connection expiration timer for ${duration}ms`)
55 lt.clearTimeout(this.expirationTimers.get(connection))
56 this.expirationTimers.set(connection, timer)
57
58 debug('Adding authentication information to connection')
59 connection.authentication = {
60 strategy: this.name,
61 accessToken
62 }
63 connection[entity] = authResult[entity]
64 } else if (event === 'disconnect' || isValidLogout) {
65 debug('Removing authentication information and expiration timer from connection')
66
67 await new Promise((resolve) =>
68 process.nextTick(() => {
69 delete connection[entity]
70 delete connection.authentication
71 resolve(connection)
72 })
73 )
74
75 lt.clearTimeout(this.expirationTimers.get(connection))
76 this.expirationTimers.delete(connection)
77 }
78 }
79
80 verifyConfiguration() {
81 const allowedKeys = ['entity', 'entityId', 'service', 'header', 'schemes']
82
83 for (const key of Object.keys(this.configuration)) {
84 if (!allowedKeys.includes(key)) {
85 throw new Error(
86 `Invalid JwtStrategy option 'authentication.${this.name}.${key}'. Did you mean to set it in 'authentication.jwtOptions'?`
87 )
88 }
89 }
90
91 if (typeof this.configuration.header !== 'string') {
92 throw new Error(`The 'header' option for the ${this.name} strategy must be a string`)
93 }
94 }
95
96 async getEntityQuery(_params: Params) {
97 return {}
98 }
99
100 /**
101 * Return the entity for a given id
102 *
103 * @param id The id to use
104 * @param params Service call parameters
105 */
106 async getEntity(id: string, params: Params) {
107 const entityService = this.entityService
108 const { entity } = this.configuration
109
110 debug('Getting entity', id)
111
112 if (entityService === null) {
113 throw new NotAuthenticated('Could not find entity service')
114 }
115
116 const query = await this.getEntityQuery(params)
117 const { provider, ...paramsWithoutProvider } = params
118 const result = await entityService.get(id, {
119 ...paramsWithoutProvider,
120 query
121 })
122
123 if (!params.provider) {
124 return result
125 }
126
127 return entityService.get(id, { ...params, [entity]: result })
128 }
129
130 async getEntityId(authResult: AuthenticationResult, _params: Params) {
131 return authResult.authentication.payload.sub
132 }
133
134 async authenticate(authentication: AuthenticationRequest, params: AuthenticationParams) {
135 const { accessToken } = authentication
136 const { entity } = this.configuration
137
138 if (!accessToken) {
139 throw new NotAuthenticated('No access token')
140 }
141
142 const payload = await this.authentication.verifyAccessToken(accessToken, params.jwt)
143 const result = {
144 accessToken,
145 authentication: {
146 strategy: 'jwt',
147 accessToken,
148 payload
149 }
150 }
151
152 if (entity === null) {
153 return result
154 }
155
156 const entityId = await this.getEntityId(result, params)
157 const value = await this.getEntity(entityId, params)
158
159 return {
160 ...result,
161 [entity]: value
162 }
163 }
164
165 async parse(req: IncomingMessage): Promise<{
166 strategy: string
167 accessToken: string
168 } | null> {
169 const { header, schemes }: { header: string; schemes: string[] } = this.configuration
170 const headerValue = req.headers && req.headers[header.toLowerCase()]
171
172 if (!headerValue || typeof headerValue !== 'string') {
173 return null
174 }
175
176 debug('Found parsed header value')
177
178 const [, scheme, schemeValue] = headerValue.match(SPLIT_HEADER) || []
179 const hasScheme = scheme && schemes.some((current) => new RegExp(current, 'i').test(scheme))
180
181 if (scheme && !hasScheme) {
182 return null
183 }
184
185 return {
186 strategy: this.name,
187 accessToken: hasScheme ? schemeValue : headerValue
188 }
189 }
190}