Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
vadymhimself opened this issue Dec 1, 2021 · 12 comments · Fixed by #1600
Closed

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

vadymhimself opened this issue Dec 1, 2021 · 12 comments · Fixed by #1600

Comments

@vadymhimself
Copy link

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
Copy link
Author

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
Copy link
Author

vadymhimself commented Dec 1, 2021

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
Copy link
Author

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

@medikoo
Copy link
Collaborator

medikoo commented Dec 2, 2021

@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.

@mohoromitch
Copy link

mohoromitch commented Dec 6, 2021

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

@xr0master
Copy link

@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.

@adieuadieu
Copy link
Contributor

adieuadieu commented Jan 20, 2022

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:

const authorizerOptions = {
identitySource: 'method.request.header.Authorization',
identityValidationExpression: '(.*)',
resultTtlInSeconds: '300',
}
if (typeof endpoint.authorizer === 'string') {
authorizerOptions.name = authFunctionName
} else {
Object.assign(authorizerOptions, endpoint.authorizer)
}

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.

@abdennour
Copy link

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
Copy link

abdennour commented Apr 9, 2022

This answer #1078 (comment) helps me, but still i have to comment the event

@gravaton
Copy link

gravaton commented Apr 27, 2022

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:

if (!provider.httpApi || !provider.httpApi.authorizers) {
return buildSuccessResult(null)
}
// TODO: add code that will actually validate a JWT.
if (!ignoreJWTSignature) {
return buildSuccessResult(null)
}

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:

const jwtSettings = this._extractJWTAuthSettings(endpoint)
if (!jwtSettings) {
return null
}

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

// Create the Auth Scheme for the endpoint
const scheme = createJWTAuthScheme(jwtSettings, this)

And hit this code

const authorizerName = jwtOptions.name
const identitySourceMatch = /^\$request.header.((?:\w+-?)+\w+)$/.exec(
jwtOptions.identitySource,
)
if (!identitySourceMatch || identitySourceMatch.length !== 2) {
throw new Error(
`Serverless Offline only supports retrieving JWT from the headers (${authorizerName})`,
)
}

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

@rion18
Copy link
Contributor

rion18 commented Jul 6, 2022

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

@yasmikash
Copy link

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

petetnt added a commit to petetnt/serverless-offline that referenced this issue Apr 17, 2023
This PR fixes the description for enabling custom authentication providers in serverless.yml files. In addition to using the correct keys, I added an usage example and direct linked the referenced integration test example.

Related to dherault#1311
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

9 participants