Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1022 from Ahana-Inc/add-jwt-authorizer
Add optional JWT authorizer for HttpApi events
- Loading branch information
Showing
11 changed files
with
515 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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( | ||
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(' ') | ||
} | ||
|
||
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') | ||
} | ||
}, | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} | ||
} |
Oops, something went wrong.