Authentication & Authorization
The order API is protected with Amazon Cognito access tokens. Each Lambda function verifies the token itself with the Powertools for AWS Lambda JWT verifier (aws_lambda_powertools.utilities.auth_alpha.JWTVerifier), there is no API Gateway authorizer.
Every signed in user can create, get and list their own orders. Deleting an order also requires the orders/manage scope, which only members of the orders-managers group get. On top of that, users only ever reach their own orders, see Order ownership.
Alpha utility
The auth_alpha utilities may change before they are generally available. Pin the Powertools version and install the extra: aws-lambda-powertools[jwt].
How it works¶
- A caller signs in to the Cognito user pool and gets an access token.
- The caller sends the token in the
Authorization: Bearer <token>header. - The route middleware created by
verifier.require()verifies the signature (with the user pool JWKS), the issuer, the expiration, the app client and the audience. On the delete route,verifier.require(scopes=['orders/manage'])also checks the scope. - The response is
401for a missing or invalid token (an ID token is rejected too) and403on delete without theorders/managescope. The/swaggerendpoints stay public. - The handler reads the caller's user id from the verified token and only reaches that user's orders, see Order ownership.
| Route | Who can call it | Middleware |
|---|---|---|
POST /api/orders, GET /api/orders, GET /api/orders/{order_id} |
Any signed in user (valid access token), on their own orders | REQUIRE_SIGNED_IN |
DELETE /api/orders/{order_id} |
Members of orders-managers (orders/manage scope), on their own orders |
REQUIRE_MANAGE |
Verifier and routes¶
| service/handlers/utils/auth.py | |
|---|---|
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | |
The verifier is created once per execution environment. It caches the user pool JWKS for 5 minutes (the Powertools default) and then fetches it again, so warm invocations don't download the keys on every request. A route uses the verifier with middlewares=REQUIRE_SIGNED_IN (create, get, list) or middlewares=REQUIRE_MANAGE (delete).
The LIST function is the exception: it runs in the Lambda Managed Instances VPC, which has no route to the public JWKS URL. A CloudFormation custom resource fetches the JWKS at deploy time and passes it to the function in the COGNITO_JWKS environment variable,
and the verifier uses those keys instead of downloading them. A new user pool triggers the custom resource again, which fetches the new keys.
Signing key rotation
Cognito might rotate the signing keys of a user pool and does not publish a schedule.
After a rotation the LIST function only has the keys fetched at deploy time, so it rejects new tokens with 401 until the next deployment fetches the new keys.
A production service should refresh the keys periodically, for example with a scheduled function that stores the JWKS in SSM Parameter Store, read by the LIST function through a VPC endpoint.
Validation runs before authorization
The Event Handler validates the request before it runs route middleware. A caller without a token that sends an invalid request gets a 422 before it gets a 401. A valid request gets the 401.
Order ownership¶
Every order belongs to the user that created it, and users can only get, delete and list their own orders.
| service/handlers/utils/caller.py | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
- The owner is the access token
subclaim, read withget_caller_id()from the claims the auth middleware adds to the resolver context.subis immutable and unique, unlike the username, which can be reused after a user is deleted. - The orders table is keyed by
owner_id(partition key) andid(sort key). Get and delete read the item by both keys, so an order of another user returns404, exactly like an order that does not exist. This does not reveal which order ids exist. - List runs a strongly consistent
Queryon the caller's partition instead of aScanof the whole table. Anext_tokenof another user, or a forged one, returns400. - The owner is part of the create order idempotency key, so the same request body sent by two users creates two orders.
Ownership comes first
The orders/manage scope does not grant access to other users' orders: a manager deletes only their own orders, and gets 404 for an order of another user. To let a support or admin role read or delete across owners, add a separate scope and an explicit code path for it.
The audience claim¶
JWTVerifier.cognito() requires the aud claim of the access token to match the resource audience. Cognito only adds a resource bound aud to access tokens in Managed Login flows, not to tokens issued by the Cognito API (AdminInitiateAuth), and those tokens only have the aws.cognito.signin.user.admin scope.
To get tokens without a browser, the user pool has a pre token generation trigger (event version 2, which requires the Essentials feature plan) that adds to the access token:
aud, set to the app client id.- The scopes of the user's groups:
orders-managersgetsorders/manage. A user in no group gets no scope, and can still create, get and list their own orders.
| service/handlers/handle_pre_token_generation.py | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
The audience is therefore the app client id, which is weaker than a resource bound audience: every API that trusts this app client accepts the same token, so a token issued for one of them can be replayed against another. If you use Managed Login, request resource binding and pass your resource URL as the audience.
Testing with real tokens¶
The integration and e2e tests do not mock the verifier. They use real tokens from the deployed user pool.
- The helpers in
tests/auth_utils.pycreate a user with the admin API, add it to a group, sign it in to get a real access token and delete it. - The fixtures in
tests/conftest.pycreate three users once per test session:member_user(no group, the regular user),manager_user(inorders-managers) andother_manager_user(to test order ownership). They are deleted when the session ends, also when a test fails. - The bearer token is passed to the tests as ready to use
Authorizationheaders:member_headers,manager_headersandother_manager_headers. order_factory(owner, name, item_count)creates orders owned by a test user (owner.sub) directly in DynamoDB.
The identity that runs the tests needs these permissions on the user pool: cognito-idp:AdminCreateUser, AdminSetUserPassword, AdminAddUserToGroup, AdminInitiateAuth and AdminDeleteUser.
More Details¶
- Powertools for AWS Lambda v3.36.0 release notes, which introduced the JWT verifier
- Understanding the access token (Amazon Cognito)
- Pre token generation Lambda trigger (Amazon Cognito)
- Scopes, M2M, and resource servers (Amazon Cognito)