UNPKG

9.73 kBPlain TextView Raw
1import merge from 'lodash/merge'
2import jsonwebtoken, { SignOptions, Secret, VerifyOptions, Algorithm } from 'jsonwebtoken'
3import { v4 as uuidv4 } from 'uuid'
4import { NotAuthenticated } from '@feathersjs/errors'
5import { createDebug } from '@feathersjs/commons'
6import { Application, Params } from '@feathersjs/feathers'
7import { IncomingMessage, ServerResponse } from 'http'
8import { AuthenticationConfiguration, defaultOptions } from './options'
9
10const debug = createDebug('@feathersjs/authentication/base')
11
12export interface AuthenticationResult {
13 [key: string]: any
14}
15
16export interface AuthenticationRequest {
17 strategy?: string
18 [key: string]: any
19}
20
21export interface AuthenticationParams extends Params {
22 payload?: { [key: string]: any }
23 jwtOptions?: SignOptions
24 authStrategies?: string[]
25 secret?: string
26 [key: string]: any
27}
28
29export type ConnectionEvent = 'login' | 'logout' | 'disconnect'
30
31export interface AuthenticationStrategy {
32 /**
33 * Implement this method to get access to the AuthenticationService
34 *
35 * @param auth The AuthenticationService
36 */
37 setAuthentication?(auth: AuthenticationBase): void
38 /**
39 * Implement this method to get access to the Feathers application
40 *
41 * @param app The Feathers application instance
42 */
43 setApplication?(app: Application): void
44 /**
45 * Implement this method to get access to the strategy name
46 *
47 * @param name The name of the strategy
48 */
49 setName?(name: string): void
50 /**
51 * Implement this method to verify the current configuration
52 * and throw an error if it is invalid.
53 */
54 verifyConfiguration?(): void
55 /**
56 * Implement this method to setup this strategy
57 * @param auth The AuthenticationService
58 * @param name The name of the strategy
59 */
60 setup?(auth: AuthenticationBase, name: string): Promise<void>
61 /**
62 * Authenticate an authentication request with this strategy.
63 * Should throw an error if the strategy did not succeed.
64 *
65 * @param authentication The authentication request
66 * @param params The service call parameters
67 */
68 authenticate?(
69 authentication: AuthenticationRequest,
70 params: AuthenticationParams
71 ): Promise<AuthenticationResult>
72 /**
73 * Update a real-time connection according to this strategy.
74 *
75 * @param connection The real-time connection
76 * @param context The hook context
77 */
78 handleConnection?(event: ConnectionEvent, connection: any, authResult?: AuthenticationResult): Promise<void>
79 /**
80 * Parse a basic HTTP request and response for authentication request information.
81 *
82 * @param req The HTTP request
83 * @param res The HTTP response
84 */
85 parse?(req: IncomingMessage, res: ServerResponse): Promise<AuthenticationRequest | null>
86}
87
88export interface JwtVerifyOptions extends VerifyOptions {
89 algorithm?: string | string[]
90}
91
92/**
93 * A base class for managing authentication strategies and creating and verifying JWTs
94 */
95export class AuthenticationBase {
96 app: Application
97 strategies: { [key: string]: AuthenticationStrategy }
98 configKey: string
99 isReady: boolean
100
101 /**
102 * Create a new authentication service.
103 *
104 * @param app The Feathers application instance
105 * @param configKey The configuration key name in `app.get` (default: `authentication`)
106 * @param options Optional initial options
107 */
108 constructor(app: Application, configKey = 'authentication', options = {}) {
109 if (!app || typeof app.use !== 'function') {
110 throw new Error('An application instance has to be passed to the authentication service')
111 }
112
113 this.app = app
114 this.strategies = {}
115 this.configKey = configKey
116 this.isReady = false
117
118 app.set('defaultAuthentication', app.get('defaultAuthentication') || configKey)
119 app.set(configKey, merge({}, app.get(configKey), options))
120 }
121
122 /**
123 * Return the current configuration from the application
124 */
125 get configuration(): AuthenticationConfiguration {
126 // Always returns a copy of the authentication configuration
127 return Object.assign({}, defaultOptions, this.app.get(this.configKey))
128 }
129
130 /**
131 * A list of all registered strategy names
132 */
133 get strategyNames() {
134 return Object.keys(this.strategies)
135 }
136
137 /**
138 * Register a new authentication strategy under a given name.
139 *
140 * @param name The name to register the strategy under
141 * @param strategy The authentication strategy instance
142 */
143 register(name: string, strategy: AuthenticationStrategy) {
144 // Call the functions a strategy can implement
145 if (typeof strategy.setName === 'function') {
146 strategy.setName(name)
147 }
148
149 if (typeof strategy.setApplication === 'function') {
150 strategy.setApplication(this.app)
151 }
152
153 if (typeof strategy.setAuthentication === 'function') {
154 strategy.setAuthentication(this)
155 }
156
157 if (typeof strategy.verifyConfiguration === 'function') {
158 strategy.verifyConfiguration()
159 }
160
161 // Register strategy as name
162 this.strategies[name] = strategy
163
164 if (this.isReady) {
165 strategy.setup?.(this, name)
166 }
167 }
168
169 /**
170 * Get the registered authentication strategies for a list of names.
171 *
172 * @param names The list or strategy names
173 */
174 getStrategies(...names: string[]) {
175 return names.map((name) => this.strategies[name]).filter((current) => !!current)
176 }
177
178 /**
179 * Returns a single strategy by name
180 *
181 * @param name The strategy name
182 * @returns The authentication strategy or undefined
183 */
184 getStrategy(name: string) {
185 return this.strategies[name]
186 }
187
188 /**
189 * Create a new access token with payload and options.
190 *
191 * @param payload The JWT payload
192 * @param optsOverride The options to extend the defaults (`configuration.jwtOptions`) with
193 * @param secretOverride Use a different secret instead
194 */
195 async createAccessToken(
196 payload: string | Buffer | object,
197 optsOverride?: SignOptions,
198 secretOverride?: Secret
199 ) {
200 const { secret, jwtOptions } = this.configuration
201 // Use configuration by default but allow overriding the secret
202 const jwtSecret = secretOverride || secret
203 // Default jwt options merged with additional options
204 const options = merge({}, jwtOptions, optsOverride)
205
206 if (!options.jwtid) {
207 // Generate a UUID as JWT ID by default
208 options.jwtid = uuidv4()
209 }
210
211 return jsonwebtoken.sign(payload, jwtSecret, options)
212 }
213
214 /**
215 * Verifies an access token.
216 *
217 * @param accessToken The token to verify
218 * @param optsOverride The options to extend the defaults (`configuration.jwtOptions`) with
219 * @param secretOverride Use a different secret instead
220 */
221 async verifyAccessToken(accessToken: string, optsOverride?: JwtVerifyOptions, secretOverride?: Secret) {
222 const { secret, jwtOptions } = this.configuration
223 const jwtSecret = secretOverride || secret
224 const options = merge({}, jwtOptions, optsOverride)
225 const { algorithm } = options
226
227 // Normalize the `algorithm` setting into the algorithms array
228 if (algorithm && !options.algorithms) {
229 options.algorithms = (Array.isArray(algorithm) ? algorithm : [algorithm]) as Algorithm[]
230 delete options.algorithm
231 }
232
233 try {
234 const verified = jsonwebtoken.verify(accessToken, jwtSecret, options)
235
236 return verified as any
237 } catch (error: any) {
238 throw new NotAuthenticated(error.message, error)
239 }
240 }
241
242 /**
243 * Authenticate a given authentication request against a list of strategies.
244 *
245 * @param authentication The authentication request
246 * @param params Service call parameters
247 * @param allowed A list of allowed strategy names
248 */
249 async authenticate(
250 authentication: AuthenticationRequest,
251 params: AuthenticationParams,
252 ...allowed: string[]
253 ) {
254 const { strategy } = authentication || {}
255 const [authStrategy] = this.getStrategies(strategy)
256 const strategyAllowed = allowed.includes(strategy)
257
258 debug('Running authenticate for strategy', strategy, allowed)
259
260 if (!authentication || !authStrategy || !strategyAllowed) {
261 const additionalInfo =
262 (!strategy && ' (no `strategy` set)') ||
263 (!strategyAllowed && ' (strategy not allowed in authStrategies)') ||
264 ''
265
266 // If there are no valid strategies or `authentication` is not an object
267 throw new NotAuthenticated('Invalid authentication information' + additionalInfo)
268 }
269
270 return authStrategy.authenticate(authentication, {
271 ...params,
272 authenticated: true
273 })
274 }
275
276 async handleConnection(event: ConnectionEvent, connection: any, authResult?: AuthenticationResult) {
277 const strategies = this.getStrategies(...Object.keys(this.strategies)).filter(
278 (current) => typeof current.handleConnection === 'function'
279 )
280
281 for (const strategy of strategies) {
282 await strategy.handleConnection(event, connection, authResult)
283 }
284 }
285
286 /**
287 * Parse an HTTP request and response for authentication request information.
288 *
289 * @param req The HTTP request
290 * @param res The HTTP response
291 * @param names A list of strategies to use
292 */
293 async parse(req: IncomingMessage, res: ServerResponse, ...names: string[]) {
294 const strategies = this.getStrategies(...names).filter((current) => typeof current.parse === 'function')
295
296 debug('Strategies parsing HTTP header for authentication information', names)
297
298 for (const authStrategy of strategies) {
299 const value = await authStrategy.parse(req, res)
300
301 if (value !== null) {
302 return value
303 }
304 }
305
306 return null
307 }
308
309 async setup() {
310 this.isReady = true
311
312 for (const name of Object.keys(this.strategies)) {
313 const strategy = this.strategies[name]
314
315 await strategy.setup?.(this, name)
316 }
317 }
318}