1 | import merge from 'lodash/merge';
|
2 | import jsonwebtoken, { SignOptions, Secret, VerifyOptions } from 'jsonwebtoken';
|
3 | import { v4 as uuidv4 } from 'uuid';
|
4 | import { NotAuthenticated } from '@feathersjs/errors';
|
5 | import Debug from 'debug';
|
6 | import { Application, Params } from '@feathersjs/feathers';
|
7 | import { IncomingMessage, ServerResponse } from 'http';
|
8 | import defaultOptions from './options';
|
9 |
|
10 | const debug = Debug('@feathersjs/authentication/base');
|
11 |
|
12 | export interface AuthenticationResult {
|
13 | [key: string]: any;
|
14 | }
|
15 |
|
16 | export interface AuthenticationRequest {
|
17 | strategy?: string;
|
18 | [key: string]: any;
|
19 | }
|
20 |
|
21 | export type ConnectionEvent = 'login' | 'logout' | 'disconnect';
|
22 |
|
23 | export interface AuthenticationStrategy {
|
24 | |
25 |
|
26 |
|
27 |
|
28 | setAuthentication? (auth: AuthenticationBase): void;
|
29 | |
30 |
|
31 |
|
32 |
|
33 | setApplication? (app: Application): void;
|
34 | |
35 |
|
36 |
|
37 |
|
38 | setName? (name: string): void;
|
39 | |
40 |
|
41 |
|
42 |
|
43 | verifyConfiguration? (): void;
|
44 | |
45 |
|
46 |
|
47 |
|
48 |
|
49 |
|
50 | authenticate? (authentication: AuthenticationRequest, params: Params): Promise<AuthenticationResult>;
|
51 | |
52 |
|
53 |
|
54 |
|
55 |
|
56 |
|
57 | handleConnection? (event: ConnectionEvent, connection: any, authResult?: AuthenticationResult): Promise<void>;
|
58 | |
59 |
|
60 |
|
61 |
|
62 |
|
63 | parse? (req: IncomingMessage, res: ServerResponse): Promise<AuthenticationRequest | null>;
|
64 | }
|
65 |
|
66 | export interface JwtVerifyOptions extends VerifyOptions {
|
67 | algorithm?: string | string[];
|
68 | }
|
69 |
|
70 |
|
71 |
|
72 |
|
73 | export class AuthenticationBase {
|
74 | app: Application;
|
75 | configKey: string;
|
76 | strategies: {
|
77 | [key: string]: AuthenticationStrategy;
|
78 | };
|
79 |
|
80 | |
81 |
|
82 |
|
83 |
|
84 |
|
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 |
|
101 |
|
102 | get configuration () {
|
103 |
|
104 | return Object.assign({}, defaultOptions, this.app.get(this.configKey));
|
105 | }
|
106 |
|
107 | |
108 |
|
109 |
|
110 | get strategyNames () {
|
111 | return Object.keys(this.strategies);
|
112 | }
|
113 |
|
114 | |
115 |
|
116 |
|
117 |
|
118 |
|
119 | register (name: string, strategy: AuthenticationStrategy) {
|
120 |
|
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 |
|
138 | this.strategies[name] = strategy;
|
139 | }
|
140 |
|
141 | |
142 |
|
143 |
|
144 |
|
145 | getStrategies (...names: string[]) {
|
146 | return names.map(name => this.strategies[name])
|
147 | .filter(current => !!current);
|
148 | }
|
149 |
|
150 | |
151 |
|
152 |
|
153 |
|
154 |
|
155 |
|
156 | async createAccessToken (payload: string | Buffer | object, optsOverride?: SignOptions, secretOverride?: Secret) {
|
157 | const { secret, jwtOptions } = this.configuration;
|
158 |
|
159 | const jwtSecret = secretOverride || secret;
|
160 |
|
161 | const options = merge({}, jwtOptions, optsOverride);
|
162 |
|
163 | if (!options.jwtid) {
|
164 |
|
165 | options.jwtid = uuidv4();
|
166 | }
|
167 |
|
168 | return jsonwebtoken.sign(payload, jwtSecret, options);
|
169 | }
|
170 |
|
171 | |
172 |
|
173 |
|
174 |
|
175 |
|
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 |
|
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 |
|
200 |
|
201 |
|
202 |
|
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 |
|
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 |
|
236 |
|
237 |
|
238 |
|
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 | }
|