With the recent release of HttpApi from AWS I've been playing with it for a bit and I wanted to see how far I can get it to use authorization without handling any logic in my application.
Creating a base code
Started with a simple base, let's set up the initial scenario which is no authentication at all. The architecture is the typical HttpApi -> Lambda, in this case, the Lambda content is irrelevant and therefore I've just used an inline code to test it's working.
AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: > Sample SAM Template using HTTP API and Cognito Authorizer Resources: # Dummy Lambda function HttpApiTestFunction: Type: AWS::Serverless::Function Properties: InlineCode: | exports.handler = function(event, context, callback) { const response = { test: 'Hello HttpApi', claims: event.requestContext.authorizer && event.requestContext.authorizer.jwt.claims }; callback(null, response); }; Handler: index.handler Runtime: nodejs12.x Timeout: 30 MemorySize: 256 Events: GetOpen: Type: HttpApi Properties: Path: /test Method: GET ApiId: !Ref HttpApi Auth: Authorizer: NONE HttpApi: Type: AWS::Serverless::HttpApi Properties: CorsConfiguration: AllowOrigins: - "*" Outputs: HttpApiUrl: Description: URL of your API endpoint Value: !Sub 'https://${HttpApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/' HttpApiId: Description: Api id of HttpApi Value: !Ref HttpApi
With the outputs of this template we can hit the endpoint and see that in fact, it's accessible. So far, nothing crazy here.
$ curl https://abc1234.execute-api.us-east-1.amazonaws.com/test {"test":"Hello HttpApi"}
Creating a Cognito UserPool and Client
The claims object is not populated because the request wasn't authenticated since no token was provided, as expected.
Let's create the Cognito UserPool with a very simple configuration assuming lots of default values since they're not relevant for this example.
## Add this fragment under Resources: # User pool - simple configuration UserPool: Type: AWS::Cognito::UserPool Properties: AdminCreateUserConfig: AllowAdminCreateUserOnly: false AutoVerifiedAttributes: - email MfaConfiguration: "OFF" Schema: - AttributeDataType: String Mutable: true Name: name Required: true - AttributeDataType: String Mutable: true Name: email Required: true UsernameAttributes: - email # User Pool client UserPoolClient: Type: AWS::Cognito::UserPoolClient Properties: ClientName: AspNetAppLambdaClient ExplicitAuthFlows: - ALLOW_USER_PASSWORD_AUTH - ALLOW_USER_SRP_AUTH - ALLOW_REFRESH_TOKEN_AUTH GenerateSecret: false PreventUserExistenceErrors: ENABLED RefreshTokenValidity: 30 SupportedIdentityProviders: - COGNITO UserPoolId: !Ref UserPool ## Add this fragment under Outputs: UserPoolId: Description: UserPool ID Value: !Ref UserPool UserPoolClientId: Description: UserPoolClient ID Value: !Ref UserPoolClient
Once we have the cognito UserPool and a client, we are in a position to start putting things together. But before there are a few things to clarify:
- Username is the email and only two fields are required to create a user: name and email.
- The client defines both ALLOW_USER_PASSWORD_AUTH and ALLOW_USER_SRP_AUTH Auth flows to be used by different client code.
- No secret is generated for this client, if you intend to use other flows, you'll need to create other clients accordingly.
Adding authorization information
Next step is to add authorization information to the HttpApi.
## Replace the HttpApi resource with this one. HttpApi: Type: AWS::Serverless::HttpApi Properties: CorsConfiguration: AllowOrigins: - "*" Auth: Authorizers: OpenIdAuthorizer: IdentitySource: $request.header.Authorization JwtConfiguration: audience: - !Ref UserPoolClient issuer: !Sub https://cognito-idp.${AWS::Region}.amazonaws.com/${UserPool} DefaultAuthorizer: OpenIdAuthorizer
We've added authorization information to the HttpApi where JWT issuer is the the Cognito UserPool previously created and the token are intended only for that client.
If we test again, nothing changes because the event associated with the lambda function says explictly "Authorizer: NONE".
To test this, we'll create a new event associated with the same lambda function but this time we'll add some authorization information to it.
## Add this fragment at the same level as GetOpen ## under Events as part of the function properties GetSecure: Type: HttpApi Properties: ApiId: !Ref HttpApi Method: GET Path: /secure Auth: Authorizer: OpenIdAuthorizer
If we test the new endpoint /secure, then we'll see the difference.
$ curl -v https://abc1234.execute-api.us-east-1.amazonaws.com/secure >>>>>> removed for brevity >>>>>> > GET /secure HTTP/1.1 > Host: abc1234.execute-api.us-east-1.amazonaws.com > User-Agent: curl/7.52.1 > Accept: */* > * Connection state changed (MAX_CONCURRENT_STREAMS updated)! < HTTP/2 401 < date: Sat, 11 Apr 2020 17:19:50 GMT < content-length: 26 < www-authenticate: Bearer < apigw-requestid: K1RYliB1IAMESNA= < * Curl_http_done: called premature == 0 * Connection #0 to host abc1234.execute-api.us-east-1.amazonaws.com left intact {"message":"Unauthorized"}
At this point we have a new endpoint that requires an access token. Now we need a token, but to get a token, we need a user first.
Fortunately Cognito can provide us with all we need in this case. Let's see how.
Creating a Cognito User
Cognito cli provides the commands to sign up and verify user accounts.
$ aws cognito-idp sign-up \ --client-id asdfsdfgsdfgsdfgfghsdf \ --username abel@example.com \ --password Test.1234 \ --user-attributes Name="email",Value="abel@example.com" Name="name",Value="Abel Perez" \ --profile default \ --region us-east-1 { "UserConfirmed": false, "UserSub": "aaa30358-3c09-44ad-a2ec-5f7fca7yyy16", "CodeDeliveryDetails": { "AttributeName": "email", "Destination": "a***@e***.com", "DeliveryMedium": "EMAIL" } }
After creating the user, it needs to be verified.
$ aws cognito-idp admin-confirm-sign-up \ --user-pool-id us-east-qewretry \ --username abel@example.com \ --profile default \ --region us-east-1
This commands gives no output, to test we are good to go, let's use the admin-get-user command
$ aws cognito-idp admin-get-user \ --user-pool-id us-east-qewretry \ --username abel@example.com \ --profile default \ --region us-east-1 \ --query UserStatus "CONFIRMED"
We have a confirmed user!
Getting a token for the Cognito User
To obtain an Access Token, we use the Cognito initiate-auth command providing the client, username and password.
$ TOKEN=`aws cognito-idp initiate-auth \ --client-id asdfsdfgsdfgsdfgfghsdf \ --auth-flow USER_PASSWORD_AUTH \ --auth-parameters USERNAME=abel@example.com,PASSWORD=Test.1234 \ --profile default \ --region us-east-1 \ --query AuthenticationResult.AccessToken \ --output text` $ echo $TOKEN
With the access token in hand, it's time to test the endpoint with it.
$ curl -H "Authorization:Bearer $TOKEN" https://abc1234.execute-api.us-east-1.amazonaws.com/secure # some formatting added here { "test": "Hello HttpApi", "claims": { "auth_time": "1586627310", "client_id": "asdfsdfgsdfgsdfgfghsdf", "event_id": "94872b9d-e5cc-42f2-8e8f-1f8ad5c6e1fd", "exp": "1586630910", "iat": "1586627310", "iss": "https://cognito-idp.us-east-1.amazonaws.com/us-east-qewretry", "jti": "878b2acd-ddbd-4e68-b097-acf834291d09", "sub": "cce30358-3c09-44ad-a2ec-5f7fca7dbd16", "token_use": "access", "username": "cce30358-3c09-44ad-a2ec-5f7fca7dbd16" } }
Voilà! We've accessed the secure endpoint with a valid access token.
What about groups ?
I wanted to know more about possible granular control of the authorization and I went and created two Cognito Groups let's say Group1 and Group2. Then, I added my newly created user to both groups and repeated the experiment.
Once the user was added to the groups, I got a new token and issued the request to the secure endpoint.
$ curl -H "Authorization:Bearer $TOKEN" https://abc1234.execute-api.us-east-1.amazonaws.com/secure # some formatting added here { "test": "Hello HttpApi", "claims": { "auth_time": "1586627951", "client_id": "2p9k1pfhtsbr17a2fukr5mqiiq", "cognito:groups": "[Group2 Group1]", "event_id": "c450ae9e-bd4e-4882-b085-5e44f8b4cefd", "exp": "1586631551", "iat": "1586627951", "iss": "https://cognito-idp.us-east-1.amazonaws.com/us-east-qewretry", "jti": "51a39fd9-98f9-4359-9214-000ea40b664e", "sub": "cce30358-3c09-44ad-a2ec-5f7fca7dbd16", "token_use": "access", "username": "cce30358-3c09-44ad-a2ec-5f7fca7dbd16" } }
Notice within the claims object, a new one has come up: "cognito:groups" and the value associated with it is "[Group2 Group1]".
Which means that we could potentially check this claim value to make some decisions in our application logic without having to handle all of the authentication inside the application code base.
This opens the possibility for more exploration within the AWS ecosystem. I hope this has been helpful, the full source code can be found at https://github.com/abelperezok/http-api-cognito-jwt-authorizer.