- Published on
AWS Cognito JWT Authorizer and API Gateway Integration
- Authors
- Name
- jonathan Bradbury
Summary
The purpose of this article is to show how we can set up a cognito user pool, add an app client to allow users to generate bearer tokens. Add a resource server to specify scopes for said app. Then we will show how to deploy an API with API gateway that will use the cognito authorizer to validate bearer tokens and scopes.
Code snippets in Yml are using cloudformation to create resources
Cognito User Pool
"A user pool is a user directory in Amazon Cognito". You can use it to manage specific users, allowing them to generate aws credentials. Or you can add App Clients, which allow you to integrate your apps with a user pool in a similar way you would a user. We're going to use the app client to create an app for order middleware.
First we create the user pool, you don't have to specify a name, it will use the logical ID and append some randomness to the end.
OMWUserPool:
Type: AWS::Cognito::UserPool
Then we make the app client. We associate it with our user pool (created above)
The OAuth Scopes will make more sense later on when we integrate some APIs with our Authorizer. They're a way to enforce at the API level what scopes a bearer tokens needs in order to use that api route. AllowedOAuthFlows let us use the generated client_id and secret to create a bearer token.
OMWUserPoolClient:
Type: AWS::Cognito::UserPoolClient
Properties:
UserPoolId: !Ref OMWUserPool
AllowedOAuthFlowsUserPoolClient: True
AllowedOAuthScopes:
- order/post.order
AllowedOAuthFlows:
- client_credentials
CallbackURLs:
- https://localhost
ClientName: omw-user-pool
DefaultRedirectURI: https://localhost
ExplicitAuthFlows:
- ALLOW_REFRESH_TOKEN_AUTH
GenerateSecret: True
Then we create a domain. This activates the /oauth2/token endpoint when creating bearer tokens
OMWCognitoDomain:
Type: AWS::Cognito::UserPoolDomain
Properties:
Domain: tb-omw-order
UserPoolId: !Ref OMWUserPool
Lastly we create a resource server. This lets us define our own custom scopes. In this example we're creating the post.order scope, we will add this scope on our api route as order/post.order, where order is the name of the resource server.
{resource_server_name}/{custom_scope}
OMWCognitoOrderResource:
Type: AWS::Cognito::UserPoolResourceServer
Properties:
Identifier: order
Name: order
UserPoolId: !Ref OMWUserPool
Scopes:
- ScopeDescription: Lets you post orders
ScopeName: post.order
I removed the client id from the screen shot, however there is an ID and a secret. There is also a user pool ID which you can use to look at the well-known openid configuration.
https://cognito-idp.us-east-1.amazonaws.com/{your_user_pool_id}/.well-known/openid-configuration
{
"authorization_endpoint": "https://tb-omw-order.auth.us-east-1.amazoncognito.com/oauth2/authorize",
"id_token_signing_alg_values_supported": ["RS256"],
"issuer": "https://cognito-idp.us-east-1.amazonaws.com/{user_pool_id}",
"jwks_uri": "https://cognito-idp.us-east-1.amazonaws.com/{user_pool_id}/.well-known/jwks.json",
"response_types_supported": ["code", "token"],
"scopes_supported": ["openid", "email", "phone", "profile"],
"subject_types_supported": ["public"],
"token_endpoint": "https://tb-omw-order.auth.us-east-1.amazoncognito.com/oauth2/token",
"token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post"],
"userinfo_endpoint": "https://tb-omw-order.auth.us-east-1.amazoncognito.com/oauth2/userInfo"
}
The token_endpoint is what we will use to create bearer tokens with our apps client_id and client_secret. The issuer endpoint is what we will use when integrating with our API.
Cognito User Pool Summary
With the above we created a user pool. We added an app client to the user pool. The app client has an ID and secret that it can use to generate bearer tokens. We added a resource server so we can have custom scopes associated with our bearer token. And lastly we added a domain prefix so we can create oauth2 tokens.
That's all there is to it for the cognito side. Next we will look into creating an API gateway endpoint that uses this cognito user pool to validate bearer tokens.
API Gateway
Lambdas
In order to run code from our API Gateway we need to create some Lambdas. The goal of this article is to just show cognito JWT authentication, so the lambda is going to be really simple.
import json
def main(event, context):
return {
'statusCode': 200,
'body': json.dumps({'message': 'It worked!'})
}
Serverless
Lambda
We're going to use the same lambda twice, so we can have two API routes. This will allow us to create one with a scope that isn't included in the resource server we associated with our app client in the above section. And another that uses a scope that is included.
functions:
order:
handler: order.main
getOrder:
handler: order.main
Authorizer
Here we create our JWT authorizer that will be attached out our API routes. Notice that the audience is set to some variable. This is our cognito user pool app client Client ID. It also has an Issuer. This is pointing to our issuer url (https://cognito-idp.us-east-1.amazonaws.com/{user_pool_id})
JWTAuthorizer:
Type: AWS::ApiGatewayV2::Authorizer
Properties:
ApiId: !Ref API
AuthorizerType: JWT
IdentitySource:
- "$request.header.Authorization" ## This takes from "Authorization" : JWToken as a header
JwtConfiguration:
Audience:
- ${self:custom.cognitoUserAudience.${self:custom.stage}}
Issuer: !Sub https://cognito-idp.${AWS::Region}.amazonaws.com/${self:custom.cognitoUserPool.${self:custom.stage}}
Name: tb-omw-cognito-jwt-authorizer-${sls:stage}
API Gateway
Here we are creating our API Gateway V2 api.
API:
Type: AWS::ApiGatewayV2::Api
Properties:
Name: tb-omw-cognito-jwt-${sls:stage}
ProtocolType: HTTP
Next we create a stage for the API. Just using $default for this case.
APIStage:
Type: AWS::ApiGatewayV2::Stage
Properties:
StageName: $default
AutoDeploy: true
ApiId: !Ref API
Now we're going to create a few Routes for our API. One is POST /api/v1/order and the other is GET /api/v1/order.
Notice the Routes have an authorization type set to JWT and are referring to the Authorizer resource we created above. It is also setting custom scopes for this API route.
The POST action will succeed because in our resource server we have added the 'post.order' scope. The GET action will fail because we did not include 'get.order' in our resource server scope.
OrderRoute:
Type: AWS::ApiGatewayV2::Route
Properties:
ApiId: !Ref API
RouteKey: "POST /api/v1/order"
Target: !Join ["/", ["integrations", !Ref OrderLambdaIntegration]]
AuthorizationType: JWT
AuthorizerId: !Ref JWTAuthorizer
AuthorizationScopes:
- order/post.order
GetOrderRoute:
Type: AWS::ApiGatewayV2::Route
Properties:
ApiId: !Ref API
RouteKey: "GET /api/v1/order"
Target: !Join ["/", ["integrations", !Ref GetOrderLambdaIntegration]]
AuthorizationType: JWT
AuthorizerId: !Ref JWTAuthorizer
AuthorizationScopes:
- order/get.order
Now we add the Lambda Integration to our API routes
OrderLambdaIntegration:
Type: AWS::ApiGatewayV2::Integration
Properties:
ApiId: !Ref API
IntegrationType: AWS_PROXY
Description: Lambda Proxy Integration
IntegrationMethod: POST
PayloadFormatVersion: "2.0"
IntegrationUri: !Sub "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${OrderLambdaFunction.Arn}/invocations"
GetOrderLambdaIntegration:
Type: AWS::ApiGatewayV2::Integration
Properties:
ApiId: !Ref API
IntegrationType: AWS_PROXY
Description: Lambda Proxy Integration
IntegrationMethod: POST
PayloadFormatVersion: "2.0"
IntegrationUri: !Sub "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetOrderLambdaFunction.Arn}/invocations"
And finally we add the lambda invoke permission so that API gateway can actually call our lambdas.
OrderLambdaInvokePermission:
Type: AWS::Lambda::Permission
Properties:
Action: "lambda:InvokeFunction"
FunctionName: !Ref OrderLambdaFunction
Principal: apigateway.amazonaws.com
GetOrderLambdaInvokePermission:
Type: AWS::Lambda::Permission
Properties:
Action: "lambda:InvokeFunction"
FunctionName: !Ref GetOrderLambdaFunction
Principal: apigateway.amazonaws.com
Summary
Thats all there is to creating the API routes and associating them with the Cognito Authorizer. Next we will show examples of creating bearer tokens and using them to authenticate requests to our API routes.
Demonstration
Get a Bearer Token
First you set your authorization to Basic Auth. The Username is your cognito user pool app client Client ID, and the password is your app client secret. Both you can find in your cognito user pool app client settings.
For the body choose x-www-form-urlencoded. Then set the key to 'grant_type' and the value to 'client_credentials'. If you remember from earlier on, in our cognito user pool app client we set it up to use client_credentials.
AllowedOAuthFlows:
- client_credentials
Now we make the POST request and we will get a bearer token in the response.
import requests
url = "https://tb-omw-order.auth.us-east-1.amazoncognito.com/oauth2/token"
payload='grant_type=client_credentials'
headers = {
'Authorization': 'Basic {your_secrets_as_base64}',
'Content-Type': 'application/x-www-form-urlencoded',
}
response = requests.request("POST", url, headers=headers, data=payload)
print(response.text)
Use Bearer Token
Now that we have a Bearer Token we can make calls to API Gateway and provide the token as authentication.
Put in the API Gateway route, go to Authorization. Choose type Bearer Token. Paste in the previously generated Bearer token, and we can see that the call works!
Now if we try the get request it will fail. This is because the token does not include the get.order scope.
Conclusion
I hope you learned a bit more about how you can use cognito to create and Authorize JWTs, as well as how to integrate your cognito authorizer with your api gateway routes.