Skip to content

Instantly share code, notes, and snippets.

@mikaelvesavuori
Last active June 21, 2023 15:06
Show Gist options
  • Save mikaelvesavuori/cc5ba274554dfc774ef1f327182c854b to your computer and use it in GitHub Desktop.
Save mikaelvesavuori/cc5ba274554dfc774ef1f327182c854b to your computer and use it in GitHub Desktop.
Example of an AWS Lambda authorizer function that runs authentication and authorization on a request. Some parts are made up or missing here. Adapted and simplified from production code, using Auth0 and GitHub as the identity provider.
import jwt from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';
import { MikroLog } from 'mikrolog';
const logger = MikroLog.start();
const keyClient: any = (jwksUri: string) =>
jwksClient({
cache: true,
cacheMaxAge: 86400000,
rateLimit: true,
jwksRequestsPerMinute: 10,
jwksUri
});
const verificationOptions: any = (audience: string, issuer: string) => {
return {
algorithms: 'RS256',
audience,
issuer
};
};
/**
* @description Authenticate user by validating and verifying their JWT token.
*/
export async function authenticate(token: string): Promise<Record<string, any>> {
const JWKS_URI = process.env.JWKS_URI || '';
const AUDIENCE = process.env.AUDIENCE || '';
const ISSUER = process.env.ISSUER || '';
if (!JWKS_URI || !AUDIENCE || !ISSUER) throw new Error('MissingAuthorizerEnvVarsError');
const decodedToken: any = jwt.decode(token, { complete: true });
const kid = decodedToken.header.kid;
const result: boolean = await new Promise((resolve, reject) => {
keyClient(JWKS_URI).getSigningKey(kid, (err: any, key: any) => {
if (err) logger.log(err);
const signingKey = key.publicKey || key.rsaPublicKey;
try {
jwt.verify(token, signingKey, verificationOptions(AUDIENCE, ISSUER), (error) => {
if (error) resolve(false);
else resolve(true);
});
} catch (error: any) {
logger.error(error);
reject(error);
}
});
});
return {
isValid: result,
nickname: decodedToken.payload.nickname,
sub: decodedToken.payload.sub
};
}
/**
* @description Encapsulated authorization logic for assessing read/write permissions
* on a GitHub repository.
*/
export async function authorize(options: AuthorizeOptions) {
const { org, repo, nickname, githubToken } = options;
// Just an example; you will probably want to get these from another service
const permissions = {
permission: 'read'
};
const canRead = getReadPermissions(permissions);
const canOperate = getOperatePermissions(permissions);
return {
canRead,
canOperate
};
}
const getReadPermissions = (permissions: Record<string, any>) =>
permissions?.permission === 'admin' ||
permissions?.permission === 'write' ||
permissions?.permission === 'read' ||
false;
const getOperatePermissions = (permissions: Record<string, any>) =>
permissions?.permission === 'admin' || permissions?.permission === 'write' || false;
type AuthorizeOptions = {
org: string;
repo: string;
nickname: string;
githubToken: string;
};
import { APIGatewayProxyEventV2, Context } from 'aws-lambda';
import { MikroLog } from 'mikrolog'
import { authenticate } from './authenticate';
import { authorize } from './authorize';
/**
* @description Basic authorizer when only authentication is enough.
*/
export async function handler(event: APIGatewayProxyEventV2, context: Context): Promise<any> {
const logger = MikroLog.start({ event, context });
try {
if (event?.requestContext?.http?.method === 'OPTIONS') return handleCors();
const token = getToken(event);
const { isValid, nickname, sub } = await authenticate(token);
if (!isValid) return authorizerEndSimple({ isValid, nickname });
// Do stuff here, like getting user tokens for GitHub or whatever
const userToken = {
org: 'someorg',
repo: 'somerepo',
nickname: 'Sam Person',
gitHubToken: 'abc123'
};
await authorize();
return authorizerEndSimple({ isValid, nickname, githubToken });
} catch (error: any) {
logger.error(error);
return authorizerEndSimple({ isValid: false });
}
}
/**
* @description Returns a valid authorizer response for simple, non-permissioned requests.
*/
function authorizerEndSimple(result: any) {
const { isValid } = result;
const response: Record<string, any> = {
isAuthorized: isValid,
context: {
nickname: result.nickname || ''
}
};
if (isValid && result.githubToken) response.context.githubToken = result.githubToken;
return response;
}
/**
* @description Returns a CORS object.
*/
function handleCors() {
return {
statusCode: 200,
headers: {
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify('OK')
};
}
/**
* @description Get token from header and clean it.
*/
function getToken(event: any): string {
const clientToken = event?.headers?.Authorization || event?.headers?.authorization || '';
if (!clientToken) throw new Error('MissingClientTokenError');
if (clientToken.split(' ')[0] === 'Bearer') return clientToken.split(' ')[1];
return clientToken || '';
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment