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
Add optional JWT authorizer for HttpApi events #1022
Changes from all commits
8cfa346
880362d
4400b23
6df1cef
4366059
bc907b1
f8efd51
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import serverlessLog from '../../serverlessLog.js' | ||
|
||
export default function authJWTSettingsExtractor( | ||
endpoint, | ||
provider, | ||
ignoreJWTSignature, | ||
) { | ||
const buildFailureResult = (warningMessage) => { | ||
serverlessLog(warningMessage) | ||
|
||
return { unsupportedAuth: true } | ||
} | ||
|
||
const buildSuccessResult = (authorizerName) => ({ authorizerName }) | ||
|
||
const { authorizer } = endpoint | ||
|
||
if (!authorizer) { | ||
return buildSuccessResult(null) | ||
} | ||
|
||
if (!provider.httpApi || !provider.httpApi.authorizers) { | ||
return buildSuccessResult(null) | ||
} | ||
|
||
// TODO: add code that will actually validate a JWT. | ||
if (!ignoreJWTSignature) { | ||
return buildSuccessResult(null) | ||
} | ||
|
||
if (!authorizer.name) { | ||
return buildFailureResult( | ||
'WARNING: Serverless Offline supports only JWT authorizers referenced by name', | ||
) | ||
} | ||
|
||
const httpApiAuthorizer = provider.httpApi.authorizers[authorizer.name] | ||
|
||
if (!httpApiAuthorizer) { | ||
return buildFailureResult( | ||
`WARNING: JWT authorizer ${authorizer.name} not found`, | ||
) | ||
} | ||
|
||
if (!httpApiAuthorizer.identitySource) { | ||
return buildFailureResult( | ||
`WARNING: JWT authorizer ${authorizer.name} missing identity source`, | ||
) | ||
} | ||
|
||
if (!httpApiAuthorizer.issuerUrl) { | ||
return buildFailureResult( | ||
`WARNING: JWT authorizer ${authorizer.name} missing issuer url`, | ||
) | ||
} | ||
|
||
if (!httpApiAuthorizer.audience || httpApiAuthorizer.audience.length === 0) { | ||
return buildFailureResult( | ||
`WARNING: JWT authorizer ${authorizer.name} missing audience`, | ||
) | ||
} | ||
|
||
const result = { | ||
authorizerName: authorizer.name, | ||
...authorizer, | ||
...httpApiAuthorizer, | ||
} | ||
|
||
return result | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import Boom from '@hapi/boom' | ||
import jwt from 'jsonwebtoken' | ||
import serverlessLog from '../../serverlessLog.js' | ||
|
||
export default function createAuthScheme(jwtOptions) { | ||
const authorizerName = jwtOptions.name | ||
|
||
const identitySourceMatch = /^\$request.header.((?:\w+-?)+\w+)$/.exec( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we get a comment roughly describing what this does for the people who are terrible with regex (like myself)? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I struggle with regex too - here's the explanation from regex 101
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suspect it's parsing the authorization header btw @chardos There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The regex is just copied from src/events/http/createAuthScheme.js I did a little digging in the git history. Based on this commit: the regex was modified from just grabbing all characters (the This doesn't actually parse the auth header. It figures out what the name of the header key is if you have customized the header value that will actually contain the token. That header key is then used when the requests are coming in to grab the auth token from the request. |
||
jwtOptions.identitySource, | ||
) | ||
if (!identitySourceMatch || identitySourceMatch.length !== 2) { | ||
throw new Error( | ||
`Serverless Offline only supports retrieving JWT from the headers (${authorizerName})`, | ||
) | ||
} | ||
const identityHeader = identitySourceMatch[1].toLowerCase() | ||
|
||
// Create Auth Scheme | ||
return () => ({ | ||
async authenticate(request, h) { | ||
console.log('') // Just to make things a little pretty | ||
|
||
// TODO: this only validates specific properties of the JWT | ||
// it does not verify the JWT is correctly signed. That would | ||
// be a great feature to add under an optional flag :) | ||
|
||
serverlessLog( | ||
`Running JWT Authorization function for ${request.method} ${request.path} (${authorizerName})`, | ||
) | ||
|
||
// Get Authorization header | ||
const { req } = request.raw | ||
let jwtToken = req.headers[identityHeader] | ||
if (jwtToken && jwtToken.split(' ')[0] === 'Bearer') { | ||
;[, jwtToken] = jwtToken.split(' ') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using array destructuring is a clever way of leveraging the output of split |
||
} | ||
|
||
try { | ||
const decoded = jwt.decode(jwtToken, { complete: true }) | ||
if (!decoded) { | ||
return Boom.unauthorized('JWT not decoded') | ||
} | ||
|
||
const expirationDate = new Date(decoded.payload.exp * 1000) | ||
if (expirationDate.valueOf() < Date.now()) { | ||
return Boom.unauthorized('JWT Token expired') | ||
} | ||
|
||
const { iss, aud, scope } = decoded.payload | ||
const clientId = decoded.payload.client_id | ||
if (iss !== jwtOptions.issuerUrl) { | ||
serverlessLog(`JWT Token not from correct issuer url`) | ||
return Boom.unauthorized('JWT Token not from correct issuer url') | ||
} | ||
|
||
if ( | ||
!jwtOptions.audience.includes(aud) && | ||
!jwtOptions.audience.includes(clientId) | ||
) { | ||
serverlessLog(`JWT Token not from correct audience`) | ||
return Boom.unauthorized('JWT Token not from correct audience') | ||
} | ||
|
||
let scopes = null | ||
if (jwtOptions.scopes && jwtOptions.scopes.length) { | ||
if (!scope) { | ||
serverlessLog(`JWT Token missing valid scope`) | ||
return Boom.forbidden('JWT Token missing valid scope') | ||
} | ||
|
||
scopes = scope.split(' ') | ||
if ( | ||
scopes.every((s) => { | ||
return !jwtOptions.scopes.includes(s) | ||
}) | ||
) { | ||
serverlessLog(`JWT Token missing valid scope`) | ||
return Boom.forbidden('JWT Token missing valid scope') | ||
} | ||
} | ||
|
||
serverlessLog(`JWT Token validated`) | ||
|
||
// Set the credentials for the rest of the pipeline | ||
// return resolve( | ||
return h.authenticated({ | ||
credentials: { | ||
claims: decoded.payload, | ||
scopes, | ||
}, | ||
}) | ||
} catch (err) { | ||
serverlessLog(`JWT could not be decoded`) | ||
serverlessLog(err) | ||
|
||
return Boom.unauthorized('Unauthorized') | ||
} | ||
}, | ||
}) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
'use strict' | ||
|
||
exports.user = async function get(context) { | ||
return { | ||
body: JSON.stringify({ | ||
status: 'authorized', | ||
requestContext: { | ||
claims: context.requestContext.authorizer.claims, | ||
scopes: context.requestContext.authorizer.scopes, | ||
}, | ||
}), | ||
statusCode: 200, | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would it be better to throw an error or return failure result if the user specifies that the signature should not be ignored?
They may be unaware that the signature isn't being validated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you don't set
ignoreJWTSignature
you will have to setnoAuth
for serverless-offline to run at all for HttpApi events with authorization. The current behavior of serverless-offline is to treat authorization as another lambda. Since there isn't a function mapped to the authorizer, serverless-offline throws an error on startup. My intention with this flag is to make it explicit that while the authorization rules defined in the serverless.yml file are being tested, this is should not be considered valid security.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let me know if you'd like to see the change you suggested. my intention was not to modify the current behavior of the library.