cloud / serverless architecture

AWS Serverless Authenticated API

A fully serverless, secure backend built on AWS — Cognito for authentication, API Gateway for routing and authorization, Lambda for compute, and DynamoDB for persistent storage — reflecting real-world microservice patterns used in enterprise environments.

The problem

Most tutorials on serverless APIs skip the part that actually matters in production: authentication. It's easy to wire up API Gateway to a Lambda function that reads and writes to DynamoDB — it's harder to make sure only legitimate, signed-in users can call it, and that the API is validating real credentials rather than trusting whatever the client claims about itself.

The goal of this project was to build that missing piece properly: a backend where every request is authenticated with a real identity provider, every token is cryptographically validated before a single line of business logic runs, and the compute layer never has more database access than it strictly needs.

Overview

The API allows authenticated users to create and retrieve items from a DynamoDB table. Authentication and authorization are handled through Amazon Cognito's Hosted UI, which issues JWT access tokens. These tokens are validated at API Gateway through a Cognito Authorizer before any Lambda function is invoked.

Key AWS services

Amazon Cognito (User Pool, Hosted UI, OAuth2) API Gateway (REST + JWT Authorizer) AWS Lambda (Python) Amazon DynamoDB IAM least-privilege roles CloudWatch Logs

Architecture diagram

👤
User
→
C
Amazon Cognito
→
A
API Gateway
→
λ
AWS Lambda
→
D
Amazon DynamoDB

Architecture flow

  1. User signs in through the Cognito Hosted UI.
  2. Cognito returns a JWT access_token to the user.
  3. User calls the API using: Authorization: <access_token>
  4. API Gateway validates the JWT using the Cognito Authorizer.
  5. Lambda executes and processes the request.
  6. DynamoDB stores or retrieves data.
  7. Response is returned to the client.

Steps taken

  1. Configured an Amazon Cognito User Pool and Hosted UI, setting up the OAuth2 client and working through Cognito's app client settings to get a working login flow issuing real JWT tokens.
  2. Built the REST API in API Gateway and attached a Cognito Authorizer directly to the routes, so token validation happens at the gateway layer — before Lambda ever executes — rather than trusting each function to check auth itself.
  3. Wrote the Lambda function in Python to handle both GET and POST against DynamoDB, including proper JSON serialization for DynamoDB's native Decimal types, a detail that silently breaks naive implementations.
  4. Diagnosed and fixed a Hosted UI misconfiguration where the login flow kept returning an authorization code instead of the expected token, by enabling Implicit Grant and setting a default redirect URL.
  5. Diagnosed a DynamoDB AccessDeniedException back to an overly narrow default Lambda execution role, and resolved it by writing a scoped, least-privilege IAM inline policy rather than reaching for a broad managed policy.
  6. Traced an authentication failure to using the wrong Cognito-issued token type (id_token instead of access_token) against API Gateway — an easy, common mistake — and corrected the client call.

Security posture

Authentication happens before authorization, and authorization happens before any application code runs: API Gateway validates the JWT's signature and claims via the Cognito Authorizer, and a request with an invalid or missing token never reaches Lambda at all. The Lambda execution role is scoped to only the specific DynamoDB actions the function needs — PutItem, Scan, GetItem — not blanket table access, following the same least-privilege principle regardless of which layer is doing the requesting.

API endpoints

GET /items Returns all items from the DynamoDB table.
POST /items Creates a new item with a generated UUID, name, and timestamp.

Lambda function (Python)

Processes both GET and POST requests:

import json
import boto3
import uuid
import decimal

dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table("YOUR_TABLE_NAME")

class DecimalEncoder(json.JSONEncoder):
    def default(self, o):
        if isinstance(o, decimal.Decimal):
            return float(o)
        return super().default(o)

def lambda_handler(event, context):
    method = event.get("httpMethod", "")

    if method == "GET":
        result = table.scan()
        return {
            "statusCode": 200,
            "headers": {"Content-Type": "application/json"},
            "body": json.dumps(result["Items"], cls=DecimalEncoder)
        }

    if method == "POST":
        body = json.loads(event["body"])
        item = {
            "id": str(uuid.uuid4()),
            "name": body.get("name", "Unnamed"),
            "timestamp": body.get("timestamp", "N/A")
        }
        table.put_item(Item=item)
        return {
            "statusCode": 200,
            "headers": {"Content-Type": "application/json"},
            "body": json.dumps({"message": "Item stored", "item": item})
        }

    return {"statusCode": 405, "body": "Method Not Allowed"}

DynamoDB table

  • Primary key: id (string)
  • Additional attributes stored: name, timestamp
  • Schema-less and scales automatically

Cognito authentication

  • Hosted UI handles user login
  • OAuth2 flows: Implicit Grant + Auth Code
  • API Gateway requires a valid JWT access token
  • Unauthorized requests return 401 Unauthorized

Notable issues along the way

hosted UI always returned response_type=code Fixed by enabling Implicit Grant and setting a Default Redirect URL.
DynamoDB AccessDeniedException Lambda's execution role was missing required permissions. Resolved by adding a least-privilege IAM inline policy granting dynamodb:PutItem, dynamodb:Scan, and dynamodb:GetItem.
using the wrong token type API Gateway requires the access_token, not the id_token — an easy mix-up since Cognito returns both.

Skills demonstrated

Amazon CognitoAPI Gateway REST APIAWS Lambda (Python) Amazon DynamoDBIAM PermissionsJWT / OAuth2CloudWatch Logs

What this project demonstrates

  • Serverless microservice design
  • Authentication & authorization best practices
  • Building secure REST APIs
  • Event-driven Lambda patterns
  • DynamoDB NoSQL data modeling
  • IAM least-privilege role construction
  • End-to-end cloud application delivery

Outcome

The result is a working, end-to-end authenticated API: a user can sign in through Cognito, receive a real JWT, and use it to create and retrieve records in DynamoDB — with every request validated at the gateway layer and every layer of the system holding only the permissions it actually needs. What started as a way to fill in the authentication gap most serverless tutorials skip became a complete, credible reference implementation of the pattern.

Previous
Previous

MCP Server project for Team-Wide AI Data Access

Next
Next

Multi-Region AWS Architecture Mapping & Systems Audit