One of the trickier parts of this site is protecting the photo gallery. The site is fully static — there’s no server to check credentials against. So the authentication has to happen at the CDN level.

The Setup

The solution uses three AWS services working together:

  1. Cognito — manages the user pool and handles Google OAuth
  2. CloudFront — serves the site and routes requests
  3. Lambda@Edge — runs on every request to protected paths and checks for a valid JWT

How It Works

When someone requests a gallery image, the request hits CloudFront first. Before CloudFront fetches the image from S3, it triggers a Lambda@Edge function on the viewer-request event:

exports.handler = async function(event) {
    const request = event.Records[0].cf.request;
    const cookies = parseCookies(request.headers);
    const token = cookies['id_token'];

    if (!token) {
        return redirectToLogin(request);
    }

    try {
        await verifyToken(token);
        return request; // allow through
    } catch (err) {
        return redirectToLogin(request);
    }
};

The function checks for an id_token cookie, validates it against Cognito’s JWKS endpoint, and either lets the request through or redirects to the login page.

The Cost Question

Lambda@Edge charges per request. For a personal site with a handful of users, it’s essentially free. But if bots start hammering your endpoints, it could add up. I keep a budget alarm set at $5/month and have a documented procedure for pulling the Lambda@Edge association if things go sideways.

Lessons Learned

  • Lambda@Edge functions must be deployed to us-east-1, regardless of where your other infrastructure lives
  • You need to publish a numbered version — $LATEST won’t work
  • Propagation to all edge locations takes 5-15 minutes after any change