UNPKG

6.66 kBPlain TextView Raw
1import merge from 'lodash/merge'
2import { NotAuthenticated } from '@feathersjs/errors'
3import '@feathersjs/transport-commons'
4import { createDebug } from '@feathersjs/commons'
5import { ServiceMethods } from '@feathersjs/feathers'
6import { resolveDispatch } from '@feathersjs/schema'
7import jsonwebtoken from 'jsonwebtoken'
8import { hooks } from '@feathersjs/hooks'
9
10import { AuthenticationBase, AuthenticationResult, AuthenticationRequest, AuthenticationParams } from './core'
11import { connection, event } from './hooks'
12import { RealTimeConnection } from '@feathersjs/feathers'
13
14const debug = createDebug('@feathersjs/authentication/service')
15
16declare module '@feathersjs/feathers/lib/declarations' {
17 // eslint-disable-next-line @typescript-eslint/no-unused-vars
18 interface FeathersApplication<Services, Settings> {
19 // eslint-disable-line
20 /**
21 * Returns the default authentication service or the
22 * authentication service for a given path.
23 *
24 * @param location The service path to use (optional)
25 */
26 defaultAuthentication?(location?: string): AuthenticationService
27 }
28
29 interface Params {
30 authenticated?: boolean
31 authentication?: AuthenticationRequest
32 }
33}
34
35export 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 * Return the payload for a JWT based on the authentication result.
64 * Called internally by the `create` method.
65 *
66 * @param _authResult The current authentication result
67 * @param params The service call parameters
68 */
69 async getPayload(_authResult: AuthenticationResult, params: AuthenticationParams) {
70 // Uses `params.payload` or returns an empty payload
71 const { payload = {} } = params
72
73 return payload
74 }
75
76 /**
77 * Returns the JWT options based on an authentication result.
78 * By default sets the JWT subject to the entity id.
79 *
80 * @param authResult The authentication result
81 * @param params Service call parameters
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 // Set the subject to the entity id if it is available
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 * Create and return a new JWT for a given authentication request.
105 * Will trigger the `login` event.
106 *
107 * @param data The authentication request (should include `strategy` key)
108 * @param params Service call parameters
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 * Mark a JWT as removed. By default only verifies the JWT and returns the result.
146 * Triggers the `logout` event.
147 *
148 * @param id The JWT to remove or null
149 * @param params Service call parameters
150 */
151 async remove(id: string | null, params?: AuthenticationParams) {
152 const { authentication } = params
153 const { authStrategies } = this.configuration
154
155 // When an id is passed it is expected to be the authentication `accessToken`
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 * Validates the service configuration.
167 */
168 async setup() {
169 await super.setup()
170
171 // The setup method checks for valid settings and registers the
172 // connection and event (login, logout) hooks
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}