In this blog post, you will learn to implement authentication and authorization for your own HTTP(S)-based applications on AWS.
Most applications offer some functionality only to authenticated clients. A client can be a human or a machine. Humans usually authenticate with username, password, and optionally a time-based one-time (TOTP) password. Machine authentication works differently. E.g., using API keys or certificates (mTLS). Authentication answer the question of who you are.
Some applications allow more granular control to allow or deny certain functionality to a subset of clients only, aka authorization. Authorization answers the question of what you are allowed to do.
Nowadays, users enjoy signing in with already existing user credentials. For example, with their Google, Facebook, or corporate identity. Whether the application is internal or external, users expect to sign in using an Identity Provider (IdP) of their choice. This could be your corporate IdP, Google, or Facebook. The simplified flow looks like this: When a user uses your application for the first time, the user is redirected to the IdP. The IdP handles the authentication and redirects the user back to your application with some sort of token.
Your application must integrate with the IdP to verify that the token is valid. The authentication itself is done by the IdP. Luckily, most IdPs implement the standard OpenID Connect (OIDC), which improves the integration challenge slightly.
If you want your own user database (aka IdP), Amazon Cognito user pool is the service of choice. Cognito also integrates with other IdPs to offer maximum flexibility.
Luckily, some AWS services deal with implementing the IdP integration for you:
- Amazon API Gateway
- REST: Validates OIDC JWT token issued by Cognito user pool only.
- HTTP: Validates OIDC JWT token (includes Cognito user pool issued tokens).
- AWS AppSync: Validates OIDC JWT token (includes Cognito user pool issued tokens).
- Application Load Balancer (ALB): Implements the complete OIDC authentication flow by redirecting the user to the IdP and remembering the token in a cookie (includes Cognito user pools).
Hint: If your application is a SPA, you can use AWS Amplify to implement the authentication flow in the browser.
If you are interested in the details, check out our Comparing API Gateways on AWS article.
Let’s divide this problem into two: Machines running inside AWS and machines running outside of AWS. A machine could be a virtual machine, container, or function.
Inside AWS, you can leverage the IAM service to authenticate (and also authorize) client machines. Your machine needs IAM credentials (e.g., via EC2 Instance Profiles or ECS Task Roles), sign requests by following SigV4, and the endpoint must be capable of verifying a SigV4 request. Only AWS has the keys to do this! The following AWS services support this:
- API Gateway (HTTP and REST)
Hint: Users can also get AWS credentials by using Cognito identity pools (instead of user pools) to use the same authentication mechanism as machines do.
If your machines are EC2 instances, you can leverage signed Instance Identity Documents for authentication. Add the identity document with the signature to the Authorization header of your requests. The receiver must validate the signed document (e.g., in a custom authorizer or your application code).
API Gateway REST API supports mutual TLS (mTLS). With mTLS, clients must present X.509 certificates to verify their identity to access your API. It is still a challenge to make the certificates available to your clients safely and rotate them regularly.
In all other cases, you are on your own. I recommend checking out Envoy and OPA. Envoy receives all requests and reaches out to OPA to make an authorization decision before the request is forwarded to the backend. The authorization decision is either based on the data on the request itself (e.g., verify JWT and check the groups claim) or more involved by taking external data into account.
So far, we have two sets of clients. Unauthenticated and authenticated clients. If you need more fine granular control, you need a way to authorize requests based on:
- roles such as admin
- or more fine-grained attributes such as resource owner
During the authentication, a client requests certain scopes, that it wants to access. The IdP authorizes the client for scopes and adds the corresponding claims -which are more fine granular- to the JWT token. For example, Cognito user pools support adding users to groups. The group membership is made available in the
cognito:groups claim in the JWT. Your application can inspect the (already verified) JWT token to check the user’s groups.
Only the API Gateway HTTP APIs support authorization based on scopes. All of the other presented AWS services do not support making authorization decisions for you. As a workaround, the API Gateway (REST and HTTP) provides custom authorizers. A custom authorizer generates an IAM policy with fine-grained control over the API endpoints (HTTP resource + verb) that the client can invoke. The policy can also be cached for latency-critical applications.
Authenticating users is a solved problem on AWS. The quickest way to get authentication working is ALB + Cognito user pool. You can also leverage Cognito user groups to implement a lightweight authorization layer in your application.
Machine-to-machine communication is where things get more complicated. Unfortunately, the ALB does not cover that use case. It gets even worse if some machines run inside AWS while others run outside.