This is a demo project to deploy a calculator API using AWS CDK v2 with Python.
The application is based on
- AWS HTTP API serverless API Gateway for the API endpoints,
- Two Lambda functions, one for addition the second for substraciton.
- Cognito for Authentication using OAuth2
- Route 53 to create the DNS record for the API endpoints
The cdk.json
file tells the CDK Toolkit how to execute your app.
This project is set up like a standard Python project. The initialization
process also creates a virtualenv within this project, stored under the .venv
directory. To create the virtualenv it assumes that there is a python3
(or python
for Windows) executable in your path with access to the venv
package. If for any reason the automatic creation of the virtualenv fails,
you can create the virtualenv manually.
To manually create a virtualenv on MacOS and Linux:
$ python -m venv .venv
After the init process completes and the virtualenv is created, you can use the following step to activate your virtualenv.
$ source .venv/bin/activate
If you are a Windows platform, you would activate the virtualenv like this:
% .venv\Scripts\activate.bat
Once the virtualenv is activated, you can install the required dependencies.
$ pip install -r requirements.txt
At this point you can now deploy the CloudFormation template for this code. Assign DNS_DOMAIN a DNS domain from your Route53 account
$ export CDK_DEFAULT_ACCOUNT=012345678910
$ export CDK_DEFAULT_REGION='us-east-1'
$ export DNS_DOMAIN='YOUR_ROUTE53_DOMAIN'
$ cdk synth
This will deploy the API HTTP with the custom domain https://calculator-api.my-domain.com The authentication will done with the endpoint https://calculator-api.my-domain.com/oauth2/token
There are many GUIs you can use to generate the JWT token like Postman or Insomnia. We'll use here the old curl command
Get first the UserPoolID
and the UserPoolClientID
created by the CDK
Uset them with aws cognito-idp
to get the ClientId
and the ClientSecret
$ aws cognito-idp describe-user-pool-client --user-pool-id us-east-1_LX6a3ggxu --client-id 618jhi0ako4kukld7rsh8tuq2g
{
"UserPoolClient": {
"UserPoolId": "us-east-1_LX6a3ggxu",
"ClientName": "Client1",
"ClientId": "618jhi0ako4kukld7rsh8tuq2g",
"ClientSecret": "e174jlu5mdkr6o1vedp5tvc4rs6lei1nqlapufjc14lup7lhim5",
"LastModifiedDate": "2021-12-22T14:30:31.698000-05:00",
"CreationDate": "2021-12-22T14:30:31.698000-05:00",
"RefreshTokenValidity": 30,
"TokenValidityUnits": {},
"ExplicitAuthFlows": [
"ALLOW_CUSTOM_AUTH",
"ALLOW_REFRESH_TOKEN_AUTH",
"ALLOW_USER_SRP_AUTH"
],
"SupportedIdentityProviders": [
"COGNITO"
],
"AllowedOAuthFlows": [
"client_credentials"
],
"AllowedOAuthScopes": [
"UserPoolResourceServer/json.read"
],
"AllowedOAuthFlowsUserPoolClient": true,
"PreventUserExistenceErrors": "ENABLED"
}
}
We'll convert the ClientId and ClientSecret to generate the JWT token (in Base64)
$python
>>> import base64
>>> base64_encoded=base64.b64encode(b'618jhi0ako4kukld7rsh8tuq2g:e174jlu5mdkr6o1vedp5tvc4rs6lei1nqlapufjc14lup7lhim5')
>>> print(base64_encoded)
b'NjE4amhpMGFrbzRrdWtsZDdyc2g4dHVxMmc6ZTE3NGpsdTVtZGtyNm8xdmVkcDV0dmM0cnM2bGVpMW5xbGFwdWZqYzE0bHVwN2xoaW01'
We'll use that JWT token to authenticate with the API HTTP Gateway using the endpoint /oauth2/token
$ curl --location --request POST 'https://calculator-api.my-domain.com/oauth2/token?grant_type=client_credentials&client_id=618jhi0ako4kukld7rsh8tuq2g$scope=AuthIdentifier/json.read' \
--header 'Authorization: Basic NjE4amhpMGFrbzRrdWtsZDdyc2g4dHVxMmc6ZTE3NGpsdTVtZGtyNm8xdmVkcDV0dmM0cnM2bGVpMW5xbGFwdWZqYzE0bHVwN2xoaW01' \
--header 'Content-Type: application/x-x-www-form-urlencoded'
{"access_token":"eyJraWQiOiJRcHNTWU5oVjJFRzNpQ1d6bDd2SVM4N....................................","expires_in":"3600","token_type":"Bearer"}
Now we can query the API Gateway using this token
$ curl --location --request GET 'https://calculator-api.my-domain.com/minus?val1=3&val2=10' \
--header 'Content-Type: application/json' --header 'Authorization: Bearer eyJraWQiOiJRcHNTWU5oVjJFRzNpQ1d6bDd2SVM4N....................................' \
--header 'Content-Type: application/json'
7
The result 7
(=10-3) is returned from the Lambda lambda_minus
However, the default route (in our case the root path) doesn't need authentication
$ curl --request GET https://calculator-api.my-domain.com
Hello! This is the default route.
received event: {"version": "2.0", "routeKey": "$default", "rawPath": "/", "rawQueryString": "", "headers": {"accept": "*/*", "content-length": "0", "host": "calculator-api.my-domain.com", "user-agent": "curl/7.75.0", "x-amzn-trace-id": "Root=1-61c38f44-6762a5b730fd53e1428fcaab", "x-forwarded-for": "44.33.22.11", "x-forwarded-port": "443", "x-forwarded-proto": "https"}, "requestContext": {"accountId": "012345678910", "apiId": "f9va58ihri", "domainName": "calculator-api.my-domain.com", "domainPrefix": "calculator-api", "http": {"method": "GET", "path": "/", "protocol": "HTTP/1.1", "sourceIp": "44.33.22.11", "userAgent": "curl/7.75.0"}, "requestId": "KxNSxgU2IAMEVvc=", "routeKey": "$default", "stage": "$default", "time": "22/Dec/2021:20:49:08 +0000", "timeEpoch": 1640206148700}, "isBase64Encoded": false}(.venv)
Enjoy!