Important:
Click on the “pets” endpoint and click on the “Enable CORS” button to enable “Cross-Origin Resource Sharing” to be able to call the API from the JavaScript application later on. Read more here.
At this point, you should be able to invoke your API. You can test it by simply going to your browser and hitting the “/pets” URL (it is the “Invoke URL” for the GET found under the “Stages” menu on the left)
Next, while we’re still in the Amazon console, we'll create a Lambda authorizer that will validate Auth0 tokens.
Auth0LambdaAuthorizer
.Below is the code for a basic Lambda authorizer. This code validates the JWT token, checks the issuer, and validates a custom risk assessment claim. It does not use any dependencies, so you can simply paste it into the Lambda code without zipping it up and uploading. If you would like to use a prebuilt JWT verification library, you will need to create a project locally, compress the files, and upload it to the Lambda function using the “Upload from” button.
/* global fetch */
import { TextEncoder } from 'util';
import crypto from 'crypto';
//For best practice, you should use the Environmental Variables
const Auth0Domain = process.env.AUTH0_DOMAIN||'<your Auth0 domain';
// Fetch JWKS from Auth0 and cache it so we don't have to fetch it every single time the API is invoked
const jwks_doc = await fetch(`https://${Auth0Domain}/.well-known/jwks.json`)
const jwks = await jwks_doc.json();
// Decode base64 URL encoded strings
const base64UrlDecode = (str) => {
return Buffer.from(str, 'base64').toString('utf8');
};
// Verify the signature of the JWT
const verifyJwtSignature = async (token, jwks) => {
const [headerB64, payloadB64, signatureB64] = token.split('.');
const header = JSON.parse(base64UrlDecode(headerB64));
const kid = header.kid;
const key = jwks.keys.find(key => key.kid === kid);
if (!key) {
throw new Error('Unable to find the appropriate key');
}
const publicKey = crypto.createPublicKey({
key: `-----BEGIN CERTIFICATE-----\n${key.x5c[0]}\n-----END CERTIFICATE-----`,
format: 'pem'
});
const data = `${headerB64}.${payloadB64}`;
const signature = Buffer.from(signatureB64, 'base64');
return crypto.verify(
'sha256',
new TextEncoder().encode(data),
publicKey,
signature
);
};
export const handler = async (event) => {
console.log("event: ",event);
// If your Authorizer setting for 'Lambda event payload' is Request and you specified 'authorization' as a header
//const token = event.headers.Authorization?.split(' ')[1];
// If your Authorizer setting for 'Lambda event payload' is Token and you specified 'Authorization' as a header
const token = event.authorizationToken?.split(' ')[1];
if (!token) {
return generatePolicy(event.methodArn,'Deny');
}
try {
const payload = JSON.parse(base64UrlDecode(token.split('.')[1]));
// console.log("payload: ",payload);
// Verify that the clientId in the token matches your client id configured in Auth0
const clientIdValid = payload['azp'] === process.env.OIDC_CLIENT_ID;
//Verify the token signature
const sig_valid = await verifyJwtSignature(token, jwks);
//Verify the token has not expired
var currentTimestamp = new Date().getTime() / 1000;
const tokenIsNotExpired = payload['exp'] > currentTimestamp;
const valid = tokenIsNotExpired && sig_valid && clientIdValid;
if (!valid) {
throw new Error('Invalid token');
}
// Audience claim validation
if(!payload['aud'].includes('api://pets')) {
return generatePolicy(event.methodArn,'Deny');
}
// Scope validation
if(!payload['scope'].includes('read:pets')) {
return generatePolicy(event.methodArn,'Deny');
}
// Custom claim validation (e.g., checking the risk level)
if (payload['risk_score_confidence'] && payload['risk_score_confidence'] === 'high') {
return generatePolicy(event.methodArn,'Allow', payload.sub);
} else {
return generatePolicy(event.methodArn,'Deny', payload.sub);
}
} catch (err) {
console.error('Token validation failed:', err);
return generatePolicy(event.methodArn,'Deny');
}
};
const generatePolicy = (methodArn, effect, principalId = 'user') => {
return {
principalId,
policyDocument: {
Version: '2012-10-17',
Statement: [
{
Action: 'execute-api:Invoke',
Effect: effect,
Resource: methodArn,
}
]
}
};
};
NOTE:
In the Configuration tab of Lambda, create two environmental variables, one for your Auth0 domain and another one for the Client ID of your application in Auth0.
jwt.verify
method checks the token's signature. Additionally, as a good practice, you should check the issuer.risk_assessment_score
, assuming it's added in the Auth0 Action (we will do this later) if the claim is anything other than high
, access is denied.Auth0_token_validation
and choose your Auth0LambdaAuthorizer
function.Auth0LambdaAuthorizer
function.Now, we need to configure things in Auth0. We’ll start with creating a logical representation of the Pets API so that we can get an access token after the user authenticates (and effectively authorizes), then we’ll create an Action to enrich the Access token with additional claims (Okta’s own risk assessment score), and finally we’ll create a client (aka application which is the logical representation of the actual Javascript application) for our Javascript frontend in order to be able to test everything out.
Go to your Auth0 tenant (if you do not have one, go here and create it…it’s free!)
exports.onExecutePostLogin = async (event, api) => {
// Getting a risk score is part of the Adaptive MFA package and might require an Enterprise level license.
// If you do not have a risk assessment feature enabled in your tenant, you can simply hardcode the claim value for testing purposes
// e.g. api.accessToken.setCustomClaim('risk_score_confidence',’high’);
if(event.authentication && event.authentication.riskAssessment)
api.accessToken.setCustomClaim('risk_score_confidence',event.authentication.riskAssessment.confidence);
};
NOTE:
Risk Assessment is part of the Adaptive MFA offering; you can read about it here.
Let's create a simple JavaScript app that authenticates a user and calls the secured API.
First, let’s create an Application (client) in Auth0 so we can get the ClientId and Secret so that we can interact with our sample Javascript app that we will create next.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Auth0 API Gateway Example</title>
<style>
</style>
</head>
<body>
<h1>Simple app to authenticate with Auth0 then call the Pets API on Amazon API Gateway</h1>
<button id="login" style="display:none">Login</button>
<button id="getATBtn" style="display:none">Get Access Token</button>
<button id="callapi" style="display:none">Call Pets API</button>
<button id="logout" style="display:none">Logout</button>
<div id="result" style="display: none;"></div>
<script src="https://cdn.auth0.com/js/auth0-spa-js/2.0/auth0-spa-js.production.js"></script>
<script>
let loginBtn, logoutBtn, getATBtn, callApiBtn, result;
let accessToken;
//document.addEventListener('DOMContentLoaded', init, false);
//async function init() {
loginBtn = document.querySelector('#login');
getATBtn = document.querySelector('#getATBtn');
logoutBtn = document.querySelector('#logout');
callApiBtn = document.querySelector('#callapi');
result = document.querySelector('#result');
const auth0Client = auth0.createAuth0Client({
domain: "<your Auth0 domain>",
clientId: "<your client id>",
authorizationParams: {
audience: "api://pets",
redirect_uri: window.location.origin,
scope: "read:pets"
}
}).then(async (auth0Client) => {
// handle coming back from login
if (location.search.includes("state=") &&
(location.search.includes("code=") ||
location.search.includes("error="))) {
await auth0Client.handleRedirectCallback();
window.history.replaceState({}, document.title, "/");
}
const isAuthenticated = await auth0Client.isAuthenticated();
console.log('isAuthenticated', isAuthenticated);
if(!isAuthenticated) loginBtn.style.display = '';
else {
logoutBtn.style.display = null;
getATBtn.style.display = null;
}
loginBtn.addEventListener("click", e => {
e.preventDefault();
auth0Client.loginWithRedirect();
});
logoutBtn.addEventListener("click", e => {
e.preventDefault();
auth0Client.logout();
});
getATBtn.addEventListener("click", async e => {
e.preventDefault();
/*const differentAudienceOptions = {
authorizationParams: {
audience: "api://pets2",
scope: "read:pets",
redirect_uri: window.location.origin
},
cacheMode: "off"
};*/
accessToken = await auth0Client.getTokenSilently();
result.style.display = null;
document.getElementById('result').innerHTML = "Access token: " + `<a href='https://jwt.io?token=${accessToken}' target="_blank">` + JSON.stringify(accessToken, null, 2) + "</a>";
callApiBtn.style.display = null;
});
callApiBtn.addEventListener('click', async e => {
e.preventDefault();
if (!accessToken) {
alert('No access token found locally');
return;
}
try {
const response = await fetch('<Amazon API endpoint for GET /pets>', {
headers: {
Authorization: `Bearer ${accessToken}`
}
});
const data = await response.json();
result.style.display = null;
document.getElementById('result').textContent = "Pets API call response: " + JSON.stringify(data, null, 2);
} catch (err) {
console.error('API call failed', err);
}
});
})
</script>
</body>
</html>
http://localhost:12345
if you ran the previous python command) and authenticate. After you are back on the application, you should see the “Get Access token” button. Clicking on that should fetch the Access token (you can click on the token link, which will take you to jwt.io to see the content of the token) and enable the “Call Pets API” button, which, when clicked, should call the API on Amazon and (hopefully 🙂) return the list of pets. Ta-da!In this article, we covered how to protect a REST API behind Amazon API Gateway using Okta's Customer Identity Cloud (powered by Auth0). By leveraging a Lambda authorizer, we were able to validate Auth0 tokens and control access based on custom claims. Finally, we demonstrated the setup with a simple JavaScript application that authenticated a user and called the API using the Bearer token. This setup ensures that only authenticated and risk-assessed users can access your API, providing a robust security layer.
Source: auth0.com