AWS supports authenticating API calls using a token issued by Cognito authentication. This allows for good integration of identity into AWS APIs. Setting up the integration is relatively easy, create an authorizer of type COGNITO_USER_POOLS
and attach it to the endpoint. Now provide the id token from a Cognito authorization response in a request through the Authorization
header and the call will succeed. Without that header, or with an invalid value, the gateway will reject the call with a 403
error. That’s the simple path, unfortunately OAuth doesn’t have just one token and using the id token for this is not always easy, especially for mobile apps. For my use case I needed to use the access token instead of the id token. Making that work required some extra configuration that I’ll cover in this post.
401 Failures
Trying to call an endpoint with an access token instead of an ID token generated a 401 failure response from the gateway. I think the 401 is trying to subtly hint that it recognizes the token but doesn’t have an allowed OAuth scope that would authorize the request. To fix that required extra configuration of the user pool, the authenticator, and the API request definition.
User Pool and Authenticator Configuration
The key to making access token authentication work is to configure the user pool correctly. There is a good section in this AWS document that describes the different configurations required for using an id token or an access token. Point 7 in the document titled “To use an access token, do the following:” is the interesting part for our purposes. In particular we need to define custom OAuth scopes that methods can use. The OAuth scopes referred to in that section must be defined in a resource server, the configuration for that is described here.
Here’s an example partial cloud formation that shows the required configuration to create a resource server with custom OAuth scopes and a matching authorizer that can use them. I’ve included the user pool client as well, it has to explicitly allow the custom OAuth scopes. Note also the use of DependsOn
in the client to ensure that the resource server, and the corresponding scopes, are created before trying to use them in the client.
Resources:
CognitoUserPool:
Type: AWS::Cognito::UserPool
#... All the other user pool properties as required
CognitoUserPoolClient:
Type: AWS::Cognito::UserPoolClient
DependsOn: UserPoolResourceServer
Properties:
# Generate an app client name based on the stage
ClientName: ${self:provider.stage}-user-pool-client
UserPoolId:
Ref: CognitoUserPool
ExplicitAuthFlows:
- ALLOW_CUSTOM_AUTH
- ALLOW_USER_PASSWORD_AUTH
- ALLOW_USER_SRP_AUTH
- ALLOW_REFRESH_TOKEN_AUTH
GenerateSecret: false
AllowedOAuthFlows:
- code
- implicit
AllowedOAuthFlowsUserPoolClient: true
AllowedOAuthScopes:
- email
- openid
- com.example.api/read
- com.example.api/write
CallbackURLs:
- folktells://auth/login
DefaultRedirectURI: exampleapp://auth/login
LogoutURLs:
- exampleapp://auth/logout
SupportedIdentityProviders:
- COGNITO
PreventUserExistenceErrors: ENABLED
UserPoolResourceServer:
Type: AWS::Cognito::UserPoolResourceServer
Properties:
Identifier: com.example.api
Name: Example API
UserPoolId: !Ref CognitoUserPool
Scopes:
- ScopeName: "write"
ScopeDescription: "Write access"
- ScopeName: "read"
ScopeDescription: "Read access"
# then add a Cognito authorizer you can reference later
ApiGatewayAuthorizer:
DependsOn:
# this is pre-defined by serverless
- ApiGatewayRestApi
Type: AWS::ApiGateway::Authorizer
Properties:
Name: cognito_auth
RestApiId: { "Ref" : "ApiGatewayRestApi" }
IdentitySource: method.request.header.Authorization
Type: COGNITO_USER_POOLS
ProviderARNs:
- Fn::GetAtt: [CognitoUserPool, Arn]
Method Request Authentication
Each method on the gateway must have OAuth scopes that match the custom scopes defined in the resource server. If you are using the Serverless Framework then that definition is attached directly to the event and looks like this:
functions:
myevent:
handler: bin/myevent
events:
- http:
path: myevent
method: get
authorizer:
type: COGNITO_USER_POOLS
authorizerId:
Ref: ApiGatewayAuthorizer
scopes:
- "com.example.api/read"
- "com.example.api/write"
Note the quotation marks around the scopes in the example above and that the scope names match the full names shown on the User Pool Client Settings, not the abbreviated names used in the Resource Server definition. The full name of the OAuth scope is the Resource Server ID slash scope name. I lost quite a bit of time because a regex somewhere in AWS didn’t like the generated content because of the special characters in the scopes. Quotation marks resolved that problem. I’m not sure what this looks like in cloud formation but likely similar.