Skip to content

Jesses Software Engineering Blog

May 21

Jesse

Serverless Auth with AWS Cognito

The rise of serverless architectures has accentuated the need for modular, robust user auth systems. While there are many options, I’m going to take a look at serverless auth with AWS Cognito. AWS Cognito offers both security with the use of the SRP protocol and JWT, as well as easy implementation.

In AWS there several tools available to run serverless architectures. For this post I will be using:

  • S3 to host a ReactJS client
  • API Gateway to manage routes
  • Lambda for backend processing

Authentication Strategies

Serverless architectures are typically built on top of REST APIs which by definition are stateless. There is no user state on the server, no storing a session hash in a cookie. Each API request needs to be validated and authorized. Whereas in the traditional web app authentication process the server returns a session ID to be stored in a client cookie after a sign in, and then the session ID is used to maintain user state on the server.

One design pattern for authoring authentication layers for REST APIs is to have a user sign up and store their hashed password in a database using a library like bcrypt which uses a salt along with a cipher to create a hash. When a user logs in, the password submitted is checked via the stored hash and if a match, a different hash is sent back to the client and stored. On subsequent requests that hash is passed along and the servers verifies that the hash is valid and active for that user, verifies that the user has access to the given resources, then returns any data and potentially a fresh hash. Since these hashes are being passed along with each request they are susceptible to interception.

When storing user passwords in the database, it is crucial that strong hashing algorithms are used. Dictionary attacks can be used to brute force simple hashing algorithm relatively easily. By storing user passwords in a database, there is not only the potential for a brute force attack, but also potential for losing secure information in the case of a database breach. Another issue with the above approach is that the password is sent across the wire each time a user logs in, which could be intercepted via a man in the middle attack. There’s also a lot of logic that needs to be shared across the code base for checking hashes, generating tokens, verifying auth, mapping user roles to resources, etc. While this can be modularized into shared libraries, there’s still a lot of pre checking every time a route is hit and a lot boilerplate code, adding to the code’s complexity, as well as incurring additional database calls to pull and validate credentials.

Secure Remote Password

The Secure Remote Password (SRP) protocol is a way for a client application to let the server know that it is making a valid user request without having to send the password along with the request. Therefore, it also does not need to store a hash of the password anywhere, rather a verifier function is stored that can validate a request hash. This means that any information intercepted on the way to the server will not contain sensitive information, nor will the database. This significantly reduces attack vectors on the user authorization layer.

JSON Web Tokens

JWT is a standard way for defining compact, secure tokens used with secure communication. Tokens can be digitally signed either with the HMAC algorithm or with RSA key pairs. The tokens contain three parts: 1) header, 2) payload, 3) signature, each section being separated by a period. Since the tokens are self contained, once a stateless server receives a token, access to protected resources can be validated based solely on the token thus not having to pull in additional resources or run additional checking logic.

AWS Cognito

AWS Cognito offers several features including both User Pools and Federated Identities.

User Pools is a managed user directory, offering user signup and signin, two factor auth, email and phone verification, and various other configurations. Having a managed user directory relieves the need for writing and maintaining a user database or having to store sensitive password information. User Pools are based off of SRP, so requests to the Cognito User Pool do not require a password to be sent over the wire. If you look at the request payload when signing in via User Pools, you will see values such as: PASSWORD_CLAIM_SECRET_BLOCK, PASSWORD_CLAIM_SIGNATURE, SRP_*, being POST to the service.

AWS Federated Identities handles authorizing users to access AWS resources, either via third party federated providers such as Facebook or Google, or via Cognito User Pools. Once an identity is obtained, it can be used to get temporary AWS credentials for accessing services such as API Gateway.

There are various work flows that can be used with AWS Cognito. This post will focus on creating and signing in users via Cognito User Pools and getting temporary AWS credentials via Federated Identities to access API Gateway.

AWS Implementation

To start, create a User Pool in AWS Cognito. The creation process will take you through user profile attributes, password policies, two factor auth settings, email templates, cient association, and workflow hooks. For this code sample, I am just selecting name and email, no aliases. Aliases provide a way to allow multiple different user names for logging in. After pool creation, you will be given both a Pool ID and a Pool ARN, take the Pool ID and the App Client ID and place them into the client code.

IMPORTANT: When associating clients to user pools, do not click the generate secret token box, this is only applicable for apps and will lead to browser authentication issues.

After the user pool has been created, set up a Federated User Identity Pool for handling credential management. During creation, set up a Cognito Identity Provider with the Pool ID and the App Client ID. Also take note of the AWS User Roles assigned to the Identity Pool, there will be two, one for authorized users and another for unauthorized users. Note that the Identity Pool ID will need to be placed into the client code. Here is an overview of the JavaScript SDK for working with Cognito Credentials.

We will also need a backend for the client that we can secure. For this project I set up an API via API Gateway which invokes a Lambda function. The Lambda simply returns a connected response. The authorization layer is built into API Gateway. When setting up the routes in API Gateway be sure to enable CORS. CORS will require the Access-Control-Allow-Origin header for both the Method Response and the Integration Response. For this demo I’m setting up two routes, /secure and /vip. Both routes point to the same Lambda but will have different auth roles. For the route authorization, under Method Request, select AWS_IAM. This will require that AWS credentials be used when accessing the API. Under the stages setting, there’s a SDK generation tab which will download a library for helping with request signing, but I chose to use a npm package instead for easier ReactJS integration; although, I likely wouldn’t use an unverified npm module in a production environment.

The idea behind the two routes, /secure and /vip, is that one is for authorized users and the other is for admin users. Going back to the default roles that the Federated Identity Pool created, we are going to whitelist the /secure route for the auth role by setting up an IAM policy allowing access to /secure and attaching it to the default role:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Stmt1495218657000",
            "Effect": "Allow",
            "Action": [
                "execute-api:*"
            ],
            "Resource": [
                "arn:aws:execute-api:us-west-2::/*/GET/secure"
            ]
        }
    ]
}

Once the policy is set and the the AWS Cognito ids are plugged into the client code, a user can login and view the secure page; however, when they hit the /vip route, they will get an error. To handle admin users, we can leverage User Pool groups. Create a new role with a policy whitelisting both the /secure and the /vip endpoints. It’s required that the new role has a trust relationship with Cognito defined which is linked via the Identity Pool ID (this is configured in the IAM role interface):

{
      "Effect": "Allow",
      "Principal": {
        "Federated": "cognito-identity.amazonaws.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "cognito-identity.amazonaws.com:aud": "us-west-2:aa94bd82-c85a-4af4-b639-b50e5ddd2fb1"
        },
        "ForAnyValue:StringLike": {
          "cognito-identity.amazonaws.com:amr": "authenticated"
        }
      }
    }

After the new role is set up, create an admin group in the User Pool interface, and attach this role to the group. Finally, in the Federated Identity configuration, under the Authentication Provider, select Choose Role from Token and Use Default Auth Role settings. This will use the User Pool group IAM role if the user is in a group, otherwise it will fallback to the default role.

An extra step to secure the API is to add API key requirements to the resources. While API keys should never be used to authorize users, they are particularly useful for throttling API requests and setting request quotes. This will help ensure costs don’t go higher than expected. AWS CloudFront alarms can be set to trigger either a page or Slack message / email if throttles are hit to help ensure no customer down time. API key requirements are enabled on the routes, where the AWS_IAM setting was configured. A usage plan will need to be created and associated with the API stages.

That’s it for the AWS implementation. At this point there are two functional API Gateway routes, both pointing to a Lambda (or backend). The /secure route is accessible by any authorized user, while the /vip route is only accessible by users in the admin group.

JavaScript Integration

SourceExample
Login: user OR admin@jessesnet, Password: House1234?

NOTE: AWS provides a great npm SDK.

For sign up the user data is wrapped as CognitoAttributes. The CognitoUserPool represents a wrapper for the Cognito User Pool service. When you enter a valid email, you will receive a confirmation email with a confirmation code. I didn’t build out the code confirmation page as users can be confirmed via the AWS UI.

When signing in, the CognitoUserPool is associated with a CognitoUser and passed into an authentication call along with AuthenticationDetails. Inspection of the headers on sigin show that no user password is sent out, rather hashes for being verified on the server. After a successful sign in, the CognitoUser object will have access to three JWT tokens: 1) access, 2) ID, 3) refresh. The ID token contains all the user info which the client app is allowed to read, and is used when requesting IAM credentials. The access token is used to change information about a user, and the refresh token is used to refresh the access token after it has expired.

After signing in the Cognito user is automatically saved to local storage and can be retrieved via the getCurrentUser call and used through out the application. When accesing the route, the user is pulled from local storage and a session is created which has access to the three JWT tokens outlined earlier. The ID token is used to get the temporaray AWS IAM credntials from the Identity Pool. The IAM credentials can then be used to sign the request to API Gateway via the API Gateways SDK generated earlier. NOTE: The session token also needs to be passed in via the x-amz-security-token header and the API key as x-api-key.

Finally, signing out is called on the user to end the session for the client app. There is also a globalSignOut function which will invalidated all the current tokens, effectively signing out of all client apps.

As part of the serverless architecture, the client is hosted on S3. To develop the sample code, run npm start, and to build static files for S3, run npm run build; the static build files are placed into a web hosted enabled S3 bucket. I would likely throw CloudFront in front of API Gateway, the CDN can help with performance on the static files from S3. You can also turn on transfer acceleration on the S3 bucket for added performance and an additional cost. By hosting in S3 auto deployment is simply copying the build to S3 and potentially invalidating a cache. The S3 bucket will require an open bucket policy as well as being enabled as a web server:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PublicReadGetObject",
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::reactjs-aws-cognito/*"
    }
  ]
}

Conclusion

This post outlines how to do AWS serverless authorization with AWS Cognito using the Secure Remote Password protocol and JWT. While Cognito offers a lot of great services, two factor auth, email confirmation, captchas, third party integration, be aware of the pricing. There is a generous free tier, but the prices do get a bit steep once a user base gets into the millions. Also the code will be highly coupled to AWS. Another solution to check out is Google’s Firebase. Overall though, Cognito is a great service and addition to a serverless stack, and is a relatively straight forward integration allowing for rapid development.

Blog Powered By Wordpress