AWS API Gateway & Access Tokens

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.

Leave a Reply