In my post Creating an API with AWS: Part 3: Additional Endpoints, I updated the simple AWS API endpoint with additional endpoints. In this post, I’ll add authentication to the API using AWS Cognito.
Prerequisites
Before you start, you’ll need the following things:
- An AWS account
- An Administrator IAM User with which to use the AWS CLI
- AWS CLI set up on your local machine
- The CloudFormation template from my previous post, Creating an API with AWS: Part 3: Additional Endpoints.
Note that AWS will charge you for creating and hosting this API if you don’t have Free Tier on your account.
Step 1: Add a Cognito User Pool to the CloudFormation Template
Starting with the CloudFormation template from my blog post Creating an API with AWS: Part 3: Additional Endpoints, add a new Cognito User Pool resource called apiuserpool
under the tagLinkTable
resource:
apiuserpool: Type: AWS::Cognito::UserPool Properties: AdminCreateUserConfig: AllowAdminCreateUserOnly: true AutoVerifiedAttributes: - email UsernameAttributes: - email UsernameConfiguration: CaseSensitive: false UserPoolName: apiuserpool
This will allow you to add users with an email address username that can authenticate against the API.
Step 2: Add a Cognito App Client to the CloudFormation Template
Add a new Cognito User Pool Client resource called applicationAPIUserPoolClient
under the apiuserpool
resource:
applicationAPIUserPoolClient: Type: AWS::Cognito::UserPoolClient Properties: AccessTokenValidity: 1 ClientName: applicationapiclient EnableTokenRevocation: true ExplicitAuthFlows: - ALLOW_USER_PASSWORD_AUTH - ALLOW_REFRESH_TOKEN_AUTH - ALLOW_ADMIN_USER_PASSWORD_AUTH GenerateSecret: true IdTokenValidity: 1 PreventUserExistenceErrors: ENABLED RefreshTokenValidity: 1 SupportedIdentityProviders: - COGNITO UserPoolId: !Ref apiuserpool DependsOn: - apiuserpool
This user pool client will allow you to get access tokens and id tokens to use to authenticate as a user against the API.
Step 3: Add an API Gateway Authorizer to the CloudFormation Template
Add a new API Gateway Authorizer resource called apiauth
under the taglink
resource:
apiauth: Type: AWS::ApiGateway::Authorizer Properties: IdentitySource: method.request.header.Authorization Name: apiauth ProviderARNs: - !GetAtt - apiuserpool - Arn RestApiId: !Ref ApplicationAPI Type: COGNITO_USER_POOLS DependsOn: - apiuserpool
This will enable the ApplicationAPI
to use the Cognito User Pool apiuserpool
when authenticating requests. The authorization token will need to be set in the ‘Authorization’ header when making API requests.
Step 4: Add Authentication to each Endpoint Method in the CloudFormation Template
Update each endpoint method (except the OPTIONS methods) to have AuthorizationType
set to COGNITO_USER_POOLS
and the AuthorizerId
set to the API Authorizer ID, !Ref apiauth
. Also update the DependsOn
attribute to include apiauth
. The ProxyResourceLinkGet
method resource is shown as an example:
ProxyResourceLinkGET: Type: AWS::ApiGateway::Method Properties: RestApiId: !Ref ApplicationAPI ResourceId: !Ref link HttpMethod: GET AuthorizationType: COGNITO_USER_POOLS AuthorizerId: !Ref apiauth Integration: Type: AWS_PROXY IntegrationHttpMethod: POST Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${requestHandler.Arn}/invocations IntegrationResponses: - StatusCode: 200 MethodResponses: - StatusCode: 200 ResponseModels: application/json: Empty DependsOn: - ApplicationAPI - apiauth - link
Step 5: Add User Pool ID and Client ID to the Outputs in the CloudFormation Template
Update the Outputs
section with two new attributes, UserPoolID
and APIClientID
:
UserPoolID: Description: The Cognito user pool ID Value: !Ref apiuserpool APIClientID: Description: The API user pool client ID Value: !Ref applicationAPIUserPoolClient
Step 6: Launch the CloudFormation Template
Save the CloudFormation template to a file called setupapidbauth.yml
. With the description updated, the whole file should look like the below:
AWSTemplateFormatVersion: "2010-09-09" Description: Creates a simple API with three endpoints that can be called to POST/GET links, tags and tag links, with a DynamoDB database and Cognito Authentication Resources: linkTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: "link_id" AttributeType: "S" - AttributeName: "link" AttributeType: "S" KeySchema: - AttributeName: "link_id" KeyType: "HASH" - AttributeName: "link" KeyType: "RANGE" ProvisionedThroughput: ReadCapacityUnits: "5" WriteCapacityUnits: "5" TableName: "linkTable" GlobalSecondaryIndexes: - IndexName: "link_index" KeySchema: - AttributeName: "link" KeyType: "HASH" - AttributeName: "link_id" KeyType: "RANGE" Projection: ProjectionType: "KEYS_ONLY" ProvisionedThroughput: ReadCapacityUnits: "5" WriteCapacityUnits: "5" tagTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: "tag_id" AttributeType: "S" - AttributeName: "tag" AttributeType: "S" KeySchema: - AttributeName: "tag_id" KeyType: "HASH" - AttributeName: "tag" KeyType: "RANGE" ProvisionedThroughput: ReadCapacityUnits: "5" WriteCapacityUnits: "5" TableName: "tagTable" GlobalSecondaryIndexes: - IndexName: "tag_index" KeySchema: - AttributeName: "tag" KeyType: "HASH" - AttributeName: "tag_id" KeyType: "RANGE" Projection: ProjectionType: "KEYS_ONLY" ProvisionedThroughput: ReadCapacityUnits: "5" WriteCapacityUnits: "5" tagLinkTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: "tag_id" AttributeType: "S" - AttributeName: "link_id" AttributeType: "S" KeySchema: - AttributeName: "tag_id" KeyType: "HASH" - AttributeName: "link_id" KeyType: "RANGE" ProvisionedThroughput: ReadCapacityUnits: "5" WriteCapacityUnits: "5" TableName: "tagLinkTable" apiuserpool: Type: AWS::Cognito::UserPool Properties: AdminCreateUserConfig: AllowAdminCreateUserOnly: true AutoVerifiedAttributes: - email UsernameAttributes: - email UsernameConfiguration: CaseSensitive: false UserPoolName: apiuserpool applicationAPIUserPoolClient: Type: AWS::Cognito::UserPoolClient Properties: AccessTokenValidity: 1 ClientName: applicationapiclient EnableTokenRevocation: true ExplicitAuthFlows: - ALLOW_USER_PASSWORD_AUTH - ALLOW_REFRESH_TOKEN_AUTH - ALLOW_ADMIN_USER_PASSWORD_AUTH GenerateSecret: true IdTokenValidity: 1 PreventUserExistenceErrors: ENABLED RefreshTokenValidity: 1 SupportedIdentityProviders: - COGNITO UserPoolId: !Ref apiuserpool DependsOn: - apiuserpool requestHandlerLambdaExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: - "lambda.amazonaws.com" Action: - "sts:AssumeRole" Path: "/" Policies: - PolicyName: "root" PolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Action: - logs:CreateLogGroup - logs:PutLogEvents - logs:CreateLogStream Resource: "*" - PolicyName: "db_access" PolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Action: - dynamodb:DeleteItem - dynamodb:GetItem - dynamodb:PutItem - dynamodb:Scan - dynamodb:UpdateItem - dynamodb:Query Resource: !Sub "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/*" RoleName: requestHandlerLambdaExecutionRole requestHandler: Type: "AWS::Lambda::Function" Properties: Code: ZipFile: !Sub | import json import boto3 import uuid # Establish credentials session_var = boto3.session.Session() credentials = session_var.get_credentials() """ --- Parameters --- """ # DynamoDB tables, read lambda function environment variables or initialize with defaults if they not exist. DYNAMODB_LINK_TABLE = 'linkTable' DYNAMODB_TAG_TABLE = 'tagTable' DYNAMODB_TAG_LINK_TABLE = 'tagLinkTable' # Initialize DynamoDB Client dynamodb = boto3.client('dynamodb') def store_link(link): link_id = str(uuid.uuid4()) dynamodb.put_item( TableName=DYNAMODB_LINK_TABLE, Item={ 'link_id': { 'S': str(link_id) }, 'link': { 'S': str(link) } } ) return link_id def store_tag(tag): tag_id = str(uuid.uuid4()) dynamodb.put_item( TableName=DYNAMODB_TAG_TABLE, Item={ 'tag_id': { 'S': str(tag_id) }, 'tag': { 'S': str(tag) } } ) return tag_id def store_taglink(tag_id, link_id): dynamodb.put_item( TableName=DYNAMODB_TAG_LINK_TABLE, Item={ 'tag_id': { 'S': str(tag_id) }, 'link_id': { 'S': str(link_id) } } ) def get_link_id(link): result = dynamodb.query( TableName=DYNAMODB_LINK_TABLE, IndexName='link_index', KeyConditionExpression='link = :link', ExpressionAttributeValues={ ':link': { "S": link } } ) if result['Count'] > 0: link_id = result['Items'][0]['link_id']['S'] else: link_id = store_link(link) return link_id def get_tag_id(tag): result = dynamodb.query( TableName=DYNAMODB_TAG_TABLE, IndexName='tag_index', KeyConditionExpression='tag = :tag', ExpressionAttributeValues={ ':tag': { "S": tag } } ) if result['Count'] > 0: tag_id = result['Items'][0]['tag_id']['S'] else: tag_id = store_tag(tag) return tag_id def get_tags(tag_id=None): tags = [] if tag_id is None: result = dynamodb.scan( TableName=DYNAMODB_TAG_TABLE, Select="SPECIFIC_ATTRIBUTES", ProjectionExpression="tag_id,tag" ) else: # Run a query with a filter on tag_id on the tagTable result = dynamodb.query( TableName=DYNAMODB_TAG_TABLE, KeyConditionExpression='tag_id = :tag_id', ExpressionAttributeValues={ ':tag_id': { "S": tag_id } } ) if result['Count'] > 0: for x in range(result['Count']): tag = {} tag['tag_id'] = result['Items'][x]['tag_id']['S'] tag['tag'] = result['Items'][x]['tag']['S'] tags.append(tag) return tags def extract_link_result(result): links = [] if result['Count'] > 0: for x in range(result['Count']): link = {} link['link_id'] = result['Items'][x]['link_id']['S'] link['link'] = result['Items'][x]['link']['S'] links.append(link) return links def get_links(tag_id=None, link_id=None): links = [] link_id_list = [] if tag_id is None and link_id is None: result = dynamodb.scan( TableName=DYNAMODB_LINK_TABLE, Select="SPECIFIC_ATTRIBUTES", ProjectionExpression="link_id,link" ) links = extract_link_result(result) elif link_id is not None: # Run a query with a filter on link_id on the linkTable result = dynamodb.query( TableName=DYNAMODB_LINK_TABLE, KeyConditionExpression='link_id = :link_id', ExpressionAttributeValues={ ':link_id': { "S": link_id } } ) links = extract_link_result(result) else: # Run a query with a filter on tag_id on the tagLinkTable result = dynamodb.query( TableName=DYNAMODB_TAG_LINK_TABLE, KeyConditionExpression='tag_id = :tag_id', ExpressionAttributeValues={ ':tag_id': { "S": tag_id } } ) if result['Count'] > 0: for x in range(result['Count']): link_id_list.append(result['Items'][x]['link_id']['S']) # From the retrieved link_ids, retrieve the links from the link table if len(link_id_list) > 0: for link_id in link_id_list: result = dynamodb.query( TableName=DYNAMODB_LINK_TABLE, KeyConditionExpression='link_id = :link_id', ExpressionAttributeValues={ ':link_id': { "S": link_id } } ) if result['Count'] > 0: link = {} link['link_id'] = link_id # There should only be one result per link_id link['link'] = result['Items'][0]['link']['S'] links.append(link) return links def error_response(error_message, aws_request_id): return { "statusCode": 500, "isBase64Encoded": False, "body": json.dumps({ "Error": error_message, "Reference": aws_request_id, }), "headers": { 'Access-Control-Allow-Origin': '*', }, } def bad_request_response(error_message, aws_request_id): return { "statusCode": 400, "isBase64Encoded": False, "body": json.dumps({ "Error": error_message, "Reference": aws_request_id, }), "headers": { 'Access-Control-Allow-Origin': '*', }, } def lambda_handler(event, context): try: print(event) print(context) return_body = "" if event['resource'] == '/link': if event['httpMethod'] == "POST": post_body = json.loads(event['body']) link = post_body['link'] tag = post_body.get('tag') tag_id = post_body.get('tag_id') if tag_id is None: tag_id = get_tag_id(tag) else: tags = get_tags(tag_id) if not tags: return bad_request_response('Unknown tag_id ' + tag_id, context.aws_request_id) link_id = get_link_id(link) store_taglink(tag_id, link_id) return_body = json.dumps({"link_id": link_id}) elif event['httpMethod'] == "GET": tag_id = None link_id = None query_params = event['queryStringParameters'] if query_params is not None: tag_id = query_params.get('tag_id', None) tag = query_params.get('tag', None) link_id = query_params.get('link_id', None) if tag_id is None and tag is not None: tag_id = get_tag_id(tag) links = get_links(tag_id, link_id) return_body = json.dumps(links) elif event['resource'] == '/tag': if event['httpMethod'] == "POST": post_body = json.loads(event['body']) tag = post_body.get('tag') tag_id = store_tag(tag) return_body = json.dumps({"tag_id": tag_id}) elif event['httpMethod'] == "GET": tag_id = None query_params = event['queryStringParameters'] if query_params is not None: tag_id = query_params.get('tag_id', None) tag = query_params.get('tag', None) if tag_id is None and tag is not None: tag_id = get_tag_id(tag) tags = get_tags(tag_id) return_body = json.dumps(tags) elif event['resource'] == '/taglink': if event['httpMethod'] == "POST": post_body = json.loads(event['body']) tag_id = post_body.get('tag_id') link_id = post_body.get('link_id') tags = get_tags(tag_id) links = get_links(tag_id=None, link_id=link_id) if not tags: return bad_request_response('Unknown tag_id ' + tag_id, context.aws_request_id) if not links: return bad_request_response('Unknown link_id ' + link_id, context.aws_request_id) store_taglink(tag_id, link_id) return_body = json.dumps({"tag_id": tag_id, "link_id": link_id}) # Because this Lambda function is called by an API Gateway proxy integration # the result object must use the following structure. return { "statusCode": 200, "isBase64Encoded": False, "body": return_body, "headers": { 'Access-Control-Allow-Origin': '*', }, } except Exception as err: print(str(err)) # If there is an error during processing, catch it and return # from the Lambda function successfully. Specify a 500 HTTP status # code and provide an error message in the body. This will provide a # more meaningful error response to the end client. return error_response(str(err), context.aws_request_id) Description: Lambda function for processing api endpoint requests FunctionName: requestHandler Handler: index.lambda_handler Role: Fn::GetAtt: - "requestHandlerLambdaExecutionRole" - "Arn" Runtime: python3.8 DependsOn: - requestHandlerLambdaExecutionRole ApplicationAPI: Type: AWS::ApiGateway::RestApi Properties: Name: ApplicationAPI Description: Simple API for an application DependsOn: - requestHandler LambdaPermission: Type: AWS::Lambda::Permission Properties: FunctionName: !GetAtt - requestHandler - Arn Action: 'lambda:InvokeFunction' Principal: apigateway.amazonaws.com SourceArn: !Join ["", [!Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:", !Ref ApplicationAPI, "/*"]] DependsOn: - ApplicationAPI link: Type: AWS::ApiGateway::Resource Properties: RestApiId: !Ref ApplicationAPI ParentId: !GetAtt [ApplicationAPI, RootResourceId] PathPart: 'link' DependsOn: - ApplicationAPI tag: Type: AWS::ApiGateway::Resource Properties: RestApiId: !Ref ApplicationAPI ParentId: !GetAtt [ApplicationAPI, RootResourceId] PathPart: 'tag' DependsOn: - ApplicationAPI taglink: Type: AWS::ApiGateway::Resource Properties: RestApiId: !Ref ApplicationAPI ParentId: !GetAtt [ApplicationAPI, RootResourceId] PathPart: 'taglink' DependsOn: - ApplicationAPI apiauth: Type: AWS::ApiGateway::Authorizer Properties: IdentitySource: method.request.header.Authorization Name: apiauth ProviderARNs: - !GetAtt - apiuserpool - Arn RestApiId: !Ref ApplicationAPI Type: COGNITO_USER_POOLS DependsOn: - apiuserpool ProxyResourceLinkGET: Type: AWS::ApiGateway::Method Properties: RestApiId: !Ref ApplicationAPI ResourceId: !Ref link HttpMethod: GET AuthorizationType: COGNITO_USER_POOLS AuthorizerId: !Ref apiauth Integration: Type: AWS_PROXY IntegrationHttpMethod: POST Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${requestHandler.Arn}/invocations IntegrationResponses: - StatusCode: 200 MethodResponses: - StatusCode: 200 ResponseModels: application/json: Empty DependsOn: - ApplicationAPI - apiauth - link ProxyResourceLinkPOST: Type: AWS::ApiGateway::Method Properties: RestApiId: !Ref ApplicationAPI ResourceId: !Ref link HttpMethod: POST AuthorizationType: COGNITO_USER_POOLS AuthorizerId: !Ref apiauth Integration: Type: AWS_PROXY IntegrationHttpMethod: POST Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${requestHandler.Arn}/invocations IntegrationResponses: - StatusCode: 200 MethodResponses: - StatusCode: 200 ResponseModels: application/json: Empty DependsOn: - ProxyResourceLinkGET - apiauth OptionsMethodLink: Type: AWS::ApiGateway::Method Properties: AuthorizationType: COGNITO_USER_POOLS AuthorizerId: !Ref apiauth RestApiId: !Ref ApplicationAPI ResourceId: !Ref link HttpMethod: OPTIONS Integration: IntegrationResponses: - StatusCode: 200 ResponseParameters: method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Methods: "'GET,POST,OPTIONS'" method.response.header.Access-Control-Allow-Origin: "'*'" ResponseTemplates: application/json: '' PassthroughBehavior: NEVER RequestTemplates: application/json: '{"statusCode": 200}' Type: MOCK MethodResponses: - StatusCode: 200 ResponseModels: application/json: Empty ResponseParameters: method.response.header.Access-Control-Allow-Headers: true method.response.header.Access-Control-Allow-Methods: true method.response.header.Access-Control-Allow-Origin: true DependsOn: - ProxyResourceLinkPOST - apiauth ProxyResourceTagGET: Type: AWS::ApiGateway::Method Properties: RestApiId: !Ref ApplicationAPI ResourceId: !Ref tag HttpMethod: GET AuthorizationType: COGNITO_USER_POOLS AuthorizerId: !Ref apiauth Integration: Type: AWS_PROXY IntegrationHttpMethod: POST Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${requestHandler.Arn}/invocations IntegrationResponses: - StatusCode: 200 MethodResponses: - StatusCode: 200 ResponseModels: application/json: Empty DependsOn: - ProxyResourceLinkPOST - apiauth ProxyResourceTagPOST: Type: AWS::ApiGateway::Method Properties: RestApiId: !Ref ApplicationAPI ResourceId: !Ref tag HttpMethod: POST AuthorizationType: COGNITO_USER_POOLS AuthorizerId: !Ref apiauth Integration: Type: AWS_PROXY IntegrationHttpMethod: POST Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${requestHandler.Arn}/invocations IntegrationResponses: - StatusCode: 200 MethodResponses: - StatusCode: 200 ResponseModels: application/json: Empty DependsOn: - ProxyResourceTagGET - apiauth OptionsMethodTag: Type: AWS::ApiGateway::Method Properties: AuthorizationType: COGNITO_USER_POOLS AuthorizerId: !Ref apiauth RestApiId: !Ref ApplicationAPI ResourceId: !Ref tag HttpMethod: OPTIONS Integration: IntegrationResponses: - StatusCode: 200 ResponseParameters: method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Methods: "'GET,POST,OPTIONS'" method.response.header.Access-Control-Allow-Origin: "'*'" ResponseTemplates: application/json: '' PassthroughBehavior: NEVER RequestTemplates: application/json: '{"statusCode": 200}' Type: MOCK MethodResponses: - StatusCode: 200 ResponseModels: application/json: Empty ResponseParameters: method.response.header.Access-Control-Allow-Headers: true method.response.header.Access-Control-Allow-Methods: true method.response.header.Access-Control-Allow-Origin: true DependsOn: - ProxyResourceTagPOST - apiauth ProxyResourceTagLinkGET: Type: AWS::ApiGateway::Method Properties: RestApiId: !Ref ApplicationAPI ResourceId: !Ref taglink HttpMethod: GET AuthorizationType: COGNITO_USER_POOLS AuthorizerId: !Ref apiauth Integration: Type: AWS_PROXY IntegrationHttpMethod: POST Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${requestHandler.Arn}/invocations IntegrationResponses: - StatusCode: 200 MethodResponses: - StatusCode: 200 ResponseModels: application/json: Empty DependsOn: - ProxyResourceTagPOST - apiauth ProxyResourceTagLinkPOST: Type: AWS::ApiGateway::Method Properties: RestApiId: !Ref ApplicationAPI ResourceId: !Ref taglink HttpMethod: POST AuthorizationType: COGNITO_USER_POOLS AuthorizerId: !Ref apiauth Integration: Type: AWS_PROXY IntegrationHttpMethod: POST Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${requestHandler.Arn}/invocations IntegrationResponses: - StatusCode: 200 MethodResponses: - StatusCode: 200 ResponseModels: application/json: Empty DependsOn: - ProxyResourceTagLinkGET - apiauth OptionsMethodTagLink: Type: AWS::ApiGateway::Method Properties: AuthorizationType: COGNITO_USER_POOLS AuthorizerId: !Ref apiauth RestApiId: !Ref ApplicationAPI ResourceId: !Ref taglink HttpMethod: OPTIONS Integration: IntegrationResponses: - StatusCode: 200 ResponseParameters: method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Methods: "'GET,POST,OPTIONS'" method.response.header.Access-Control-Allow-Origin: "'*'" ResponseTemplates: application/json: '' PassthroughBehavior: NEVER RequestTemplates: application/json: '{"statusCode": 200}' Type: MOCK MethodResponses: - StatusCode: 200 ResponseModels: application/json: Empty ResponseParameters: method.response.header.Access-Control-Allow-Headers: true method.response.header.Access-Control-Allow-Methods: true method.response.header.Access-Control-Allow-Origin: true DependsOn: - ProxyResourceTagLinkPOST - apiauth Deployment: DependsOn: "OptionsMethodTagLink" Type: AWS::ApiGateway::Deployment Properties: RestApiId: Ref: "ApplicationAPI" Description: "Application API deployment" StageName: "prod" Outputs: APIBaseUrl: Description: Application API Gateway base URL Value: !Join - '' - - 'https://' - !Ref ApplicationAPI - '.execute-api.' - !Ref AWS::Region - '.amazonaws.com/prod' APILinkUrl: Description: Application API Gateway link endpoint URL Value: !Join - '' - - 'https://' - !Ref ApplicationAPI - '.execute-api.' - !Ref AWS::Region - '.amazonaws.com/prod/link' APITagUrl: Description: Application API Gateway tag endpoint URL Value: !Join - '' - - 'https://' - !Ref ApplicationAPI - '.execute-api.' - !Ref AWS::Region - '.amazonaws.com/prod/tag' APITagLinkUrl: Description: Application API Gateway taglink endpoint URL Value: !Join - '' - - 'https://' - !Ref ApplicationAPI - '.execute-api.' - !Ref AWS::Region - '.amazonaws.com/prod/taglink' UserPoolID: Description: The Cognito user pool ID Value: !Ref apiuserpool APIClientID: Description: The API user pool client ID Value: !Ref applicationAPIUserPoolClient
Launch the CFT with this command:
$ aws cloudformation create-stack --stack-name apidbauth --template-body file://setupapidbauth.yml --capabilities CAPABILITY_NAMED_IAM
Poll for its progress with:
$ aws cloudformation describe-stacks --stack-name apidbauth
When complete, make a note of the APIBaseUrl
, UserPoolID
and APIClientID
values. Export them to environment variables with these commands. Be sure the replace the values in angle brackets with the real values:
$ export BASEURL=<APIBaseUrl> $ export POOL_ID=<UserPoolID> $ export CLIENT_ID=<APIClientID>
Step 7: Create a New User
Create a new user with the following commands. Replace <Your email> with your email address and <PASSWORD> with a valid password with at least 8 characters, including 1 upper case character, 1 digit and 1 special character:
$ export EMAIL=<Your email> $ aws cognito-idp admin-create-user --user-pool-id $POOL_ID --username $EMAIL $ aws cognito-idp admin-set-user-password --user-pool-id $POOL_ID --username $EMAIL --password <PASSWORD> --permanent
Step 8: Get the Client Secret
Run this command to get the Client Secret for the User Pool Client:
$ aws cognito-idp describe-user-pool-client --client-id $CLIENT_ID --user-pool-id $POOL_ID
You will need to extract the ClientSecret
value from the response. Set it to an environment variable:
$ export CLIENT_SECRET=<clientsecret>
Step 9: Generate the Secret Hash
You will need to generate a secret hash, which is used for obtaining id tokens for api requests. Open a file called get_client_secret.py
and paste in the following code:
import sys import hmac, hashlib, base64 username = sys.argv[1] app_client_id = sys.argv[2] key = sys.argv[3] message = bytes(sys.argv[1]+sys.argv[2], 'utf-8') key = bytes(sys.argv[3], 'utf-8') secret_hash = base64.b64encode(hmac.new(key, message, digestmod=hashlib.sha256).digest()).decode() print("SECRET HASH:", secret_hash)
Run this code to generate the hash, with the arguments as shown (you will need python installed locally; run sudo yum install python3
or similar, depending on your operating system):
$ python get_client_secret.py $EMAIL $CLIENT_ID $CLIENT_SECRET
Export the generated hash to an environment variable:
$ export SECRET_HASH=<SECRET HASH>
Step 9: Get the ID Token
To get the ID token for making API requests, run the following. Be sure to replace <PASSWORD> with your user password, and <Your Region> with your AWS region (e.g. us-east-1):
aws cognito-idp initiate-auth --region <Your Region> --auth-flow USER_PASSWORD_AUTH --client-id $CLIENT_ID --auth-parameters USERNAME=$EMAIL,PASSWORD=<PASSWORD>,SECRET_HASH=$SECRET_HASH
The token you will need is the one under the IdToken
attribute in the response. Export this to an environment variable:
export TOKEN=<IdToken>
Step 10: Try an Unauthenticated API Request
Now try making a request to the API, without the Authorization header or token:
$ curl -X "POST" -H "Content-Type: application/json" -d "{\"link\": \"https://www.allthecoding.com\", \"tag\": \"blog\"}" $BASEURL/link
You should get the response:
{"message":"Unauthorized"}
Step 11: Try an Authenticated API Request
Now try making a request to the API, this time with the Authorization header and token:
$ curl -X "POST" -H "Content-Type: application/json" -H "Authorization: $TOKEN" -d "{\"link\": \"https://www.allthecoding.com\", \"tag\": \"blog\"}" $BASEURL/link
You should get a response something like the below:
{"link_id": "4343fd21-761c-4717-9f1b-78bd249b4380"}
Step 12: Clean Up
If you don’t want to keep your API (note that AWS will charge you for it if you don’t have Free Tier on your account), delete the CFT stack (all resources created) by running:
$ aws cloudformation delete-stack --stack-name apidbauth
You will then need to remove any logs that were generated by the Lambda function. To do this:
- Log into the AWS Console (your AWS account) using your Administrator IAM user, and navigate to CloudWatch.
- On the left-hand side, select Logs -> Log Groups.
- Select ‘aws/lambda/requestHandler’.
- Under Actions, select ‘Delete log groups’ and confirm.
Conclusion
You should now be able to create a simple API on AWS with a CloudFormation template, with three endpoints and three DynamoDB tables. In this post, you learned how to add a Cognito Authorizer, so that only authenticated requests to the API are accepted.
If you want to find out how to add DELETE methods to your API, check out my follow-on post, Creating an API with AWS: Part 5: DELETE methods. Thanks for reading!
2 Responses
[…] Creating an API with AWS: Part 4: Cognito Authentication December 18, 2021 […]
[…] Creating an API with AWS: Part 4: Cognito Authentication December 18, 2021 […]