One of the most important considerations when designing and building an API is security.
Let’s go through the process of creating a Cognito user pool through AWS CDK, then create an API Gateway with a single endpoint that is secured with a Cognito-issued short-lived OAuth access token.
The first step is to create the Cognito User Pool and Client
const userPool = new UserPool(this, env + '_TestUserPool', {
standardAttributes: {
phoneNumber: {required: false}
},
signInAliases: {
email: true
},
selfSignUpEnabled: true,
email: UserPoolEmail.withSES({
sesRegion: 'us-west-2',
fromEmail: 'no-reply@test.com',
fromName: 'Test Sender'
})
});
var callbackUrls = new Array();
callbackUrls.push('https://' + env + '.test.com');
if (env=='dev') {
callbackUrls.push('http://localhost:3000');
}
var poolClient = userPool.addClient(env + '_Test_AppClient', {
oAuth: {
flows: {
authorizationCodeGrant: true
},
callbackUrls: callbackUrls,
},
idTokenValidity: Duration.hours(8),
accessTokenValidity: Duration.hours(8),
});
Use your CI/CD pipeline to provision this Cognito resource to your account.
Confirm that the Cognito user pool was created in the console

Next, create a REST API in API Gateway along with a test endpoint in CDK and provision that.
Creating the API Gateway:
const api = new RestApi(this, "Test_API_" + env,
{
description: 'Test API Gateway',
deployOptions: {
stageName: env
},
// enable CORS
defaultCorsPreflightOptions: {
allowHeaders: [
'Content-Type',
'X-Amz-Date',
'Authorization',
'X-Api-Key',
'Access-Control-Allow-Credentials',
'Access-Control-Allow-Headers',
'Impersonating-User-Sub'
],
allowMethods: ['OPTIONS', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowCredentials: true,
allowOrigins: Cors.ALL_ORIGINS
}
}
);
Creating a test endpoint and secure it:
const authorizer = new CfnAuthorizer(this, env+ '-test-authorizer', {
restApiId: api.restApiId,
type: 'COGNITO_USER_POOLS',
name: env + '_test_cognitoauthorizer',
providerArns: [userPool.userPoolArn], // userPoolArn is userPool.arn value
identitySource: 'method.request.header.Authorization',
});
const tests = api.root.addResource("tests");
var getTestsLambda = new NodejsFunction(this,
"get-tests-lambda",
{
runtime: Runtime.NODEJS_14_X,
entry: "./src/handlers/tests.tsx",
handler: "getTestsHandler",
timeout: Duration.seconds(180),
environment: {ENV: env},
bundling: {
nodeModules: []
}
}
);
var testsEndpoint = tests.addMethod('GET',
new LambdaIntegration(getTestsLambda, {proxy: true})
);
const resourceGetTestsEndpoint = testsEndpoint.node.findChild('Resource');
(resourceGetTestsEndpoint as CfnResource).addPropertyOverride('AuthorizationType', AuthorizationType.COGNITO);
(resourceGetTestsEndpoint as CfnResource).addPropertyOverride('AuthorizerId', { Ref: authorizer.logicalId });
Deploy the API with an associated CName record in Route53:
new CfnOutput(this, 'apiUrl', {value: api.url});
const zone = HostedZone.fromLookup(this, "Zone_" + env, {domainName: 'test.com'});
new CnameRecord(this, 'ApiGatewayRecordSet_' + env, {
zone: zone,
recordName: env + '-api',
domainName: domain.domainNameAliasDomainName
});
Deploy the changes and ensure that your API gets created along with the endpoint.

Finally, secure the endpoint with Congito:
const resourceTestsGetEndpoint = testsEndpoint.node.findChild('Resource');
(resourceTestsGetEndpoint as CfnResource).addPropertyOverride('AuthorizationType', AuthorizationType.COGNITO);
(resourceTestsGetEndpoint as CfnResource).addPropertyOverride('AuthorizerId', { Ref: authorizer.logicalId });
In order to test out our endpoint, navigate to Cognito in AWS Console and create a new user. Then, go to the app client and click “View Hosted UI”. This will open a new tab where you can login.

Login and set a password, then note that the browser will redirect to your redirect URL previously specified (eg. http://localhost:3000). Notice that the URL also includes a “code” in the query string. This is a code that is issued only once and can be used to get our OAuth token via calling the Cognito tokens endpoint. That token can then be used in the Authorization header for all secured endpoints, until it expires. After the token expires, your application should take care of getting a new token to use.
Here is some sample code that uses the aforementioned code in the query string and fetches a token from the tokens endpoint:
const axios = require('axios');
try {
var eventBody = '';
if (event.body) {
eventBody = event.body;
}
var redirectUri = 'https://dev-auth.test.com/';
if (event.headers.origin == 'http://localhost:3000') {
redirectUri = 'http://localhost:3000';
}
const acode = JSON.parse(eventBody);
const details:any = {
grant_type: "authorization_code",
code: <INSERT AUTH CODE HERE>,
client_id: <INSERT CLIENT ID HERE>,
redirect_uri: redirectUri
};
const formBody = Object.keys(details).map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(details[key])}`).join("&");
await axios.post('https://dev-auth.test.com/oauth2/token', formBody, {
headers: {
"Content-Type": "application/x-www-form-urlencoded"
}
}).then((res:any) => {
console.log(res.data.access_token);
console.log(res.data.id_token);
}).catch((err:any) => {
console.log(err);
});
It’s a good idea to put this behind an endpoint that gets called when the user logs in. Take the token that gets returned and persist it in local storage to use for every subsequent API call that requires authorization. Logging out should just clear the token from local memory, forcing the user to log in again in order to access the application.
To validate that the authorization is working, we can make 2 separate requests, one without the Authorization header and one with the Authorization header including our issued token.
Without Authorization header:

With Authorization header:

Conclusion
One of the main advantages of working entirely within the AWS ecosystem is how well everything integrates together. With a handful of lines of code, we can create our identity provider in Cognito, and secure our API endpoints using the OAuth tokens that it issues.