feathersjs/feathers

View on GitHub
packages/authentication-oauth/src/strategy.ts

Summary

Maintainability
A
0 mins
Test Coverage
A
96%
import {
  AuthenticationRequest,
  AuthenticationBaseStrategy,
  AuthenticationResult,
  AuthenticationParams
} from '@feathersjs/authentication'
import { Params } from '@feathersjs/feathers'
import { NotAuthenticated } from '@feathersjs/errors'
import { createDebug, _ } from '@feathersjs/commons'
import qs from 'qs'

const debug = createDebug('@feathersjs/authentication-oauth/strategy')

export interface OAuthProfile {
  id?: string | number
  [key: string]: any
}

export class OAuthStrategy extends AuthenticationBaseStrategy {
  get configuration() {
    const { entity, service, entityId, oauth } = this.authentication.configuration
    const config = oauth[this.name] as any

    return {
      entity,
      service,
      entityId,
      ...config
    }
  }

  get entityId(): string {
    const { entityService } = this

    return this.configuration.entityId || (entityService && (entityService as any).id)
  }

  async getEntityQuery(profile: OAuthProfile, _params: Params) {
    return {
      [`${this.name}Id`]: profile.sub || profile.id
    }
  }

  async getEntityData(profile: OAuthProfile, _existingEntity: any, _params: Params) {
    return {
      [`${this.name}Id`]: profile.sub || profile.id
    }
  }

  async getProfile(data: AuthenticationRequest, _params: Params) {
    return data.profile
  }

  async getCurrentEntity(params: Params) {
    const { authentication } = params
    const { entity } = this.configuration

    if (authentication && authentication.strategy) {
      debug('getCurrentEntity with authentication', authentication)

      const { strategy } = authentication
      const authResult = await this.authentication.authenticate(authentication, params, strategy)

      return authResult[entity]
    }

    return null
  }

  async getAllowedOrigin(params?: Params) {
    const { redirect, origins = this.app.get('origins') } = this.authentication.configuration.oauth

    if (Array.isArray(origins)) {
      const referer = params?.headers?.referer || origins[0]
      const allowedOrigin = origins.find((current) => referer.toLowerCase().startsWith(current.toLowerCase()))

      if (!allowedOrigin) {
        throw new NotAuthenticated(`Referer "${referer}" is not allowed.`)
      }

      return allowedOrigin
    }

    return redirect
  }

  async getRedirect(
    data: AuthenticationResult | Error,
    params?: AuthenticationParams
  ): Promise<string | null> {
    const queryRedirect = (params && params.redirect) || ''
    const redirect = await this.getAllowedOrigin(params)

    if (!redirect) {
      return null
    }

    const redirectUrl = `${redirect}${queryRedirect}`
    const separator = redirectUrl.endsWith('?') ? '' : redirect.indexOf('#') !== -1 ? '?' : '#'
    const authResult: AuthenticationResult = data
    const query = authResult.accessToken
      ? { access_token: authResult.accessToken }
      : { error: data.message || 'OAuth Authentication not successful' }

    return `${redirectUrl}${separator}${qs.stringify(query)}`
  }

  async findEntity(profile: OAuthProfile, params: Params) {
    const query = await this.getEntityQuery(profile, params)

    debug('findEntity with query', query)

    const result = await this.entityService.find({
      ...params,
      query
    })
    const [entity = null] = result.data ? result.data : result

    debug('findEntity returning', entity)

    return entity
  }

  async createEntity(profile: OAuthProfile, params: Params) {
    const data = await this.getEntityData(profile, null, params)

    debug('createEntity with data', data)

    return this.entityService.create(data, _.omit(params, 'query'))
  }

  async updateEntity(entity: any, profile: OAuthProfile, params: Params) {
    const id = entity[this.entityId]
    const data = await this.getEntityData(profile, entity, params)

    debug(`updateEntity with id ${id} and data`, data)

    return this.entityService.patch(id, data, _.omit(params, 'query'))
  }

  async getEntity(result: any, params: Params) {
    const { entityService } = this
    const { entityId = (entityService as any).id, entity } = this.configuration

    if (!entityId || result[entityId] === undefined) {
      throw new NotAuthenticated('Could not get oAuth entity')
    }

    if (!params.provider) {
      return result
    }

    return entityService.get(result[entityId], {
      ..._.omit(params, 'query'),
      [entity]: result
    })
  }

  async authenticate(authentication: AuthenticationRequest, originalParams: AuthenticationParams) {
    const entity: string = this.configuration.entity
    const { provider, ...params } = originalParams
    const profile = await this.getProfile(authentication, params)
    const existingEntity = (await this.findEntity(profile, params)) || (await this.getCurrentEntity(params))

    debug('authenticate with (existing) entity', existingEntity)

    const authEntity = !existingEntity
      ? await this.createEntity(profile, params)
      : await this.updateEntity(existingEntity, profile, params)

    return {
      authentication: { strategy: this.name },
      [entity]: await this.getEntity(authEntity, originalParams)
    }
  }
}