Introduction#
There are different ways to control access to API Gateway. This note shows two methods
- Part 1. Use cognito userpool and built-in api gw token validation
- Part 2. Use a lambda authorizer
- github
Cognito Authorizer#
According to the docs, both IdToken and AccessToken can be used to control access to API Gateway. In case of access token, it should be created from hosted ui here
create a cognito user pool
export class CognitoAuthorizer extends Stack {public readonly userPool: stringconstructor(scope: Construct, id: string, props: StackProps) {super(scope, id, props)const pool = new aws_cognito.UserPool(this, 'UserPoolDemo', {userPoolName: 'UserPoolDemo',selfSignUpEnabled: true,signInAliases: {email: true},autoVerify: {email: true},removalPolicy: RemovalPolicy.DESTROY})const client = pool.addClient('WebClient', {authFlows: {userPassword: true,adminUserPassword: true,custom: true,userSrp: true},userPoolClientName: 'WebClient'})this.userPool = pool.userPoolArn}}
integrate the cognito user pool with a api gateway method
const apigw = new aws_apigateway.RestApi(this, 'ApiGwCognitoDemo', {restApiName: 'pollyapi',deploy: false})const book = apigw.root.addResource('book')book.addMethod('GET',new aws_apigateway.LambdaIntegration(func, {proxy: true}),{authorizationType: aws_apigateway.AuthorizationType.COGNITO,authorizer: new aws_apigateway.CognitoUserPoolsAuthorizer(this,'CognitoAuthorizer',{cognitoUserPools: [userPool]})})
- Option 1. Use the IdToken
Use this SDK function to get the IdToken
resp = client.admin_initiate_auth(UserPoolId=CONFIG["USER_POOL_ID"],ClientId=CONFIG["APP_CLIENT_ID"],AuthFlow='ADMIN_NO_SRP_AUTH',AuthParameters={"USERNAME": CONFIG["USER_NAME"],"PASSWORD": CONFIG["PASSWORD"]})
Put the IdToken into the Authorization header
response = requests.get(url=CONFIG["API_URL"],headers={"Authorization": f'Bearer {CONFIG["ACCESS_TOKEN"]}'})
- Option 2. Use the access token
Have to setup Cognito hosted UI, cognito resource server, and API Gateway Auth Scope. Get the access token from the hosted UI, then insert the token into request Authorization header
response = requests.get(url=CONFIG["API_URL"],headers={"Authorization": f'Bearer {CONFIG["ACCESS_TOKEN"]}'})
Lambda Authorizer
GitHub this uses cognito and lambda to do api authentication and deply by using CDK. Basic concepts:
- ID providers: Auth0, cognito, oicd provider
- Api gateway auth methods: iam, token based (jwt, oauth2), request based, cognito
- Reference projects: api-gateway-authopenbanking-brazilian-auth-samples
- User request/get a jwt token from cognito
- Cognito response a jwt token to the user
- User send a request with the jwt in the header to apigw
- Apigw call a lambda auth to validate the jwt token
- The lamba auth validate and return a temporary iam policy
- Given the iam policy user request can access things
Cognito User Pool Stack#
export class CongitoUserPool extends Stack {constructor(scope: Construct, id: string, props?: StackProps) {super(scope, id, props)// cognito user poolconst userPool = new aws_cognito.UserPool(this, 'UserPoolApiAuthDemo', {userPoolName: 'UserPoolForApiAuthDemo',selfSignUpEnabled: true,signInAliases: {email: true},autoVerify: {email: true}})// add a clientconst client = userPool.addClient('apigw-auth-demo', {authFlows: {userPassword: true,adminUserPassword: true,userSrp: true,custom: true},userPoolClientName: 'ApiAuthClient'})// app client idconst clientId = client.userPoolClientId}}
pass userPoolId and appClientId from cognito stack to apigw stack in /bin/apigw-auth-app.ts
#!/usr/bin/env nodeimport * as cdk from 'aws-cdk-lib'import { ApiGwAuthStack, CongitoUserPool } from '../lib/apigw-auth-cognito'// appconst app = new cdk.App()// cognito user pool stackconst cognito = new CongitoUserPool(app, 'CognitoUserPool', {})// apigw auth stacknew ApiGwAuthStack(app, 'ApiGwAuthStack', {userPoolId: cognito.userPoolId,appClientId: cognito.appClientId})
ApiGw Auth Stack#
create an api gateway
const api = new aws_apigateway.RestApi(this, 'AuthApiDemo', {restApiName: 'AuthApiDemo'})
create a lambda backend for bussiness logic
const lambdaBackend = new aws_lambda.Function(this, 'LambdaBackend', {functionName: 'LambdaBackend',runtime: aws_lambda.Runtime.PYTHON_3_9,code: aws_lambda.Code.fromAsset(path.join(__dirname, './../lambda')),handler: 'lambda_backend.handler'})
create a lambda authorizer
const lambdaAuthorizer = new aws_lambda.Function(this, 'LambdaAuth', {functionName: 'LambdaAuth',runtime: aws_lambda.Runtime.NODEJS_16_X,code: aws_lambda.Code.fromAsset(path.join(__dirname, './../lambda')),handler: 'lambda_auth.handler',environment: {ACCOUNT_ID: this.account,API_ID: api.restApiId,USER_POOL_ID: props.userPoolId,APP_CLIENT_POOL_ID: props.appClientId}})
create an token based authorizer gateway
const authorizer = new aws_apigateway.TokenAuthorizer(this,'JwtTokenAuthLambda',{handler: lambdaAuthorizer,validationRegex:'^(Bearer )[a-zA-Z0-9-_]+?.[a-zA-Z0-9-_]+?.([a-zA-Z0-9-_]+)$'})
create a lambda integration target for apigw
const apiLambdaIntegration = new aws_apigateway.LambdaIntegration(lambdaBackend,{requestTemplates: { 'application/json': '{ "statusCode": "200" }' }})
create a api resource
const bookResource = api.root.addResource('book')
create a method with authorizer
bookResource.addMethod('GET', apiLambdaIntegration, { authorizer })
The Lamba Authorizer#
This function has two tasks: 1) verify the cognito jwt token 2) generate iam policy.
use aws-jwt-verify lib to verify the cognito jwt token
import { CognitoJwtVerifier } from 'aws-jwt-verify'
validate function
async function verifyAccessToken(accessToken) {// verifier that expects valid access tokens:const verifier = CognitoJwtVerifier.create({userPoolId: process.env.USER_POOL_ID,tokenUse: 'access',clientId: process.env.APP_CLIENT_ID})// decoded tokenlet decodedTokentry {decodedToken = await verifier.verify(accessToken)console.log('Token is valid. Payload:', decodedToken)} catch {decodedToken = {}console.log('Token not valid!')}return decodedToken}
generate iam policy function
function generateIAMPolicy(scopeClaims) {// Declare empty policy statements arrayconst policyStatements = []// Iterate over API Permissionsfor (let i = 0; i < apiPermissions.length; i++) {// Check if token scopes exist in API Permissionif (scopeClaims.indexOf(apiPermissions[i].scope) > -1) {// User token has appropriate scope, add API permission to policy statementspolicyStatements.push(generatePolicyStatement(apiPermissions[i].arn,apiPermissions[i].stage,apiPermissions[i].httpVerb,apiPermissions[i].resource,'Allow'))}}// Check if no policy statements are generated, if so, create default deny all policy statementif (policyStatements.length === 0) {return defaultDenyAllPolicy} else {return generatePolicy('user', policyStatements)}}
and lambda handler
export const handler = async (event, context) => {// Declare Policylet iamPolicy = null// Capture raw token and trim 'Bearer ' string, if presentconst token = event.authorizationToken.replace('Bearer ', '')console.log('JWT Token', token)// Validate tokenawait verifyAccessToken(token).then(data => {// Retrieve token scopesconsole.log('Decoded and Verified JWT Token', JSON.stringify(data))// For testing purposes using a ID token without scopes. If you have an access token with scopes,// uncomment 'data.claims.scp' and pass the array of scopes present in the scp attribute instead.const scopeClaims = ['email'] // data.claims.scp;// Generate IAM PolicyiamPolicy = generateIAMPolicy(scopeClaims)}).catch(err => {console.log(err)iamPolicy = defaultDenyAllPolicy})console.log('IAM Policy', JSON.stringify(iamPolicy))return iamPolicy}
Testing with Boto3#
create a config.json to locally store things for testing. when deploy, can store these in system parameters or secrete maanger, lambda environments, cdk stack references.
{"USER_POOL_ID": "","APP_CLIENT_ID": "","USER_EMAIL": "","USER_NAME": "","PASSWORD": "","API_URL": "","token": ""}
test api auth in python using requests
def test_auth_api():"""test auth api using request and jwt token"""# send request with tokenresponse = requests.get(url=CONFIG["API_URL"],headers={"Authorization": f'Bearer {CONFIG["token"]}'})print(response)print(response.json())
curl option
curl -X GET https://$APIID.execute-api.$REGION.amazonaws.com/prod/book -H "Authorization: Bearer $TOKEN"
Project Structure#
apigw-auth-cognito--bin|--apigw-cognito-auth-app.ts|--apigw-lambda-auth-app.ts--lambda|--lambda_hello.py|--lambda_auth.js|--lamda_backend.py|--package.json--lib|--apigw-auth-cognito.ts|--apigw-cognito-authorizer.ts--test|--index.html|--profile.html|--config.js|--test_auth.py
Basically, need to note a few things
- test_auth.py for testing using SDK
- index.html and profile.html for testing using browser
- it is possible to run html locally or CloudFront
CDK Synth and Deploy Backend#
Before synth and deploy, please ensure to install depedencies. Below steps will do this.
- install dependencies for cdk
- install dependencies for lambda
clone this project
git clone https://github.com/entest-hai/apigw-auth-cognito
go to root project directory
cd apigw-auth-cognito
install dependencies for cdk
npm install package.json
install dependencies for lambda auth (aws-jwt-verify). then cdk will deploy depdencies for this lambda by zipping and uploading.
cdk lambda
and
npm install package.json
Now synth and deploy. When running cdk synth the below command in cdk.json will be executed to synthesize the apigw-cognito-auth-app into cloudformation template.
"app": "npx ts-node --prefer-ts-exts bin/apigw-cognito-auth-app.ts",
To synthesize the lambda authorizer app, thre are two options
- option 1. modify the cdk.json
"app": "npx ts-node --prefer-ts-exts bin/apigw-cognito-auth-app.ts",
- option 2. type below custom command right in the terminal to build the lambda authroizer app A
cdk --app 'npx ts-node --prefer-ts-exts bin/apigw-lambda-auth-app.ts' synth