
APIs Beginner to Advanced - API Authentication and Authorization on AWS with Cognito and JWTs
Learn API authentication vs. authorization with Amazon API Gateway, Cognito, and JWTs. Build public and protected routes, then test access with two users.
Series: New To AWS Beginners Guide (9 articles)
- …
- 9APIs Beginner to Advanced - API Authentication and Authorization on AWS with Cognito and JWTs This article
Follow Alice's bank-balance request through Amazon Cognito, an API Gateway JWT authorizer, and an account access check. Then build it with starter code and Agent Toolkit for AWS.
Alice should be able to check her bank balance. Bob shouldn't, even after he signs in to the same app. Bob isn't a hacker. He's curious, and he knows how to edit a URL.
A sign-in screen alone is a bouncer who checks your ticket, then lets you wander into every dressing room backstage. Your API also needs to check which account each person can open.
You'll follow one request through a fictional banking app with four stops: authentication versus authorization, identity providers, tokens, and authorizers. You don't need the earlier videos. You only need to know that an app sends a request and an API sends back a response.
Quick answer: authentication establishes an identity, and authorization decides what that identity can access. In this example, Amazon Cognito signs Alice in and issues an access token. An API Gateway JWT authorizer validates the token and checks the route's required scope. Our Lambda code makes the final call on the specific account Alice requested.
This learning app uses fictional balances and two test users, so no real money gets hurt and every access check stays visible.
What is the difference between API authentication and authorization?
Authentication checks who you are. Authorization checks what you're allowed to do.
Alice signs in, and the app now has evidence that she is Alice. That's authentication.
Alice asks to see her account,
5521, and the app allows it. She asks for Bob's account, 8842, and the app refuses. Those are authorization decisions.
The tiny world you'll build:
| Person | Can access | Cannot access |
|---|---|---|
| Alice | Account 5521 | Bob's account 8842 |
| Bob | Account 8842 | Alice's account 5521 |
| Someone who hasn't signed in | Public branch locations | Either account balance |
An account number in a URL tells the API which account someone requested. It doesn't prove the requester may view it. Typing
8842 into a URL proves Bob can type. The handler code comes back to this.How does Amazon Cognito sign users in to an API-backed app?
First, something has to check Alice's sign-in details. That job belongs to the identity provider, the service responsible for signing in the app's users.
This project uses Amazon Cognito. A Cognito user pool is the directory of your app's users. An app client tells Cognito which application is asking to sign someone in, and your local browser app gets its own. Cognito user pool overview
Alice signs in on a Cognito-hosted page and returns to the app. She never signs in to API Gateway, and the account-balance handler never sees her password.
This tutorial has two different sign-ins:
| Sign-in | Who uses it? | What is it for? |
|---|---|---|
| Your AWS sign-in | You, the builder | Creating and managing the project's AWS resources |
| Your app's Cognito sign-in | Alice and Bob | Using the fictional banking app |
Alice doesn't need an AWS account. She barely has a bank account.
Use a user pool for this exercise. You don't need a Cognito identity pool, which supplies temporary AWS credentials. (Yes, Cognito has two kinds of pools. Picking the right one is the first test.) Other compatible identity providers also work with API Gateway. User pools and identity pools
Why does an API need an access token after sign-in?
Alice signs in once, but every balance check is a new request. The API needs to recognize her without asking for her password each time.
The app carries an access token with each protected request. Think of it as a concert wristband handed out after sign-in. Staff at each door check the wristband, not your ID.

The balance request looks like this, with a placeholder token:
1
2
3
GET /accounts/5521/balance HTTP/1.1
Host: YOUR_API_ID.execute-api.YOUR_REGION.amazonaws.com
Authorization: Bearer <access_token>The route is the HTTP method and path together:
GET /accounts/5521/balance. GET asks to read something. A header carries extra information, and Authorization is where the pass goes.Bearer means whoever holds the token can present it. That's convenient, and it's why a stolen token matters: whoever holds the wristband gets in, including the person who found it on the floor. Keep tokens out of URLs, logs, and source control (and the screenshot you post asking "why is this a 401?"), and send them over HTTPS. Bearer token usage
What is a JWT, and what are claims?
The Cognito access token uses the JSON Web Token, or JWT, format. The spec suggests pronouncing it "jot." The signed JWT in this example has three dot-separated parts:
1
header.payload.signatureThe header describes how the token was signed. The payload holds claims, statements such as a user identifier, issuer, and expiry time. The signature lets a verifier detect changes and confirm a trusted issuer signed it. JWT specification

Below are selected claims from an illustrative decoded payload. The identifiers are fictional, and the timestamps are Unix times: seconds since January 1, 1970, the day computers decided time began.
1
2
3
4
5
6
7
8
9
{
"sub": "alice-user-id-from-cognito",
"iss": "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_EXAMPLE",
"client_id": "example-browser-app-client",
"token_use": "access",
"scope": "openid bank-api/accounts.read",
"iat": 1791849600,
"exp": 1791853200
}| Claim | Beginner explanation |
|---|---|
sub | The user's identifier within the user pool; our code uses it to look up access |
iss | The issuer: the user pool that issued this token |
client_id | The Cognito app client used to sign in |
token_use | What kind of token this is; here, an access token |
scope | Named permissions carried by the token |
iat / exp | When it was issued / when it expires |
Cognito assigns the real
sub value. Don't substitute an email address or the word Alice when binding the starter's account data. Cognito access token claims Decoding a JWT doesn't validate it. The payload is encoded, not encrypted, which is a fancy way of saying "readable by anyone with a decoder and two minutes." If someone edits the payload to swap Alice's user ID for Bob's, the original signature no longer validates, the way a bank notices a check signed in crayon. The API must verify the token before trusting its claims. Verifying Cognito JWTs
Should the app send an access token or an ID token?
Send the access token to this API. An ID token describes the signed-in user to the client application. Handing an ID token to this API is like showing the bouncer your library card: real ID, wrong door. The protected routes require the custom scope
bank-api/accounts.read, which lives in the access token. That scope requirement also helps keep an ID token from slipping through. Cognito token types How does an API Gateway JWT authorizer protect a route?
Alice's app sends the pass. Someone has to check it.
For our API Gateway HTTP API, a built-in JWT authorizer does that work. An authorizer evaluates credentials and configured permissions before a request reaches the integration, here a Lambda function.
You configure the authorizer with the Cognito issuer, the expected audience, and
$request.header.Authorization as the token source. Gateway verifies the signature and time claims, checks the issuer and audience, evaluates the route's scope, then passes validated claims to Lambda. API Gateway JWT authorizers Here, the audience setting is the Cognito app client ID. Cognito access tokens have
client_id and no aud claim. Gateway checks client_id when aud is absent, and if a token has aud, Gateway checks that instead. Use the user pool's issuer URL, not the sign-in page's domain. Mixing those two up is a reliable way to spend an afternoon staring at 401s.Gateway validates signatures with the provider's public signing keys and can cache them, so it isn't phoning Cognito on every request like a nervous intern.
Which API routes should be public or protected?
Branch locations stay public, and account data stays private:
| Route | JWT authorizer | Required scope | What Lambda does |
|---|---|---|---|
GET /branches | None | None | Returns fictional branch locations |
GET /accounts | Required | bank-api/accounts.read | Returns only accounts accessible to the validated user |
GET /accounts/{accountId}/balance | Required | bank-api/accounts.read | Checks access to that account before returning its balance |
The braces in
{accountId} mean "take the account ID from this part of the path," so /accounts/5521/balance fills it with 5521.
What is an OAuth scope, and why isn't it enough?
OAuth 2.0 defines how applications obtain and use access tokens. A scope is a named permission carried by a token. Our
bank-api/accounts.read scope lets the token call the account-reading routes you attach it to.It doesn't mean "read every account." Alice and Bob can both hold that scope and still have access to different records, the same way a gym membership doesn't come with a key to everyone's locker.
In Cognito, you define a resource server with identifier
bank-api and a scope named accounts.read. Here, "resource server" is a configuration describing your API's permissions, not another machine. Naming things is hard, even for standards authors. Cognito resource servers and custom scopes That gives you two authorization checks: Gateway checks permission to call the route, and your handler checks access to the requested account.
How do you stop one user from accessing another user's account?
Suppose Alice edits the request path:
1
2
GET /accounts/8842/balance
Authorization: Bearer <Alice_access_token>Her token can be valid, unexpired, and correctly scoped. The account is still Bob's.
Your handler, the function that processes the request, must compare the validated identity with the account's permitted users. Here each account has one owner. Larger apps can add joint owners or delegated access.
The core check, simplified from the starter handler :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
claims = event["requestContext"]["authorizer"]["jwt"]["claims"]
subject = claims["sub"] # Identity from Gateway's validated token
account_id = event["pathParameters"]["accountId"]
accounts = demo_accounts() # Fictional data configured on the server
account = accounts.get(account_id)
if account is None or subject not in account["allowedSubjects"]:
return response(404, {"message": "Account not found"})
return response(200, {
"accountId": account_id,
"balanceMinor": account["balanceMinor"],
"currency": account["currency"],
})The full handler also handles missing identity and filters the account list. It takes trusted identity from the API Gateway event, never from a
userId field in a URL, query string, or request body. Trusting the browser's word would be like a bank accepting a sticky note that says "I'm Alice." allowedSubjects is the server's list of permitted users.balanceMinor stores money as whole minor units, cents for USD, so 125000 means $1,250.00. Integers keep the math exact. Floating-point is how 0.1 + 0.2 becomes 0.30000000000000004, which nobody wants on a bank statement.The handler returns the same
404 for an unknown account and for an account this user can't access, so the API plays dumb on purpose and never confirms whether another person's account exists. That's our application's choice, not something the JWT authorizer does for you.Alice's own account returns
200 OK with JSON the browser can display:1
2
3
4
5
{
"accountId": "5521",
"balanceMinor": 125000,
"currency": "USD"
}Gateway checks the token and route scope. Your code checks access to the account.
What do 401, 403, and 404 mean in this example?
Failures happen at different steps, so a denial doesn't always mean sign-in broke. A
401 says "who are you?", a 403 says "I know who you are, and no," and this app's 404 says "what account?"| Request | Expected result | Where the decision happens |
|---|---|---|
GET /branches, no token | 200 OK | Public route and Lambda |
| Protected route, no token | 401 Unauthorized | API Gateway |
| Protected route, expired or invalid token | 401 Unauthorized | API Gateway |
| Protected route, valid access token missing our scope | 403 Forbidden | API Gateway |
Alice requests account 5521 | 200 OK | Lambda permits account access |
Alice requests Bob's account 8842 | 404 Not Found | Lambda hides inaccessible account |
Bob requests account 8842 | 200 OK | Lambda permits account access |

When Gateway rejects the token or scope, the handler never runs. If Alice's token passes but she requests Bob's account, Lambda runs and rejects it. AWS examples show both invalid-token responses and 403 responses for insufficient scope .
Build it: a Cognito and API Gateway authentication starter project
Your challenge is to make every row of that test table pass. Clone the starter code repo on GitHub (or use Code > Download ZIP) and open the project README . You get a local browser app, an AWS SAM template, tests, and cleanup steps.
What architecture will you build?

The browser is the frontend, where Alice clicks buttons. API Gateway receives requests and selects a route. Cognito handles sign-in and issues tokens. Lambda runs the backend code.
The starter keeps made-up accounts in the Lambda code and binds them to the two users' real Cognito
sub values through configuration. No database to provision means no database to forget to delete. The browser never decides who owns an account.AWS SAM describes the resources as code, and deploying its template creates a CloudFormation stack, a group of AWS resources managed together. The stack includes the user pool, app client, sign-in domain, scope, API, Lambda, and supporting permissions and logs.
Start with the new AWS sign-in experience
Start with the new AWS sign-up experience , or sign in to an existing project . For a walkthrough, see my guide to getting started with the new AWS experience .
AWS documents a limited rollout. Its supported-services list includes Cognito, API Gateway, Lambda, CloudFormation, and CloudWatch. Use your project's assigned Region and check its permissions and plan before deploying. Availability doesn't mean every resource is free. New AWS experience and supported services
This is your builder sign-in. You'll create Alice and Bob in Cognito.
Run the starter and connect the sign-in flow
Open the cloned
api-authentication-authorization-starter-code folder in your editor. The README covers these milestones:- Run the local handler tests. Confirm Alice/Bob isolation first.
- Deploy the SAM template in your chosen learning project. The initial account bindings deny access until you configure them.
- Generate the browser configuration from the stack outputs and start the frontend.
- Create two Cognito test users through the sign-in page, then bind account
5521to Alice'ssuband8842to Bob'ssubin the stack configuration. - Sign in as each user and run the test matrix, including requests for the other account. Channel your inner Bob.
The browser uses Cognito's authorization code flow with PKCE, short for Proof Key for Code Exchange and pronounced "pixy." Cognito sends a short-lived code back after sign-in. The app exchanges that code for tokens and proves it started the sign-in. The public app client has no client secret, because a secret shipped in browser code is a secret shared with everyone who opens dev tools. Cognito authorization endpoint and PKCE
Request
openid bank-api/accounts.read through that OAuth flow. A common gotcha is calling a password-authentication API, then wondering where the custom scope went. That ordinary API sign-in flow doesn't issue custom scopes, so the starter uses the OAuth endpoints. Cognito custom-scope behavior The frontend keeps the access token in memory, never in browser storage. Reloading means signing in again, which is annoying in a real app and handy in a lab. A separate "without account scope" sign-in option helps you reproduce the
403 case.Use this prompt with your coding assistant and Agent Toolkit for AWS
Agent Toolkit for AWS gives a compatible coding assistant AWS documentation, skills, and tools for working with your AWS project. It helps you build and inspect resources. Alice's balance request travels through the deployed API, not the coding assistant. Agent Toolkit overview
Connect your assistant with my Agent Toolkit setup guide , or follow the official setup instructions . Then open the cloned repo folder in your assistant and paste this prompt. The repo's PROMPT.md has a longer, more detailed version if you want extra guardrails:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Help me build this beginner API authentication lab, and explain each step.
Read README.md, template.yaml, the Python handler, tests, and frontend first.
Use Agent Toolkit for AWS and current official AWS documentation.
Confirm my AWS project/account, assigned Region, supported services,
and development permissions. Explain the resources and expected charges
before deployment. Use my current plan and made-up data only.
Follow the starter's architecture:
- Amazon Cognito user pool, public app client without a client secret,
and hosted sign-in using authorization code flow with PKCE.
- API Gateway HTTP API with a JWT authorizer using the user pool issuer
and app client ID as its configured audience.
- Public GET /branches, with no authorizer.
- Protected GET /accounts and GET /accounts/{accountId}/balance,
both requiring the bank-api/accounts.read scope.
- Lambda reads sub from Gateway's validated claims, filters the account
list, and checks access before returning a requested balance.
Run the local tests. Help me create Alice and Bob as separate test users
and bind their actual Cognito sub values to accounts 5521 and 8842.
Explain why an email or userId sent by the browser is not proof of identity.
Walk me through live tests: public access, no token, invalid/expired token,
valid token without the required scope, own-account success, and cross-user
denial in both directions. Use the access token, not the ID token.
Never print tokens or passwords, or add them to URLs, source, or logs.
Show which outcomes were tested locally and which were verified on AWS.
Finish by walking me through deleting the learning stack and checking
for leftover resources created during deployment.A green sign-in screen proves your login page works. Success means Alice sees Alice's balance, Bob sees Bob's balance, and editing the account number can't cross that line.
API authentication interview questions: can you explain the decisions?
Does a valid JWT prove a user can read a particular record?
No. It supplies validated claims and can carry permissions, but the application must still apply record-level access rules. Here, the account number comes from the request, the identity comes from validated claims, and the handler connects the two.
Is decoding a JWT the same as verifying it?
No. Decoding makes the data readable. Verification checks whether you can trust it: signature, issuer, audience, and time restrictions. Here, the built-in authorizer does that.
Does CORS protect an account balance?
No. CORS controls which browser origins may read responses. Other clients can call the API directly, and
curl has never heard of CORS. Configure CORS for your frontend, but keep JWT and account checks on protected routes. The starter configures HTTP API CORS so browser preflight requests work too. HTTP API CORS Does this exact authorizer setup apply to REST APIs?
This tutorial uses the HTTP API built-in JWT authorizer. REST APIs use a different Cognito user pool authorizer configuration. Follow the instructions for the API type you built. REST API Cognito authorizers
Does signing out immediately invalidate a copied token?
Don't assume so. Signing out doesn't reach into someone else's clipboard and erase a token they copied. Cognito also documents that revocation doesn't make a JWT fail an external verifier that checks only its signature and expiration. Token lifetime and token protection still matter. Cognito token revocation
Build the lab, run the two-user test, and explain one allowed and one denied request out loud. If you can point to who signed in, what the token proves, and where the app decides account access, you understand the core of API authentication and authorization. And if Bob edits the URL and gets a
404, you built it right. Sorry, Bob.Series: New To AWS Beginners Guide (9 articles)
- …
- 9APIs Beginner to Advanced - API Authentication and Authorization on AWS with Cognito and JWTs This article
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article