@feathersjs/authentication-local
Version:
Local authentication strategy for @feathers/authentication
143 lines (110 loc) • 4 kB
text/typescript
/* eslint-disable @typescript-eslint/no-unused-vars */
import bcrypt from 'bcryptjs'
import get from 'lodash/get'
import { NotAuthenticated } from '@feathersjs/errors'
import { Query, Params } from '@feathersjs/feathers'
import { AuthenticationRequest, AuthenticationBaseStrategy } from '@feathersjs/authentication'
import { createDebug } from '@feathersjs/commons'
const debug = createDebug('@feathersjs/authentication-local/strategy')
export class LocalStrategy extends AuthenticationBaseStrategy {
verifyConfiguration() {
const config = this.configuration
;['usernameField', 'passwordField'].forEach((prop) => {
if (typeof config[prop] !== 'string') {
throw new Error(`'${this.name}' authentication strategy requires a '${prop}' setting`)
}
})
}
get configuration() {
const authConfig = this.authentication.configuration
const config = super.configuration || {}
return {
hashSize: 10,
service: authConfig.service,
entity: authConfig.entity,
entityId: authConfig.entityId,
errorMessage: 'Invalid login',
entityPasswordField: config.passwordField,
entityUsernameField: config.usernameField,
...config
}
}
async getEntityQuery(query: Query, _params: Params) {
return {
$limit: 1,
...query
}
}
async findEntity(username: string, params: Params) {
const { entityUsernameField, errorMessage } = this.configuration
if (!username) {
// don't query for users without any condition set.
throw new NotAuthenticated(errorMessage)
}
const query = await this.getEntityQuery(
{
[entityUsernameField]: username
},
params
)
const findParams = Object.assign({}, params, { query })
const entityService = this.entityService
debug('Finding entity with query', params.query)
const result = await entityService.find(findParams)
const list = Array.isArray(result) ? result : result.data
if (!Array.isArray(list) || list.length === 0) {
debug('No entity found')
throw new NotAuthenticated(errorMessage)
}
const [entity] = list
return entity
}
async getEntity(result: any, params: Params) {
const entityService = this.entityService
const { entityId = (entityService as any).id, entity } = this.configuration
if (!entityId || result[entityId] === undefined) {
throw new NotAuthenticated('Could not get local entity')
}
if (!params.provider) {
return result
}
return entityService.get(result[entityId], {
...params,
[entity]: result
})
}
async comparePassword(entity: any, password: string) {
const { entityPasswordField, errorMessage } = this.configuration
// find password in entity, this allows for dot notation
const hash = get(entity, entityPasswordField)
if (!hash) {
debug(`Record is missing the '${entityPasswordField}' password field`)
throw new NotAuthenticated(errorMessage)
}
debug('Verifying password')
const result = await bcrypt.compare(password, hash)
if (result) {
return entity
}
throw new NotAuthenticated(errorMessage)
}
async hashPassword(password: string, _params: Params) {
return bcrypt.hash(password, this.configuration.hashSize)
}
async authenticate(data: AuthenticationRequest, params: Params) {
const { passwordField, usernameField, entity, errorMessage } = this.configuration
const username = get(data, usernameField)
const password = get(data, passwordField)
if (!password) {
// exit early if there is no password
throw new NotAuthenticated(errorMessage)
}
const { provider, ...paramsWithoutProvider } = params
const result = await this.findEntity(username, paramsWithoutProvider)
await this.comparePassword(result, password)
return {
authentication: { strategy: this.name },
[entity]: await this.getEntity(result, params)
}
}
}