serverless-offline
serverless-offline copied to clipboard
Serverless Offline only supports retrieving JWT from the headers (undefined)
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
I have to add that my offline is producing weird log outputs as well.
It feels like these could be related.
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
@medikoo is this library still actively maintained? Seems like it has problems supporting httpApi
in Serverless
@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.
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
@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.
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.
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"
This answer https://github.com/dherault/serverless-offline/issues/1078#issuecomment-764657545 helps me, but still i have to comment the event
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
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
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