1 | import Debug from 'debug';
|
2 | import merge from 'lodash/merge';
|
3 | import { NotAuthenticated } from '@feathersjs/errors';
|
4 | import { AuthenticationBase, AuthenticationResult, AuthenticationRequest } from './core';
|
5 | import { connection, event } from './hooks';
|
6 | import '@feathersjs/transport-commons';
|
7 | import { Application, Params, ServiceMethods, ServiceAddons } from '@feathersjs/feathers';
|
8 | import jsonwebtoken from 'jsonwebtoken';
|
9 |
|
10 | const debug = Debug('@feathersjs/authentication/service');
|
11 |
|
12 | declare module '@feathersjs/feathers' {
|
13 | interface Application<ServiceTypes = {}> {
|
14 |
|
15 | |
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 | defaultAuthentication (location?: string): AuthenticationService;
|
22 | }
|
23 |
|
24 | interface Params {
|
25 | authenticated?: boolean;
|
26 | authentication?: AuthenticationRequest;
|
27 | }
|
28 | }
|
29 |
|
30 | export interface AuthenticationService extends ServiceAddons<AuthenticationResult> {}
|
31 |
|
32 | export class AuthenticationService extends AuthenticationBase implements Partial<ServiceMethods<AuthenticationResult>> {
|
33 | constructor (app: Application, configKey: string = 'authentication', options = {}) {
|
34 | super(app, configKey, options);
|
35 |
|
36 | if (typeof app.defaultAuthentication !== 'function') {
|
37 | app.defaultAuthentication = function (location?: string) {
|
38 | const configKey = app.get('defaultAuthentication');
|
39 | const path = location || Object.keys(this.services).find(current =>
|
40 | this.service(current).configKey === configKey
|
41 | );
|
42 |
|
43 | return path ? this.service(path) : null;
|
44 | };
|
45 | }
|
46 | }
|
47 | |
48 |
|
49 |
|
50 |
|
51 |
|
52 |
|
53 | async getPayload (_authResult: AuthenticationResult, params: Params) {
|
54 |
|
55 | const { payload = {} } = params;
|
56 |
|
57 | return payload;
|
58 | }
|
59 |
|
60 | |
61 |
|
62 |
|
63 |
|
64 |
|
65 |
|
66 | async getTokenOptions (authResult: AuthenticationResult, params: Params) {
|
67 | const { service, entity, entityId } = this.configuration;
|
68 | const jwtOptions = merge({}, params.jwtOptions, params.jwt);
|
69 | const value = service && entity && authResult[entity];
|
70 |
|
71 |
|
72 | if (value && !jwtOptions.subject) {
|
73 | const idProperty = entityId || this.app.service(service).id;
|
74 | const subject = value[idProperty];
|
75 |
|
76 | if (subject === undefined) {
|
77 | throw new NotAuthenticated(`Can not set subject from ${entity}.${idProperty}`);
|
78 | }
|
79 |
|
80 | jwtOptions.subject = `${subject}`;
|
81 | }
|
82 |
|
83 | return jwtOptions;
|
84 | }
|
85 |
|
86 | |
87 |
|
88 |
|
89 |
|
90 |
|
91 |
|
92 | async create (data: AuthenticationRequest, params: Params) {
|
93 | const authStrategies = params.authStrategies || this.configuration.authStrategies;
|
94 |
|
95 | if (!authStrategies.length) {
|
96 | throw new NotAuthenticated('No authentication strategies allowed for creating a JWT (`authStrategies`)');
|
97 | }
|
98 |
|
99 | const authResult = await this.authenticate(data, params, ...authStrategies);
|
100 |
|
101 | debug('Got authentication result', authResult);
|
102 |
|
103 | if (authResult.accessToken) {
|
104 | return authResult;
|
105 | }
|
106 |
|
107 | const [ payload, jwtOptions ] = await Promise.all([
|
108 | this.getPayload(authResult, params),
|
109 | this.getTokenOptions(authResult, params)
|
110 | ]);
|
111 |
|
112 | debug('Creating JWT with', payload, jwtOptions);
|
113 |
|
114 | const accessToken = await this.createAccessToken(payload, jwtOptions, params.secret);
|
115 |
|
116 | return merge({ accessToken }, authResult, {
|
117 | authentication: {
|
118 | accessToken,
|
119 | payload: jsonwebtoken.decode(accessToken)
|
120 | }
|
121 | });
|
122 | }
|
123 |
|
124 | |
125 |
|
126 |
|
127 |
|
128 |
|
129 |
|
130 | async remove (id: string | null, params: Params) {
|
131 | const { authentication } = params;
|
132 | const { authStrategies } = this.configuration;
|
133 |
|
134 |
|
135 | if (id !== null && id !== authentication.accessToken) {
|
136 | throw new NotAuthenticated('Invalid access token');
|
137 | }
|
138 |
|
139 | debug('Verifying authentication strategy in remove');
|
140 |
|
141 | return this.authenticate(authentication, params, ...authStrategies);
|
142 | }
|
143 |
|
144 | |
145 |
|
146 |
|
147 | setup () {
|
148 |
|
149 |
|
150 | const { secret, service, entity, entityId } = this.configuration;
|
151 |
|
152 | if (typeof secret !== 'string') {
|
153 | throw new Error(`A 'secret' must be provided in your authentication configuration`);
|
154 | }
|
155 |
|
156 | if (entity !== null) {
|
157 | if (service === undefined) {
|
158 | throw new Error(`The 'service' option is not set in the authentication configuration`);
|
159 | }
|
160 |
|
161 | if (this.app.service(service) === undefined) {
|
162 | throw new Error(`The '${service}' entity service does not exist (set to 'null' if it is not required)`);
|
163 | }
|
164 |
|
165 | if (this.app.service(service).id === undefined && entityId === undefined) {
|
166 | throw new Error(`The '${service}' service does not have an 'id' property and no 'entityId' option is set.`);
|
167 | }
|
168 | }
|
169 |
|
170 | this.hooks({
|
171 | after: {
|
172 | create: [ connection('login'), event('login') ],
|
173 | remove: [ connection('logout'), event('logout') ]
|
174 | }
|
175 | });
|
176 |
|
177 | this.app.on('disconnect', async (connection) => {
|
178 | await this.handleConnection('disconnect', connection);
|
179 | });
|
180 |
|
181 | if (typeof this.publish === 'function') {
|
182 | this.publish(() => null);
|
183 | }
|
184 | }
|
185 | }
|