UNPKG

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