
Identity in Serverless Architectures: Authentication Patterns for Lambda and Cloud Functions
An examination of identity management patterns for Lambda and cloud functions, focusing on Cognito authorizers and serverless security.
In traditional monolithic architectures, the application server holds the session state and performs the heavy lifting of identity validation before routing a request to a database. In serverless architectures, specifically using AWS Lambda and Cloud Functions, this model shifts dramatically. The function itself is ephemeral and stateless; it cannot hold a session. Instead, identity becomes a transient artifact attached to the incoming HTTP request, passed through a strict chain of trust before the function code ever executes. The core mechanism here is not "authentication" in the sense of logging in, but rather "context propagation" where a verified identity token is injected into the Lambda event payload.
The Trust Boundary: API Gateway as the Gatekeeper
The first mechanism to understand is the role of the API Gateway. It acts as the sole entry point and the primary trust boundary. When a request hits your Lambda function, it has already passed through this boundary. The most common pattern involves an Amazon Cognito User Pool acting as the Identity Provider (IdP).
Consider a scenario where a mobile application, AppClient, attempts to access a protected resource, UserProfileService. The request arrives at the API Gateway endpoint /users/profile. The API Gateway is configured with a Cognito Authorizer. This authorizer does not execute business logic; it performs a cryptographic handshake. It extracts the Authorization header containing a Bearer token (typically a JSON Web Token or JWT).
The mechanism works as follows:
- Extraction: API Gateway pulls the token from the header.
- Validation: It contacts the Cognito User Pool's public key endpoint (or uses a cached key) to verify the token's signature. This ensures the token was issued by the trusted IdP and has not been tampered with.
- Decoding: If valid, the authorizer decodes the JWT payload.
- Injection: The authorizer constructs a new event object. It injects the user's identity data (specifically the
claimsfrom the JWT) into therequestContextof the Lambda event.
If you inspect the Lambda event payload in this flow, you will see a requestContext object that contains authorizer data. This data includes the claims map, which holds attributes like email, sub (subject), and custom:role. The Lambda function never sees the raw JWT string; it only sees the parsed, validated claims. This separation of concerns is critical. The function assumes the identity is valid because the API Gateway would have rejected the request otherwise.
JWT Mechanics: The Data Payload
To understand how authorization works downstream, one must dissect the JSON Web Token (JWT) itself. A JWT consists of three parts: Header, Payload, and Signature. The Payload is where the identity lives. In the context of Cognito, the payload contains standard claims defined by RFC 7519, plus custom claims.
The sub (Subject) claim is the immutable identifier for the user. The scope claim indicates the permissions granted by the specific access token. For example, a scope might be openid email profile. The groups claim, if configured in Cognito, lists the user groups they belong to, which is often used for Role-Based Access Control (RBAC).
When the Lambda function executes, it receives these claims as a simple JavaScript object. There is no need for the function to perform cryptographic verification again. The function's logic relies on the sub to fetch data from a database (e.g., DynamoDB) and the groups to determine if the user is allowed to perform the action.
For instance, a function named updateProfile might check:
exports.handler = async (event) => {
const userId = event.requestContext.authorizer.claims.sub;
const userGroups = event.requestContext.authorizer.claims['cognito:groups'] || [];
// Logic to verify if 'admin' group exists in the list
const isAdmin = userGroups.includes('admin');
if (!isAdmin && event.body.action !== 'self-update') {
return { statusCode: 403, body: JSON.stringify({ error: 'Forbidden' }) };
}
// Proceed with update logic...
};This pattern shifts the security burden from the function code to the infrastructure layer (API Gateway + Cognito). The function code is purely a consumer of verified data. If the sub claim is missing or malformed, the API Gateway authorizer would have already returned a 401 Unauthorized before the Lambda code ran.
The Cold Start Cost of Identity
A subtle but important mechanism in serverless identity is the interaction between the identity flow and the Lambda execution model. Lambda functions have a "cold start" latency where the runtime environment is initialized. If your identity logic requires fetching external resources during this initialization, you incur a penalty.
Some patterns attempt to fetch user-specific configuration or validate tokens against a custom database during the cold start. This is an anti-pattern. The API Gateway authorizer handles the token validation before the Lambda container is even spun up for the business logic. Therefore, the function should treat the identity as a given.
However, if you use a Lambda Authorizer (a custom authorizer written in code) instead of the managed Cognito Authorizer, the dynamics change. In a Lambda Authorizer, the function runs to validate the token. This means every request invokes the Lambda Authorizer function. While this invocation happens for every request, the underlying execution model reuses warm containers when available, distinguishing this from the cold start latency discussed earlier. If you are using a Lambda Authorizer to fetch user permissions from a database on every request, you are introducing significant latency and cost due to the repeated execution, not necessarily repeated cold starts. The recommended mechanism is to keep the heavy lifting (token validation) in the managed Cognito Authorizer or a lightweight Lambda Authorizer that only checks signatures, not database lookups.
Security Anti-Patterns and Best Practices
In serverless environments, the temptation is to offload security logic to the client or the function code. This creates vulnerabilities.
Anti-Pattern 1: Client-Side Validation. Relying on the frontend to check if a user is "admin" before sending a request is insecure. The API Gateway must enforce the policy. The function should assume the client is malicious.
Anti-Pattern 2: Token Leakage. Passing the JWT in the URL query parameters (e.g., ?token=eyJ...) is dangerous. Query parameters are often logged in server access logs, load balancer logs, and proxy logs. If a token is leaked here, an attacker can replay the request. Always pass tokens in the Authorization: Bearer <token> header.
Anti-Pattern 3: Over-permissive IAM Roles. The Lambda function itself runs with an IAM role. If the function needs to write to a DynamoDB table, the IAM role must have write permissions. However, the identity of the user making the request (the JWT) is separate from the identity of the function (the IAM role). A common mistake is granting the function broad permissions and relying solely on the JWT to restrict data access. While this works, it violates the principle of least privilege. Ideally, the function should use the userId from the JWT to scope its own IAM permissions if using fine-grained IAM policies (though this is complex) or strictly validate the userId in the code against the resource being accessed.
Common Pitfalls
Beyond the general anti-patterns, specific implementation errors frequently undermine serverless security.
- Confusing User Identity with Function Identity: Developers often conflate the identity of the caller (from the JWT) with the identity of the Lambda execution role (IAM). The function's IAM role determines what AWS resources it can access, while the JWT claims determine which data within those resources the user is allowed to touch. Failing to enforce this distinction can lead to data leaks where a function with broad permissions allows any authenticated user to access any record.
- Storing Tokens in Local State: Because Lambda is stateless, attempting to cache tokens or session data in local memory (variables defined outside the handler) can lead to data leakage between invocations if the container is reused incorrectly, or simply fail to persist across cold starts. Session data should be retrieved from a managed store (like Redis or DynamoDB) keyed by the
subclaim, not stored in the function's local scope. - Ignoring Token Expiration in Custom Logic: While API Gateway validates the signature, custom business logic must still respect the
exp(expiration) claim in the payload. Relying solely on the infrastructure to reject expired tokens is insufficient if the function performs long-running background tasks based on stale session data. Always checkexpagainst the current time in your handler logic.
Practical Takeaways
To build secure serverless applications, adopt these mental models and rules of thumb:
- Infrastructure Authenticates, Function Consumes: Trust the API Gateway to validate the token. Your function should treat the
requestContextclaims as immutable facts provided by a trusted source. Do not attempt to re-validate the JWT signature inside the function. - Separate User Identity from Function IAM Role: Clearly distinguish between who the user is (JWT claims) and what the function is allowed to do (IAM Role). Use the user's identity to scope data access logically, even if the IAM role provides the necessary permissions to reach the data.
- Never Pass Tokens in URL Query Parameters: Tokens are sensitive credentials. Never include them in the URL path or query string, as these are frequently logged by proxies, load balancers, and CDNs. Always use the
Authorizationheader.
FAQ
How does API Gateway handle JWT validation? API Gateway uses a Cognito Authorizer (or a custom Lambda Authorizer) to intercept the request. For Cognito, it retrieves the public keys associated with the User Pool, verifies the cryptographic signature of the incoming JWT, and checks standard claims like expiration. If validation fails, it returns a 401 response immediately, preventing the Lambda function from executing.
What is the difference between Cognito and Lambda Authorizers? A Cognito Authorizer is a managed service that validates tokens issued by a Cognito User Pool. It is generally faster and cheaper for standard authentication flows. A Lambda Authorizer is a custom function you write that can validate tokens from any provider (e.g., Auth0, Okta) or implement custom logic like checking a database for permissions. However, Lambda Authorizers incur a cold start and execution cost for every request.
Can Lambda functions store session state? No, Lambda functions are stateless by design. You cannot rely on local variables or in-memory storage to maintain session state between requests, especially after a cold start. Session state must be managed externally using services like Amazon ElastiCache (Redis), DynamoDB, or a dedicated session store, keyed by the user's unique identifier.
Conclusion
Serverless identity is a distributed trust model. The API Gateway acts as the gatekeeper, validating the cryptographic proof of identity (the JWT) and injecting the resulting claims into the Lambda event. The Lambda function then consumes these claims to enforce business logic. This separation allows the function to remain stateless and focused on data processing, while the infrastructure handles the security handshake. By understanding the flow from the Authorization header to the requestContext claims, developers can build robust, secure serverless applications that scale without compromising on identity verification.
The key takeaway is that the function does not authenticate the user; the infrastructure authenticates the user and tells the function who it is. The function's job is simply to trust that message and act accordingly.
Related posts
Decoding client_secret_basic Default in RFC 8414
An examination of the token_endpoint_auth_methods_supported metadata field in RFC 8414 and why client_secret_basic remains the default authentication method.
OpenID Connect Guide: Extending OAuth 2.0 for Identity Verification
An examination of OpenID Connect (OIDC) and how it extends OAuth 2.0 to handle identity verification using ID tokens and discovery protocols.
Angular OAuth2/OIDC: loadDiscoveryDocumentAndTryLogin
Learn how to use loadDiscoveryDocumentAndTryLogin and strict discovery document validation in Angular for secure OAuth2/OIDC authentication.