Skip to content

Instantly share code, notes, and snippets.

@chidindu-ogbonna
Created September 28, 2021 11:44
Show Gist options
  • Save chidindu-ogbonna/42b74d93f5169513f8bfea203706e2f6 to your computer and use it in GitHub Desktop.
Save chidindu-ogbonna/42b74d93f5169513f8bfea203706e2f6 to your computer and use it in GitHub Desktop.
Authorization Helpers for AWS Authorizers
/*global require exports Buffer */
const { promisify } = require("util");
const fetch = require("node-fetch");
const jwkToPem = require("jwk-to-pem");
const jsonwebtoken = require("jsonwebtoken");
/**
* Generate Auth Error
* @param {('invalid_token'|'boye')} code Error message code
* @param {string} message Message of the error
* @returns
*/
const AuthError = (code, message) => {
const error = new Error(message);
error.name = "AuthError";
error.code = `auth/${code}`;
return error;
};
/**
* Grant API access to request
* @param {object} headers Request headers
*/
exports.grantAPIAccess = async (headers) => {
try {
const cognitoIssuer =
headers["X-Cognito-Issuer"] || headers["x-cognito-issuer"];
const authorization = headers["Authorization"] || headers["authorization"];
const token = authorization.split(" ")[1];
const tokenSections = (token || "").split(".");
if (tokenSections.length < 2) {
throw AuthError("invalid_token", "Requested token is incomplete");
}
const headerJSON = Buffer.from(tokenSections[0], "base64").toString("utf8");
const header = JSON.parse(headerJSON);
const keys = await getPublicKeys(cognitoIssuer);
const key = keys[header.kid];
if (key === undefined) {
throw AuthError("invalid_token", "Claim made for unknown kid");
}
const claim = await verifyPromised(token, key.pem);
const currentSeconds = Math.floor(new Date().valueOf() / 1000);
if (currentSeconds > claim.exp || currentSeconds < claim.auth_time) {
throw AuthError("invalid_token", "Claim initiated is expired");
}
return { isValid: true, claim: JSON.stringify(claim) };
} catch (error) {
if (error.name === "TokenExpiredError") {
throw AuthError("expired_token", `${error.name} - ${error.message}`);
}
throw error;
}
};
/**
* Generate IAM policy to access API
* @param {string} principalId
* @param {('Allow'|'Deny')} effect
* @param {string} resouce The event.methodArn. Set as a default value of '*'.
* This is a workaround to this [error](https://stackoverflow.com/questions/50331588/aws-api-gateway-custom-authorizer-strange-showing-error)
* @param {object} context Addition information to add to application
* @returns
*/
exports.generatePolicy = function (
principalId,
effect,
resource = "*",
context = {}
) {
const authResponse = {};
authResponse.principalId = principalId;
if (effect && resource) {
const policyDocument = {};
policyDocument.Version = "2012-10-17";
policyDocument.Statement = [];
const 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;
};
const getPublicKeys = async (cognitoIssuer) => {
const url = `${cognitoIssuer}/.well-known/jwks.json`;
const response = await fetch(url, { method: "get" });
const publicKeys = await response.json();
return publicKeys.keys.reduce((total, currentValue) => {
const pem = jwkToPem(currentValue);
total[currentValue.kid] = { instance: currentValue, pem };
return total;
}, {});
};
const verifyPromised = promisify(jsonwebtoken.verify.bind(jsonwebtoken));
/*global require exports */
const { generatePolicy, grantAPIAccess } = require("../../utils/auth-utils");
const { logEvent } = require("../../utils/lambda");
/**
* Middleware to control access to your API.
*
* @param {import('aws-lambda').APIGatewayAuthorizerEvent} event
* @param {import('aws-lambda').APIGatewayAuthorizerWithContextHandler} context
* @param {import('aws-lambda').APIGatewayAuthorizerCallback} callback
* @returns {import('aws-lambda').APIGatewayAuthorizerResult}
*/
exports.handler = async (event, context, callback) => {
const principalId = "client";
try {
const headers = event.headers;
const response = await grantAPIAccess(headers);
const { claim } = response;
const claimObject = JSON.parse(claim);
const { sub, iss, exp, iat, email, auth_time } = claimObject;
console.log({
sub,
iss,
exp: exp * 1000,
iat: iat * 10000,
auth_time: auth_time * 1000,
email,
});
return callback(
null,
generatePolicy(principalId, "Allow", event.methodArn, response)
);
} catch (error) {
logEvent("error", error);
const denyErrors = ["auth/invalid_token", "auth/expired_token"];
if (denyErrors.includes(error.code)) {
return callback(
null,
generatePolicy(principalId, "Deny", event.methodArn)
);
}
return callback("Unauthorized");
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment