Skip to content

Instantly share code, notes, and snippets.

@daaru00
Last active July 12, 2023 12:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save daaru00/85bbaf6d9b92b00f88bd00e960bbaefa to your computer and use it in GitHub Desktop.
Save daaru00/85bbaf6d9b92b00f88bd00e960bbaefa to your computer and use it in GitHub Desktop.
Lambda authorizer to check OAuth2 authorization token
const https = require('https');
const jose = require('node-jose');
const region = process.env.COGNITO_REGION;
const userpool_id = process.env.COGNITO_USER_POOL_ID;
const app_client_id = process.env.COGNITO_CLIENT_ID;
const keys_url = 'https://cognito-idp.' + region + '.amazonaws.com/' + userpool_id + '/.well-known/jwks.json';
/**
* Lambda handler
*
* @param {*} event
*/
exports.handler = async (event) => {
const token = event.authorizationToken.replace('Bearer ', '');
console.log('token: ', token);
if (token == undefined || token.toString().trim().length == 0) {
throw new Error('Unauthorized, Authorization token not found');
}
// get the kid from the headers prior to verification
let header = jose.util.base64url.decode(token.split('.')[0]);
header = JSON.parse(header);
const kid = header.kid;
console.log('kid: ', kid);
// download the public keys
const key = await retrivePublicKey(keys_url, kid);
// construct the public key
const publicKey = await jose.JWK.asKey(key);
// verify the signature
const verifyResponse = await jose.JWS.createVerify(publicKey).verify(token);
// now we can use the claims
const claims = JSON.parse(verifyResponse.payload);
console.log('claims: ', claims)
// additionally we can verify the token expiration
current_ts = Math.floor(new Date() / 1000);
if (current_ts > claims.exp) {
throw new Error('Unauthorized: Token is expired');
}
// and verify the Audience
if (claims.client_id != app_client_id) {
throw new Error('Unauthorized: Token was not issued for this audience');
}
// if everything is ok allow user and return claims
return generatePolicy('user', 'Allow', event.methodArn, claims);
}
/**
* Help function to retrive public key
*
* @param {String} url
* @param {String} kid
*/
function retrivePublicKey(url, kid) {
return new Promise((resolve, reject) => {
https.get(url, (response) => {
if (response.statusCode == 200) {
let chunk = '';
response.on('data', (body) => {
chunk += body;
})
response.on('end', () => {
const body = JSON.parse(chunk);
const keyFound = body.keys.find((key) => {
return key.kid == kid;
})
if (keyFound === undefined) {
reject('Unauthorized: Public key not recognized');
} else {
resolve(keyFound);
}
})
}else{
console.error(response)
reject('Internal error: Cannot verifiy using jwks')
}
})
})
}
/**
* Help function to generate an IAM policy
*
* @param {String} principalId
* @param {String} effect (Allow|Deny)
* @param {String} resource
* @param {*} context
*/
function generatePolicy(principalId, effect, resource, context) {
var authResponse = {};
authResponse.principalId = principalId;
if (effect && resource) {
var policyDocument = {};
policyDocument.Version = '2012-10-17';
policyDocument.Statement = [];
var statementOne = {};
statementOne.Action = 'execute-api:Invoke';
statementOne.Effect = effect;
statementOne.Resource = resource;
policyDocument.Statement[0] = statementOne;
authResponse.policyDocument = policyDocument;
}
// Optional output with custom properties of the String, Number or Boolean type.
authResponse.context = context;
return authResponse;
}
service: ProtectAPI
provider:
name: aws
runtime: nodejs8.10
stage: dev
region: eu-west-1
functions:
app:
handler: app.handler
events:
- http:
path: /my-protected-path
method: GET
authorizer:
name: authorizer
resultTtlInSeconds: 0
identitySource: method.request.header.Authorization
type: token
authorizer:
handler: authorizer.handler
environment:
COGNITO_REGION: ${opt:region, self:provider.region}
COGNITO_USER_POOL_ID:
Ref: UserPool
COGNITO_CLIENT_ID:
Ref: UserPoolClient
resources:
Resources:
UserPool:
Type: "AWS::Cognito::UserPool"
Properties:
UserPoolName: "${self:service}-user-pool"
AutoVerifiedAttributes:
- "email"
Schema:
- Name: name
AttributeDataType: String
Mutable: true
Required: true
- Name: email
AttributeDataType: String
Mutable: false
Required: true
UserPoolClient:
Type: "AWS::Cognito::UserPoolClient"
Properties:
ClientName: "${self:service}-client"
GenerateSecret: true
ExplicitAuthFlows:
- ADMIN_NO_SRP_AUTH
UserPoolId:
Ref: "UserPool"
IdentityPool:
Type: "AWS::Cognito::IdentityPool"
Properties:
IdentityPoolName: "${self:service}Identity"
AllowUnauthenticatedIdentities: true
CognitoIdentityProviders:
- ClientId:
Ref: "UserPoolClient"
ProviderName:
"Fn::GetAtt": ["UserPool", "ProviderName"]
IdentityPoolRoleMapping:
Type: "AWS::Cognito::IdentityPoolRoleAttachment"
Properties:
IdentityPoolId:
Ref: "IdentityPool"
Roles:
authenticated:
"Fn::GetAtt": ["CognitoAuthorizedRole", "Arn"]
@leonardoviada
Copy link

@daaru00
Copy link
Author

daaru00 commented Jul 12, 2023

Hi @leonardoviada,
would definitely be a better solution, this script is older than the JWT parsing library. In fact it would also render node-jose useless, which it would be nice to replace with aws-jwt-verify.

The related generatePolicy function remains valid, if there isn't some new, slightly more convenient library there too.

@leonardoviada
Copy link

Seems like that's still the default approach, thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment