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