36

I am configuring an app with various frontends (mobile and web apps) and a single API backend, powered by Lambda and accessed via AWS API Gateway.

As I'm planning to use Cognito to authenticate and authorize users, I have set up a Cognito User Pool authorizer on my API Gateway and several API methods.

With an architecture like this, it seems logical that my apps (e.g. an iOS or Vue.js app) are the Client applications from an OAuth perspective, and my API Gateway backend is a Resource Server. Based on this Auth0 forum post it seems clear that I should therefore use an ID token in my client app, and pass an Access Token to authorize my API Gateway resources.

When I hit the Cognito /oauth2/authorize endpoint to get an access code and use that code to hit the /oauth2/token endpoint, I get 3 tokens - an Access Token, an ID Token and a Refresh Token. So far so good, as I should have what I need.

This is where I've run into difficulties - using the test function on the API Gateway Cognito User Pool Authorizer console, I can paste in the ID token and it passes (decoding the token on-screen). But when I paste in the Access Token, I get 401 - unauthorized.

In my Cognito setup, I have enabled Authorization Code Grant flow only, with email and openid scopes (this seems to be the minimum allowed by Cognito as I get an error trying to save without at least these ticked).

Do I need to add some specific scopes to get API Gateway to authorize a request with the Access Code? If so, where are these configured?

Or am I missing something? Will API Gateway only allow an ID token to be used with a Cognito User Pool Authorizer?

Harry
  • 4,660
  • 7
  • 37
  • 65

4 Answers4

55

You can use an access token with the same authorizer that works for the id token, but there is some additional setup to be done in the User Pool and the APIG.

Even when this extra setup is done you cannot use the built-in authorizer test functionality with an access token, only an id token. Typical 80% solution from AWS!

To use an access token you need to set up resource servers in the User Pool under App Integration -> Resource Servers it doesn't matter what you use but I will assume you use <site>.com for the Identifier and you have one scope called api.

No go to the method in APIG and enter the Method Request for the method. Assuming this is already set up with an authorizer tested with the id token, you then add <site>.com/api to the Settings -> OAuth Scopes section.

Just by adding the OAuth Scope it will make sure that the token now has to be an access token and an id token is no longer accepted.

This is detailed here: https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-enable-cognito-user-pool.html

Ravenscar
  • 2,730
  • 19
  • 18
  • Thanks for this. I just don't understand why using the default scopes doesn't work. Like email, phone, profile and so on. We're forced to specify our resource server and scopes even if we want to use the default scopes. – couceirof Nov 01 '18 at 10:05
  • 5
    The only addition to the above answer would be to enable `Allowed Custom Scopes` for your scope in `App client settings`. – asr9 Nov 09 '18 at 22:44
  • 3
    And yes - re-deploy the api ! Also refer https://aws.amazon.com/premiumsupport/knowledge-center/cognito-custom-scopes-api-gateway/ – donlys Jan 13 '20 at 13:10
  • 1
    Thanks for this, AWS and its quirks is just a pain. This saved me a day's work. – jemonsanto May 29 '20 at 13:44
  • 5
    Did I understand correctly that it's not possible to have an endpoint that accepts both an `access_token` and an `id_token` at the same time when using the default Cognito Authorizer?? Isn't that a huge shortcoming in Cognito+API Gateway? What if the endpoint must be used by a sign-in user (`id_token`) and by an external job (`acces_token`) at the same time? Did anyone find a way around this without dropping the use of `id_tokens` entirely? – cahen Oct 09 '20 at 16:19
  • 2
    @cahen we did something similar and we ended up creating multiple junk endpoints with different authorizers then creating a custom authorizer on the actual endpoint that delegated to the junk ones based on the token. I remember thinking it was pretty dumb at the time. – Ravenscar Oct 11 '20 at 09:54
  • @Ravenscar I think I will have to follow a similar approach, many thanks :) – cahen Oct 12 '20 at 11:18
10

For those looking for an answer and are not using OAuth and are deploying using Serverless framework:

What worked for me to make APGW accept accessToken was to modify my serverless.yml file as follows:

functions:
  my-function:
    handler: path to source file
    events:
      - http:
          path: my-function
          method: post
          cors: true
          authorizer:
            type: COGNITO_USER_POOLS
            scopes:
              - YOUR SCOPE HERE <- THIS IS THE TRICK
            authorizerId:
              Ref: ApiGatewayAuthorizer

The value of the scope can be found by reading the contents of your accessToken (for by pasting the token into https://jwt.io/ debugger).

3

Yes, API Gateway will only use idToken to Authorize.

After user enters correct credentials, Access Code is provided by Identity provider authorizing that the user entered correct credential and this access code is used by client just to get you idToken and refreshToken from /oauth2/token endpoint for that given user. All your further calls would only use idToken in Authorization header.

Even that access code expires after you retrieve you user tokens.

  • 2
    Thanks, that does seem to be how is working, but doesn't this contravene best practice for use of tokens? – Harry May 18 '18 at 09:53
  • 1
    This is not true - the Cognito User Pool Authorizer supports both ID Tokens and Access tokens, depending on how it is configured (whether a Scope is specified or not in the Authorizer configuration). See the above (most upvoted) answer. – schiavuzzi Jul 16 '22 at 00:14
2

If anyone was curious how to accomplish this in CDK, here’s how I managed to create an API that accepts an auth token as part of the Authorization header. There's some good information above on how it works conceptually. As the AWS CDK documentation was inevitably lacking, I figured out the CDK way by looking for constructs that mapped to the concepts mentioned above and iteratively adding the right constructs to the api and user pools.

Create a user pool

const userPool = new cognito.UserPool(this, "****");

Create a resource server and scopes. These scopes will be important later when assigning custom scopes to api methods. Identifier - AWS recommends using the domain name

const apiScope = new cognito.ResourceServerScope({
  scopeName: '**',
  scopeDescription: '**'
});

const userServer = userPool.addResourceServer('**', {
  identifier: props.subdomain,
  scopes: [apiScope]
});

Create a hosted UI domain. Users will log into the Hosted UI to get an auth code to use in the auth code authentication flow and receive id/access tokens.

userPool.addDomain('**', {
  cognitoDomain: {
    domainPrefix: '**',
  },
});

Create the client, configure the desired auth flows, and assign the oauth scopes you want to allow for users.

const userPoolClient = userPool.addClient('**', {
  authFlows: {
    adminUserPassword: true
  },
  supportedIdentityProviders: [
    cognito.UserPoolClientIdentityProvider.COGNITO
  ],
  oAuth: {
    flows: {
      authorizationCodeGrant: true,
      implicitCodeGrant: true
    },
    scopes: [
      cognito.OAuthScope.resourceServer(userServer, apiScope),
      cognito.OAuthScope.OPENID,
      cognito.OAuthScope.COGNITO_ADMIN
    ]
  },
  refreshTokenValidity: Duration.days(10)
});

You can deploy the app at this point and see the scopes in the AWS console under User Pools -> User Pool Name -> App Integration -> App client list -> App client name -> Hosted UI -> Custom Scopes. Scopes are a combination of the resource server id and the scope name.

Create a Cognito user pools authorizer for the user pool

const authorizer = new apigateway.CognitoUserPoolsAuthorizer(this, '**', {
  cognitoUserPools: [userPool]
});

Add authorizer to the appropriate method of your API. Make sure to add the correct authorization scopes.

const api = new apigateway.RestApi(this, '***', {
  ...options
});
const resource = api.root.addResource('resource');

resource.addMethod('POST', integration, {
  authorizer,
  authorizationScopes: [
    <scope names>
  ]
});

Adding the correct authorization scopes was crucial, and where I got tripped up for a while. This needs to match at least one of the custom resource server scopes created above.

Yash Dalal
  • 21
  • 4