Search code examples
amazon-web-servicesauthenticationamazon-cognitoserverless-framework

Serverless Framework How to Get Access, Id and Refresh Tokens from AWS Cognito


I am trying to secure my serverless NodeJS apis using AWS Cognito User Pools.

Below is a sample of my serverless framework configuration:

service: hello-world
frameworkVersion: '3'

provider:
  name: aws
  runtime: nodejs14.x
  environment:
    user_pool_id: { Ref: UserPool }
    client_id: { Ref: UserClient }
  iam:
    role:
      statements:
        - Effect: Allow
          Action:
            - cognito-idp:AdminInitiateAuth
            - cognito-idp:AdminCreateUser
            - cognito-idp:AdminSetUserPassword
          Resource: "*"

functions:
  loginUser:
    handler: ./auth/login.handler
    events:
      - http:
          path: auth/login
          method: post
          cors: true

  signupUser:
    handler: ./auth/signup.handler
    events:
      - http:
          path: auth/signup
          method: post
          cors: true
  list:
    handler: ./users/users.handler
    events:
      - http:
          path: users/list
          method: get
          cors: true 
          authorizer:
             type: COGNITO_USER_POOLS
             authorizerId: 
               Ref: ApiGatewayAuthorizer

resources:
  Resources:
    ApiGatewayAuthorizer: 
      Type: AWS::ApiGateway::Authorizer
      Properties: 
        Name: CognitoUserPool
        Type: COGNITO_USER_POOLS
        IdentitySource: method.request.header.Authorization
        RestApiId: 
          Ref: ApiGatewayRestApi
        ProviderARNs: 
          - Fn::GetAtt:
              - UserPool
              - Arn
    UserPool:
      Type: AWS::Cognito::UserPool
      Properties:
        UserPoolName: serverless-auth-pool
        Schema:
          - Name: email
            Required: true
            Mutable: true
        Policies:
          PasswordPolicy:
            MinimumLength: 6
        AutoVerifiedAttributes: ["email"]
    UserClient:
      Type: AWS::Cognito::UserPoolClient
      Properties:
        ClientName: user-pool-ui
        GenerateSecret: false
        UserPoolId: { Ref: UserPool }
        AccessTokenValidity: 1
        IdTokenValidity: 1
        ExplicitAuthFlows:
          - "ADMIN_NO_SRP_AUTH"

I can successfully can call the signup and login endpoints to get a token and then use this token as an Authorization header to call my /users/list endpoint to get a list of users.

My problem is that I was expecting the login endpoint to return 3 tokens - an id token, an access token and a refresh token.

The login endpoint currently only returns one token that has a claim of:

"token_use": "id"

If I pass this token to the /users/list api then it is successfully validated, but I thought that the api would need the access token instead of the id token for authentication.

Does anyone know if my assumption is correct and how to fix the issue or have I misunderstood how the auth flow works ?


Solution

  • I managed to figure out that my login function was only returning the id token. Below is the updated login function to return all 3 tokens:

    const AWS = require('aws-sdk')
    const { sendResponse, validateInput } = require("../functions");
    
    const cognito = new AWS.CognitoIdentityServiceProvider()
    
    module.exports.handler = async (event) => {
        try {
            const isValid = validateInput(event.body)
            if (!isValid)
                return sendResponse(400, { message: 'Invalid input' })
    
            const { email, password } = JSON.parse(event.body)
            const { user_pool_id, client_id } = process.env
            const params = {
                AuthFlow: "ADMIN_NO_SRP_AUTH",
                UserPoolId: user_pool_id,
                ClientId: client_id,
                AuthParameters: {
                    USERNAME: email,
                    PASSWORD: password
                }
            }
            const response = await cognito.adminInitiateAuth(params).promise();
            return sendResponse(200, { 
                message: 'Success',
                accessToken: response.AuthenticationResult.AccessToken,
                idToken: response.AuthenticationResult.IdToken ,
                refreshToken: response.AuthenticationResult.RefreshToken ,
    
                })
        }
        catch (error) {
            const message = error.message ? error.message : 'Internal server error'
            return sendResponse(500, { message })
        }
    }

    Note: I also had to add scopes to the authorizer to be make the api accept the access token

    list:
        handler: ./users/users.handler
        events:
          - http:
              path: users/list
              method: get
              cors: true 
              authorizer:
                 type: COGNITO_USER_POOLS
                 scopes:
                    - aws.cognito.signin.user.admin
                 authorizerId: 
                   Ref: ApiGatewayAuthorizer

    The full signup/login flow that I am using is based on the following free code camp example:

    https://www.freecodecamp.org/news/aws-cognito-authentication-with-serverless-and-nodejs/