node.jsoauthfeathersjsfeathers-authenticationfeathers-vuex

Unhandled rejection at promise with feathersjs oauth call


I'm trying to create a frontend Vue(x) app with feathers-vuex and a backend feathers API using only oauth for authentication.

If I hit the backend directly on localhost:3030/oauth/google then the right flow and re-directs occur with google and I end up back at localhost:8080/#/access_token=ey.... with a valid jwt so the oauth config seems fine on the backend and with the oauth configuration at google. The configuration is..

"oauth": {
  "redirect": "http://localhost:8080/",
  "google": {
    "key": "GOOGLE_CLIENT_KEY",
    "secret": "GOOGLE_CLIENT_SECRET",
    "scope": [
      "email",
      "profile",
      "openid"
    ]
  }
}

However, from my front end app running on localhost:8080 when I click something that invokes the login method..

login() {
  this.$store.dispatch('auth/authenticate', {strategy: 'google'})
}

.. I get a rejected promise on the backend. With DEBUG=feathers*,@feathersjs* I see the following output:


  @feathersjs/transport-commons Got 'create' call for service 'authentication' +0ms
  @feathersjs/transport-commons Running method 'create' on service 'authentication' {
  provider: 'socketio',
  headers: {
    host: 'localhost:3030',
    connection: 'Upgrade',
    pragma: 'no-cache',
    'cache-control': 'no-cache',
    'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.79 Safari/537.36',
    upgrade: 'websocket',
    origin: 'http://localhost:8080',
    'sec-websocket-version': '13',
    'accept-encoding': 'gzip, deflate, br',
    'accept-language': 'en-GB,en-US;q=0.9,en;q=0.8',
    cookie: 'connect.sid=s%3ACZWmOVc_1gtoJwDYsb0kvJ7vx06U-Rmd.jbQnNQL1GlFLWsJzyDP312ALpnI32mmYK7BIRka9Ro4',
    'sec-websocket-key': 'umfZAU0K/7k+sKUg1A/wvA==',
    'sec-websocket-extensions': 'permessage-deflate; client_max_window_bits'
  }
} [ { strategy: 'google' }, {}, [Function] ] +0ms
  @feathersjs/authentication/base Running authenticate for strategy google [ 'jwt', 'google' ] +16s
  @feathersjs/authentication-oauth/strategy getProfile of oAuth profile from grant-profile with { strategy: 'google' } +0ms
  @feathersjs/transport-commons Error in method 'create' on service 'authentication' Error: 401 Unauthorized
    at module.exports (/home/darren/projects/stbgfc/develop/admin-api/node_modules/request-compose/utils/error.js:3:15)
    at /home/darren/projects/stbgfc/develop/admin-api/node_modules/request-compose/response/status.js:11:11
    at processTicksAndRejections (internal/process/task_queues.js:93:5) {
  message: '401 Unauthorized',
  res: IncomingMessage {
    _readableState: ReadableState {
      objectMode: false,
      highWaterMark: 16384,
      buffer: BufferList { head: null, tail: null, length: 0 },
      length: 0,
      pipes: null,
      pipesCount: 0,
      flowing: true,
      ended: true,
      endEmitted: true,
      reading: false,
      sync: false,
      needReadable: false,
      emittedReadable: false,
      readableListening: false,
      resumeScheduled: false,
      paused: false,
      emitClose: true,
      autoDestroy: false,
      destroyed: false,
      defaultEncoding: 'utf8',
      awaitDrain: 0,
      readingMore: false,
      decoder: null,
      encoding: null
    },
    readable: false,
    _events: [Object: null prototype] {
      end: [Array],
      data: [Function],
      error: [Function]
    },
    _eventsCount: 3,
    _maxListeners: undefined,
    socket: TLSSocket {
      _tlsOptions: [Object],
      _secureEstablished: true,
      _securePending: false,
      _newSessionPending: false,
      _controlReleased: true,
      _SNICallback: null,
      servername: 'openidconnect.googleapis.com',
      alpnProtocol: false,
      authorized: true,
      authorizationError: null,
      encrypted: true,
      _events: [Object: null prototype],
      _eventsCount: 11,
      connecting: false,
      _hadError: false,
      _parent: null,
      _host: 'openidconnect.googleapis.com',
      _readableState: [ReadableState],
      readable: false,
      _maxListeners: undefined,
      _writableState: [WritableState],
      writable: false,
      allowHalfOpen: false,
      _sockname: null,
      _pendingData: null,
      _pendingEncoding: '',
      server: undefined,
      _server: null,
      ssl: null,
      _requestCert: true,
      _rejectUnauthorized: true,
      timeout: 5000,
      parser: null,
      _httpMessage: [ClientRequest],
      write: [Function: writeAfterFIN],
      [Symbol(res)]: [TLSWrap],
      [Symbol(asyncId)]: 125,
      [Symbol(kHandle)]: null,
      [Symbol(lastWriteQueueSize)]: 0,
      [Symbol(timeout)]: Timeout {
        _idleTimeout: -1,
        _idlePrev: null,
        _idleNext: null,
        _idleStart: 19758,
        _onTimeout: null,
        _timerArgs: undefined,
        _repeat: null,
        _destroyed: true,
        [Symbol(refed)]: null,
        [Symbol(asyncId)]: 135,
        [Symbol(triggerId)]: 133
      },
      [Symbol(kBuffer)]: null,
      [Symbol(kBufferCb)]: null,
      [Symbol(kBufferGen)]: null,
      [Symbol(kBytesRead)]: 689,
      [Symbol(kBytesWritten)]: 150,
      [Symbol(connect-options)]: [Object]
    },
    connection: TLSSocket {
      _tlsOptions: [Object],
      _secureEstablished: true,
      _securePending: false,
      _newSessionPending: false,
      _controlReleased: true,
      _SNICallback: null,
      servername: 'openidconnect.googleapis.com',
      alpnProtocol: false,
      authorized: true,
      authorizationError: null,
      encrypted: true,
      _events: [Object: null prototype],
      _eventsCount: 11,
      connecting: false,
      _hadError: false,
      _parent: null,
      _host: 'openidconnect.googleapis.com',
      _readableState: [ReadableState],
      readable: false,
      _maxListeners: undefined,
      _writableState: [WritableState],
      writable: false,
      allowHalfOpen: false,
      _sockname: null,
      _pendingData: null,
      _pendingEncoding: '',
      server: undefined,
      _server: null,
      ssl: null,
      _requestCert: true,
      _rejectUnauthorized: true,
      timeout: 5000,
      parser: null,
      _httpMessage: [ClientRequest],
      write: [Function: writeAfterFIN],
      [Symbol(res)]: [TLSWrap],
      [Symbol(asyncId)]: 125,
      [Symbol(kHandle)]: null,
      [Symbol(lastWriteQueueSize)]: 0,
      [Symbol(timeout)]: Timeout {
        _idleTimeout: -1,
        _idlePrev: null,
        _idleNext: null,
        _idleStart: 19758,
        _onTimeout: null,
        _timerArgs: undefined,
        _repeat: null,
        _destroyed: true,
        [Symbol(refed)]: null,
        [Symbol(asyncId)]: 135,
        [Symbol(triggerId)]: 133
      },
      [Symbol(kBuffer)]: null,
      [Symbol(kBufferCb)]: null,
      [Symbol(kBufferGen)]: null,
      [Symbol(kBytesRead)]: 689,
      [Symbol(kBytesWritten)]: 150,
      [Symbol(connect-options)]: [Object]
    },
    httpVersionMajor: 1,
    httpVersionMinor: 1,
    httpVersion: '1.1',
    complete: true,
    headers: {
      pragma: 'no-cache',
      date: 'Thu, 02 Jan 2020 22:13:43 GMT',
      'cache-control': 'no-cache, no-store, max-age=0, must-revalidate',
      expires: 'Mon, 01 Jan 1990 00:00:00 GMT',
      'content-type': 'application/json; charset=utf-8',
      vary: 'X-Origin, Referer, Origin,Accept-Encoding',
      server: 'ESF',
      'x-xss-protection': '0',
      'x-frame-options': 'SAMEORIGIN',
      'x-content-type-options': 'nosniff',
      'alt-svc': 'quic=":443"; ma=2592000; v="46,43",h3-Q050=":443"; ma=2592000,h3-Q049=":443"; ma=2592000,h3-Q048=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000',
      'accept-ranges': 'none',
      connection: 'close'
    },
    rawHeaders: [
      'Pragma',
      'no-cache',
      'Date',
      'Thu, 02 Jan 2020 22:13:43 GMT',
      'Cache-Control',
      'no-cache, no-store, max-age=0, must-revalidate',
      'Expires',
      'Mon, 01 Jan 1990 00:00:00 GMT',
      'Content-Type',
      'application/json; charset=utf-8',
      'Vary',
      'X-Origin',
      'Vary',
      'Referer',
      'Server',
      'ESF',
      'X-XSS-Protection',
      '0',
      'X-Frame-Options',
      'SAMEORIGIN',
      'X-Content-Type-Options',
      'nosniff',
      'Alt-Svc',
      'quic=":443"; ma=2592000; v="46,43",h3-Q050=":443"; ma=2592000,h3-Q049=":443"; ma=2592000,h3-Q048=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000',
      'Accept-Ranges',
      'none',
      'Vary',
      'Origin,Accept-Encoding',
      'Connection',
      'close'
    ],
    trailers: {},
    rawTrailers: [],
    aborted: false,
    upgrade: false,
    url: '',
    method: null,
    statusCode: 401,
    statusMessage: 'Unauthorized',
    client: TLSSocket {
      _tlsOptions: [Object],
      _secureEstablished: true,
      _securePending: false,
      _newSessionPending: false,
      _controlReleased: true,
      _SNICallback: null,
      servername: 'openidconnect.googleapis.com',
      alpnProtocol: false,
      authorized: true,
      authorizationError: null,
      encrypted: true,
      _events: [Object: null prototype],
      _eventsCount: 11,
      connecting: false,
      _hadError: false,
      _parent: null,
      _host: 'openidconnect.googleapis.com',
      _readableState: [ReadableState],
      readable: false,
      _maxListeners: undefined,
      _writableState: [WritableState],
      writable: false,
      allowHalfOpen: false,
      _sockname: null,
      _pendingData: null,
      _pendingEncoding: '',
      server: undefined,
      _server: null,
      ssl: null,
      _requestCert: true,
      _rejectUnauthorized: true,
      timeout: 5000,
      parser: null,
      _httpMessage: [ClientRequest],
      write: [Function: writeAfterFIN],
      [Symbol(res)]: [TLSWrap],
      [Symbol(asyncId)]: 125,
      [Symbol(kHandle)]: null,
      [Symbol(lastWriteQueueSize)]: 0,
      [Symbol(timeout)]: Timeout {
        _idleTimeout: -1,
        _idlePrev: null,
        _idleNext: null,
        _idleStart: 19758,
        _onTimeout: null,
        _timerArgs: undefined,
        _repeat: null,
        _destroyed: true,
        [Symbol(refed)]: null,
        [Symbol(asyncId)]: 135,
        [Symbol(triggerId)]: 133
      },
      [Symbol(kBuffer)]: null,
      [Symbol(kBufferCb)]: null,
      [Symbol(kBufferGen)]: null,
      [Symbol(kBytesRead)]: 689,
      [Symbol(kBytesWritten)]: 150,
      [Symbol(connect-options)]: [Object]
    },
    _consuming: true,
    _dumped: false,
    req: ClientRequest {
      _events: [Object: null prototype],
      _eventsCount: 5,
      _maxListeners: undefined,
      outputData: [],
      outputSize: 0,
      writable: true,
      _last: true,
      chunkedEncoding: false,
      shouldKeepAlive: false,
      useChunkedEncodingByDefault: false,
      sendDate: false,
      _removedConnection: false,
      _removedContLen: false,
      _removedTE: false,
      _contentLength: 0,
      _hasBody: true,
      _trailer: '',
      finished: true,
      _headerSent: true,
      socket: [TLSSocket],
      connection: [TLSSocket],
      _header: 'GET /v1/userinfo HTTP/1.1\r\n' +
        'user-agent: grant-profile 0.0.8\r\n' +
        'authorization: Bearer undefined\r\n' +
        'Host: openidconnect.googleapis.com\r\n' +
        'Connection: close\r\n' +
        '\r\n',
      _onPendingData: [Function: noopPendingOutput],
      agent: [Agent],
      socketPath: undefined,
      timeout: 5000,
      method: 'GET',
      path: '/v1/userinfo',
      _ended: true,
      res: [Circular],
      aborted: false,
      timeoutCb: [Function: emitRequestTimeout],
      upgradeOrConnect: false,
      parser: null,
      maxHeadersCount: null,
      [Symbol(kNeedDrain)]: false,
      [Symbol(isCorked)]: false,
      [Symbol(kOutHeaders)]: [Object: null prototype]
    }
  },
  body: {
    error: 'invalid_request',
    error_description: 'Invalid Credentials'
  },
  raw: '{\n' +
    '  "error": "invalid_request",\n' +
    '  "error_description": "Invalid Credentials"\n' +
    '}',
  hook: {
    type: 'before',
    arguments: [ [Object], [Object] ],
    service: {
      app: [EventEmitter],
      strategies: [Object],
      configKey: 'authentication',
      create: [Function: newMethod],
      remove: [Function: newMethod],
      methods: [Object],
      hooks: [Function: hooks],
      _events: [Object: null prototype],
      _eventsCount: 2,
      _maxListeners: undefined,
      setMaxListeners: [Function: setMaxListeners],
      getMaxListeners: [Function: getMaxListeners],
      emit: [Function: emit],
      addListener: [Function: addListener],
      on: [Function: addListener],
      prependListener: [Function: prependListener],
      once: [Function: once],
      prependOnceListener: [Function: prependOnceListener],
      removeListener: [Function: removeListener],
      off: [Function: removeListener],
      removeAllListeners: [Function: removeAllListeners],
      listeners: [Function: listeners],
      rawListeners: [Function: rawListeners],
      listenerCount: [Function: listenerCount],
      eventNames: [Function: eventNames],
      publish: [Function: publish],
      registerPublisher: [Function: registerPublisher],
      _super: undefined,
      [Symbol(@feathersjs/transport-commons/publishers)]: [Object]
    },
    app: [Function: app] EventEmitter {
      _events: [Object: null prototype],
      _eventsCount: 6,
      _maxListeners: undefined,
      setMaxListeners: [Function: setMaxListeners],
      getMaxListeners: [Function: getMaxListeners],
      emit: [Function: emit],
      addListener: [Function: addListener],
      on: [Function: addListener],
      prependListener: [Function: prependListener],
      once: [Function: once],
      prependOnceListener: [Function: prependOnceListener],
      removeListener: [Function: removeListener],
      off: [Function: removeListener],
      removeAllListeners: [Function: removeAllListeners],
      listeners: [Function: listeners],
      rawListeners: [Function: rawListeners],
      listenerCount: [Function: listenerCount],
      eventNames: [Function: eventNames],
      init: [Function: init],
      defaultConfiguration: [Function: defaultConfiguration],
      lazyrouter: [Function: lazyrouter],
      handle: [Function: handle],
      use: [Function: newMethod],
      route: [Function: route],
      engine: [Function: engine],
      param: [Function: param],
      set: [Function: set],
      path: [Function: path],
      enabled: [Function: enabled],
      disabled: [Function: disabled],
      enable: [Function: enable],
      disable: [Function: disable],
      acl: [Function],
      bind: [Function],
      checkout: [Function],
      connect: [Function],
      copy: [Function],
      delete: [Function],
      get: [Function],
      head: [Function],
      link: [Function],
      lock: [Function],
      'm-search': [Function],
      merge: [Function],
      mkactivity: [Function],
      mkcalendar: [Function],
      mkcol: [Function],
      move: [Function],
      notify: [Function],
      options: [Function],
      patch: [Function],
      post: [Function],
      propfind: [Function],
      proppatch: [Function],
      purge: [Function],
      put: [Function],
      rebind: [Function],
      report: [Function],
      search: [Function],
      source: [Function],
      subscribe: [Function],
      trace: [Function],
      unbind: [Function],
      unlink: [Function],
      unlock: [Function],
      unsubscribe: [Function],
      all: [Function: all],
      del: [Function],
      render: [Function: render],
      listen: [Function: newMethod],
      request: [IncomingMessage],
      response: [ServerResponse],
      cache: {},
      engines: {},
      settings: [Object],
      locals: [Object: null prototype],
      mountpath: '/',
      configure: [Function: configure],
      service: [Function: service],
      setup: [Function: newMethod],
      version: '4.4.3',
      methods: [Array],
      mixins: [Array],
      services: [Object],
      providers: [Array],
      _setup: false,
      hookTypes: [Array],
      hooks: [Function: hooks],
      eventMappings: [Object],
      _super: undefined,
      _router: [Function],
      rest: [Object],
      channel: [Function: channel],
      publish: [Function: publish],
      registerPublisher: [Function: registerPublisher],
      lookup: [Function: lookup],
      defaultAuthentication: [Function],
      logger: [DerivedLogger],
      io: [Server],
      _isSetup: true,
      [Symbol(@feathersjs/transport-commons/channels)]: [Object],
      [Symbol(@feathersjs/transport-commons/publishers)]: [Object],
      [Symbol(@feathersjs/transport-commons/router)]: [Object]
    },
    method: 'create',
    path: 'authentication',
    data: { strategy: 'google' },
    params: {
      query: {},
      route: {},
      connection: [Object],
      provider: 'socketio',
      headers: [Object]
    }
  }
} +2s
error: Unhandled Rejection at: Promise 

And then a few seconds later, the front-end errors with a timeout due to the call not returning:

Timeout {type: "FeathersError", name: "Timeout", message: "Timeout of 5000ms exceeded calling create on authentication", code: 408, className: "timeout", …}
type: "FeathersError"
name: "Timeout"
message: "Timeout of 5000ms exceeded calling create on authentication"
code: 408
className: "timeout"
data: {timeout: 5000, method: "create", path: "authentication"}
errors: {}
hook: {type: "before", arguments: Array(2), service: {…}, app: {…}, method: "create", …}
stack: "Timeout: Timeout of 5000ms exceeded calling create on authentication↵    at new Timeout (webpack-internal:///./node_modules/@feathersjs/errors/lib/index.js:135:17)↵    at eval (webpack-internal:///./node_modules/@feathersjs/transport-commons/lib/client.js:60:55)"
__proto__: FeathersError

It appears to be complaining that the call to create on the authentication service itself requires authenticating (my reading of the 401 response message that can be seen near the top of the backend debug output).

My feathers-client.js on the front-end is almost verbatim from the docs..

import feathers from '@feathersjs/feathers'
import socketio from '@feathersjs/socketio-client'
import auth from '@feathersjs/authentication-client'
import io from 'socket.io-client'
import { iff, discard } from 'feathers-hooks-common'
import feathersVuex from 'feathers-vuex'

const socket = io('http://localhost:3030', { transports: ['websocket'] })

const feathersClient = feathers()
  .configure(socketio(socket))
  .configure(auth({ 
    storageKey: 'auth',
    storage: window.localStorage 
  }))
  .hooks({
    before: {
      all: [
        iff(
          context => ['create', 'update', 'patch'].includes(context.method),
          discard('__id', '__isTemp')
        )
      ]
    }
  })

export default feathersClient

// Setting up feathers-vuex
const { makeServicePlugin, makeAuthPlugin, BaseModel, models, FeathersVuex } = feathersVuex(
  feathersClient,
  {
    serverAlias: 'api', // optional for working with multiple APIs (this is the default value)
    idField: '_id', // Must match the id field in your database table/collection
    whitelist: ['$regex', '$options']
  }
)

export { makeAuthPlugin, makeServicePlugin, BaseModel, models, FeathersVuex }

.. and given that the backend is invoked at all suggests that the vuex auth plugin is also correctly configured and used, but that could be speculation.

Can anyone suggest pointers as to how to proceed? Unfortunately I've come across no examples of this working anywhere (all examples I've seen with feathers authentication use local strategy only).


Solution

  • For anyone that happens to have similar issue: it seems I had an old and incompatible version of feathers-vuex installed in the project. Updating to 3.3.0 made it work.