serverless-offline icon indicating copy to clipboard operation
serverless-offline copied to clipboard

Serverless Offline only supports retrieving JWT from the headers (undefined)

Open vadymhimself opened this issue 3 years ago • 12 comments

Bug Report

I am trying to implement a jwt authorizer as per this guide

Current Behavior

offline: [object Object]
offline: [object Object]
offline: [object Object]
offline: Configuring JWT Authorization: GET /api/v1/users/current
 
 Error ---------------------------------------------------
 
  Error: Serverless Offline only supports retrieving JWT from the headers (undefined)
      at createAuthScheme (/Users/vadym/projects/gigradar-monorepo/gigradar-aws-functions/node_modules/serverless-offline/dist/events/http/createJWTAuthScheme.js:23:11)
      at HttpServer._configureJWTAuthorization (/Users/vadym/projects/gigradar-monorepo/gigradar-aws-functions/node_modules/serverless-offline/dist/events/http/HttpServer.js:354:53)
      at HttpServer.createRoutes (/Users/vadym/projects/gigradar-monorepo/gigradar-aws-functions/node_modules/serverless-offline/dist/events/http/HttpServer.js:483:105)
      at Http._create (/Users/vadym/projects/gigradar-monorepo/gigradar-aws-functions/node_modules/serverless-offline/dist/events/http/Http.js:43:65)
      at /Users/vadym/projects/gigradar-monorepo/gigradar-aws-functions/node_modules/serverless-offline/dist/events/http/Http.js:52:12
      at Array.forEach (<anonymous>)
      at Http.create (/Users/vadym/projects/gigradar-monorepo/gigradar-aws-functions/node_modules/serverless-offline/dist/events/http/Http.js:47:12)
      at ServerlessOffline._createHttp (/Users/vadym/projects/gigradar-monorepo/gigradar-aws-functions/node_modules/serverless-offline/dist/ServerlessOffline.js:256:53)
      at processTicksAndRejections (internal/process/task_queues.js:95:5)
      at async Promise.all (index 0)
      at async ServerlessOffline.start (/Users/vadym/projects/gigradar-monorepo/gigradar-aws-functions/node_modules/serverless-offline/dist/ServerlessOffline.js:161:5)
      at async ServerlessOffline._startWithExplicitEnd (/Users/vadym/projects/gigradar-monorepo/gigradar-aws-functions/node_modules/serverless-offline/dist/ServerlessOffline.js:215:5)
      at async PluginManager.runHooks (/Users/vadym/projects/gigradar-monorepo/gigradar-aws-functions/node_modules/serverless/lib/classes/PluginManager.js:573:35)
      at async PluginManager.invoke (/Users/vadym/projects/gigradar-monorepo/gigradar-aws-functions/node_modules/serverless/lib/classes/PluginManager.js:611:9)
      at async PluginManager.run (/Users/vadym/projects/gigradar-monorepo/gigradar-aws-functions/node_modules/serverless/lib/classes/PluginManager.js:672:7)
      at async Serverless.run (/Users/vadym/projects/gigradar-monorepo/gigradar-aws-functions/node_modules/serverless/lib/Serverless.js:468:5)
      at async /Users/vadym/projects/gigradar-monorepo/gigradar-aws-functions/node_modules/serverless/scripts/serverless.js:832:9
 
     For debugging logs, run again after setting the "SLS_DEBUG=*" environment variable.

Sample Code

  • file: serverless.yml
provider:
  name: aws
  stage: ${self:custom.stage}
  runtime: nodejs12.x
  region: us-west-2
  lambdaHashingVersion: 20201221
  httpApi:
    payload: '2.0'
    cors:
      allowedHeaders:
        - Content-Type
        - Authorization
      allowedMethods:
        - GET
        - OPTIONS
      allowedOrigins:
        - https://localhost:8000
    authorizers:
      accessTokenAuth0:
        identitySource: $request.header.Authorization
        issuerUrl: ${env:JWT_TOKEN_ISSUER}
        audience:
          - ${env:JWT_AUDIENCE}

functions:
  - getCurrentUser:
      handler: api.app
      events:
        - httpApi:
            method: GET
            path: /api/v1/users/current
            authorizer:
              name: accessTokenAuth0

Environment

  • serverless version: 2.67.0
  • serverless-offline version: 8.3.1

vadymhimself avatar Dec 01 '21 01:12 vadymhimself

I have to add that my offline is producing weird log outputs as well. It feels like these could be related. Screen Shot 2021-12-01 at 1 42 13 PM

vadymhimself avatar Dec 01 '21 05:12 vadymhimself

Investigating further I found that serverless-offline tries to set up a JWT authorizer despite the fact that it is declared as type: custom

provider:
  name: aws
  stage: ${self:custom.stage}
  runtime: nodejs12.x
  region: us-west-2
  httpApi:
    shouldStartNameWithService: true
    authorizers:
      msAuthorizer:
        type: request
        functionName: authorizeMemberstack
functions:
  - postProposalV1:
      handler: handler.postProposalV1
      timeout: 900
      events:
        - httpApi:
            path: /v1/proposal
            method: post
            authorizer:
              name: msAuthorizer

  - authorizeMemberstack:
      handler: handler.authorizeV1

Triggers the same error:

offline: Starting Offline: local us-west-2.
offline: Offline [http for lambda] listening on https://localhost:3002
offline: Function names exposed for local invocation by aws-sdk:
           * postProposalV1: gigradar-aws-functions-local-postProposalV1
           * authorizeMemberstack: gigradar-aws-functions-local-authorizeMemberstack
offline: Configuring JWT Authorization: POST /v1/proposal
 
 Error ---------------------------------------------------
 
  Error: Serverless Offline only supports retrieving JWT from the headers (undefined)

Can anyone tell me what am I doing wrong? @daniel-cottone @abdulghani

vadymhimself avatar Dec 01 '21 06:12 vadymhimself

@medikoo is this library still actively maintained? Seems like it has problems supporting httpApi in Serverless

vadymhimself avatar Dec 01 '21 23:12 vadymhimself

@vadymhimself we have limited time handling this library. Still, we'll open for maintainers that may help us with that. Also, we'll try to look into every PR that addresses some important issue.

If you know how to fix it, please propose a PR, and we'll do our best to take it in.

medikoo avatar Dec 02 '21 08:12 medikoo

Getting the same issue here, @vadymhimself (or anyone else landing here) if you still want to be able to still make and partially handle offline requests, you can use the --noAuth flag -> sls offline --noAuth or add the following under custom: in your serverless.yml for that to be default behaviour.

  serverless-offline:
    noPrependStageInUrl: true
    noAuth: true

Of course, this will disable the authorization step for offline calls, but given the alternative of literally nothing working?... it may be useful to have at least partial functionality. If you depend on this plugin to validate your authorization, and you deploy directly to production, then this won't work for you.

For those who practice good development practices and can deploy to a personal or at least development/staging environment and run automated tests against that, then this may be a reasonable compromise in the meantime.

Edit: added extra details so this comment isn't misconstrued by others

mohoromitch avatar Dec 06 '21 16:12 mohoromitch

@vadymhimself I also try to implement the request authorizer and found on the Internet the flag --ignoreJWTSignature to turn off the JWT validation. However, after that, there is still a whole bunch of issues. In your case it will be Function "msAuthorizer" doesn't exist in this Service. The httpApi authorizer doesn't use the configuration from the authorizers.

So, I tried to set the "Function" and use authorizeMemberstack instead of msAuthorizer, but in this case, payload version 2.0 is not supported. The response will be

{
    "statusCode": 403,
    "error": "Forbidden",
    "message": "No principalId set on the Response"
}

Disappointment... Perhaps it is better not to use a separate authorizer and let each function do it. It might even work faster.

@mohoromitch disable authorization and hopes that it will work in production, thanks for the advice.

xr0master avatar Dec 18 '21 22:12 xr0master

I'm not sure if this is directly relevant to the issue/goals from OP, but in case anyone lands here and is are having issues using a custom authorizer with an httpApi (v2) endpoint, the trick seems to be to add the type: request field under the authorizer config directly on the function. This is not how the Serverless configuration specifies it, so serverless-offline is in conflict, but at least it works:

custom:
  serverless-offline:
    ignoreJWTSignature: true

provider:
  httpApi:
    authorizers:
      api-authorizer:
        type: request
        functionName: api-authorizer
        resultTtlInSeconds: 300
        identitySource:
          - $request.header.Authorization # this is ignored by serverless-offline but will default to the Authorization header anyway

functions:
  api-endpoint:
    events:
      - httpApi:
          method: '*'
          path: /
          authorizer:
            name: api-authorizer
            type: request # <-- this is the key part which will "trick" serverless-offline into using a custom authorizer
    handler: '...'

The immediate cause of the issue appears to come from these lines:

https://github.com/dherault/serverless-offline/blob/8d61bde74cdfb37410a5c1952ca608e815eeb1cf/src/events/http/HttpServer.js#L376-L386

Here, none of the settings configured in provider.httpApi.authorizers are used. In fact, it seems the whole thing is really intended for restApi authorizers, and the httpApi request authorizer stuff was halfheartedly cobbled on later.

adieuadieu avatar Jan 20 '22 05:01 adieuadieu

I hope i will hear good news soon ! In the meantime, we are telling developers to comment some code in serverless.yml :( when running "sls offline"

abdennour avatar Apr 09 '22 03:04 abdennour

This answer https://github.com/dherault/serverless-offline/issues/1078#issuecomment-764657545 helps me, but still i have to comment the event

abdennour avatar Apr 09 '22 09:04 abdennour

I just ran into this issue recently, with a perhaps newer version of the plugin (v8.7.0) and what I found was that if you were using a configuration similar to this:

provider:
  name: aws
  runtime: nodejs12.x
  profile: AWS-profile
  stage: ${opt:stage, 'dev'}
  region: AWS-region
  httpApi:
    authorizers:
      authTokenAuthorizer:
        identitySource: '$request.header.Authorization'
        issuerUrl: https://issuer.url/
        audience: https://audience.url

functions:
  profile:
    handler: src/function/function.router
    events:
      - httpApi:
          path: /function/{parameter}
          method: post
          authorizer:
            name: authTokenAuthorizer

What actually ends up happening is that when attempting to build the JWT handling function we end up failing because the plugin doesn't have the capability to actually validate JWTs but we haven't told it not to bother. If you take a look at the following code in authJWTSettingsExtractor.js:

https://github.com/dherault/serverless-offline/blob/81a81a102b03921a10cca9d8cade84fb3aff953a/src/events/http/authJWTSettingsExtractor.js#L27-L34

You can see that there's a cool TODO about actually validating JWTs and then a hard "successful null" exit for this function if you don't have the ignoreJWTSignature parameter set. This all makes sense for local development, however the problem is that this particular problem condition isn't well communicated to the user. Back in HttpServer.js:

https://github.com/dherault/serverless-offline/blob/10afe5e3893cfba50c6cb9cc992faad46cd0188d/src/events/http/HttpServer.js#L307-L310

The null check here seems extraneous since an object will always be returned no matter the outcome. And then slightly later we just pass this jwtSettings result right on into createJWTAuthScheme

https://github.com/dherault/serverless-offline/blob/10afe5e3893cfba50c6cb9cc992faad46cd0188d/src/events/http/HttpServer.js#L330-L331

And hit this code

https://github.com/dherault/serverless-offline/blob/10afe5e3893cfba50c6cb9cc992faad46cd0188d/src/events/http/createJWTAuthScheme.js#L6-L15

And bam, our error: Serverless Offline only supports retrieving JWT from the headers (Undefined) because authorizerName is Undefined because the jwtOptions object passed into it was a null result BECAUSE we never set ignoreJWTSignature.

The long-term solution is to support JWT signature validation but that's of an unknown complexity/effort level. However, in the short term it's probably a good idea to surface this error in a more clear way, it's probably not a big deal for most people to disable signature validation in a local dev environment.

Edit: Better links to source and minor clarity changes

gravaton avatar Apr 27 '22 03:04 gravaton

After digging a lot, I found this example.

https://github.com/dherault/serverless-offline/blob/abc134ab2aaccd5d6d9285ebbabcce4acace7352/tests/integration/custom-authentication/serverless.yml

This works in my particular case since I don't have control over the authorizer I use in production, however, I know that this authorizer injects some particular fields on the context object. Since I don't have access to that code, I've created a custom authorizer that injects THOSE values using serverless-offline based on headers I send when working locally. This way, when working locally, I send headers X-Field1, X-Field2 and X-Field3 and those values populate my context, while production does whatever magic it needs to populate the context. If this is your use case, then the way to create a serverless-offline "lambda authorizer" is to create a custom authentication provider.

Following the example, what I did was:

custom:
  offline:
    customAuthenticationProvider: ./src/localAuth

And then create the file ./src/localAuth.js

module.exports = (endpoint, functionKey, method, path) => {
  return {
    getAuthenticateFunction: () => ({
      async authenticate(request, h) {
        const context = { 
          expected: 'it works',
          awesomeField: request.headers['x-field1'],
          equallyAwesomeField: request.headers['x-field2'],
          particularlyAwesomeField: request.headers['x-field3'],
        }
        return h.authenticated({
          credentials: {
            context,
          },
        })
      },
    }),
    name: functionKey,
    scheme: functionKey,
  }
}

Inside your function, you can of course call these fields using event.requestContext.authorizer.awesomeField. Your auth logic will be inside of the getAuthenticateFunction

rion18 avatar Jul 06 '22 23:07 rion18

so I guess we still don't have a solution for this as the maintainers have limited time in reviewing the issues that are being raised

yasmikash avatar Sep 17 '22 17:09 yasmikash