UNPKG

4 kBPlain TextView Raw
1/* eslint-disable @typescript-eslint/no-unused-vars */
2import bcrypt from 'bcryptjs'
3import get from 'lodash/get'
4import { NotAuthenticated } from '@feathersjs/errors'
5import { Query, Params } from '@feathersjs/feathers'
6import { AuthenticationRequest, AuthenticationBaseStrategy } from '@feathersjs/authentication'
7import { createDebug } from '@feathersjs/commons'
8
9const debug = createDebug('@feathersjs/authentication-local/strategy')
10
11export class LocalStrategy extends AuthenticationBaseStrategy {
12 verifyConfiguration() {
13 const config = this.configuration
14
15 ;['usernameField', 'passwordField'].forEach((prop) => {
16 if (typeof config[prop] !== 'string') {
17 throw new Error(`'${this.name}' authentication strategy requires a '${prop}' setting`)
18 }
19 })
20 }
21
22 get configuration() {
23 const authConfig = this.authentication.configuration
24 const config = super.configuration || {}
25
26 return {
27 hashSize: 10,
28 service: authConfig.service,
29 entity: authConfig.entity,
30 entityId: authConfig.entityId,
31 errorMessage: 'Invalid login',
32 entityPasswordField: config.passwordField,
33 entityUsernameField: config.usernameField,
34 ...config
35 }
36 }
37
38 async getEntityQuery(query: Query, _params: Params) {
39 return {
40 $limit: 1,
41 ...query
42 }
43 }
44
45 async findEntity(username: string, params: Params) {
46 const { entityUsernameField, errorMessage } = this.configuration
47 if (!username) {
48 // don't query for users without any condition set.
49 throw new NotAuthenticated(errorMessage)
50 }
51
52 const query = await this.getEntityQuery(
53 {
54 [entityUsernameField]: username
55 },
56 params
57 )
58
59 const findParams = Object.assign({}, params, { query })
60 const entityService = this.entityService
61
62 debug('Finding entity with query', params.query)
63
64 const result = await entityService.find(findParams)
65 const list = Array.isArray(result) ? result : result.data
66
67 if (!Array.isArray(list) || list.length === 0) {
68 debug('No entity found')
69
70 throw new NotAuthenticated(errorMessage)
71 }
72
73 const [entity] = list
74
75 return entity
76 }
77
78 async getEntity(result: any, params: Params) {
79 const entityService = this.entityService
80 const { entityId = (entityService as any).id, entity } = this.configuration
81
82 if (!entityId || result[entityId] === undefined) {
83 throw new NotAuthenticated('Could not get local entity')
84 }
85
86 if (!params.provider) {
87 return result
88 }
89
90 return entityService.get(result[entityId], {
91 ...params,
92 [entity]: result
93 })
94 }
95
96 async comparePassword(entity: any, password: string) {
97 const { entityPasswordField, errorMessage } = this.configuration
98 // find password in entity, this allows for dot notation
99 const hash = get(entity, entityPasswordField)
100
101 if (!hash) {
102 debug(`Record is missing the '${entityPasswordField}' password field`)
103
104 throw new NotAuthenticated(errorMessage)
105 }
106
107 debug('Verifying password')
108
109 const result = await bcrypt.compare(password, hash)
110
111 if (result) {
112 return entity
113 }
114
115 throw new NotAuthenticated(errorMessage)
116 }
117
118 async hashPassword(password: string, _params: Params) {
119 return bcrypt.hash(password, this.configuration.hashSize)
120 }
121
122 async authenticate(data: AuthenticationRequest, params: Params) {
123 const { passwordField, usernameField, entity, errorMessage } = this.configuration
124 const username = get(data, usernameField)
125 const password = get(data, passwordField)
126
127 if (!password) {
128 // exit early if there is no password
129 throw new NotAuthenticated(errorMessage)
130 }
131
132 const { provider, ...paramsWithoutProvider } = params
133
134 const result = await this.findEntity(username, paramsWithoutProvider)
135 await this.comparePassword(result, password)
136
137 return {
138 authentication: { strategy: this.name },
139 [entity]: await this.getEntity(result, params)
140 }
141 }
142}