Day 13: Adding authentication - API keys done simply

Day 13 of 30. Today users can sign up, log in, and get API keys.

#oauth #security

Day 13 of 30. Today users can sign up, log in, and get API keys.

Our landing page has a “Get Started” button. It currently goes to a 404. Oops :)

Let’s fix that!

Authentication strategy

We need two kinds of auth:

1. Dashboard auth. Users can log in to see their usage, manage keys, and update billing. This will be session-based auth with social login providers like Google and GitHub.

2. API auth. Developers authenticate API requests with API keys. The calls are stateless, key-based, and suitable for server-to-server calls, or any form of automation.

Since these are both different problems, they require different solutions.

See the auth flows

Explore how OAuth login and API key validation work:

auth-flow - Authentication flows
👤
User
🌐
Allscreenshots
🔒
Google/GitHub
1
Click "Sign in"
User clicks login button
2
Redirect to OAuth
Redirect to Google/GitHub
3
Authenticate
User grants permission
4
Auth code
Callback with code
5
Exchange token
Get access token
6
Session created
User logged in!
🛡 Security Features
[✓] Keys stored as SHA-256 hashes
[✓] HTTP-only session cookies
[✓] Token bucket rate limiting
[✓] No password storage

Dashboard: social login with OAuth providers

We’re keeping it simple. No don’t have a username/password login at this moment, we use OAuth only (“Sign in with Google” or “Sign in with GitHub”).

Why do we currently have no username/password?

  • This adds a dependency on external email providers for verification
  • Password reset flows are complex to build securely
  • We’d need to store and encrypt passwords properly
  • Our users are mostly developers - you likely have a GitHub or Google account already

If users really need username/password, or other forms of authentication, please let us know, we’ll add it later. For now, we have a support link for edge cases, so please contact us if the above options are not sufficient.

The user model

Our user entity stores OAuth provider information instead of passwords:

User:
    id: UUID (primary key)
    email: string (unique)
    name: string (optional)
    oauthProvider: enum (GOOGLE, GITHUB)
    oauthProviderId: string (the user's ID from the provider)
    plan: enum (FREE, PRO, SCALE), default FREE
    createdAt: timestamp
    lastLoginAt: timestamp (optional)

OAuth flow

The OAuth process works like this:

When user clicks "Sign in with Google":
    1. Redirect to Google's OAuth authorization URL
       - Include our client ID, redirect URI, and requested scopes (email, profile)

    2. User authenticates with Google and grants permission

    3. Google redirects back to our callback URL with an authorization code

    4. We exchange the code for access tokens (server-side)

    5. We fetch the user's profile from Google using the access token

    6. Look up user by oauthProvider + oauthProviderId:
       - If exists: update lastLoginAt, create session
       - If not exists: create new user, create session

    7. Redirect to dashboard

Spring Security’s OAuth2 client handles most of this automatically, so we can focus on configuring the providers and implement the user registration logic.

Session management

We use HTTP-only cookies with session IDs. This ensures that JavaScript cannot read (and thus steal) the information. The security configuration:

Security rules:
    - API routes (/api/v1/**): permit all (uses API key auth)
    - Auth routes (/auth/**, /oauth2/**): permit all
    - Public pages (/, /signup, /login): permit all
    - Dashboard routes (/dashboard/**): require authenticated session

Session settings:
    - Create session only when needed
    - Cookie: HTTP-only, Secure, SameSite=Lax

API keys: the real auth

For Screenshot API requests, we use API keys. They’re simpler than JWT and easier for developers to use.

API key format

Our keys look like:

sk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6

Components:

  • sk_ - secret key (vs. pk_ for publishable keys)
  • live_ - environment (vs. test_ for sandbox)
  • 32 random alphanumeric characters

This format is inspired by Stripe. It’s self-documenting: you can tell what a key is just by looking at it.

Storing keys securely

We never store raw API keys, only the hashes, which we again do for security reasons.

Our API key looks somewhat like this:

ApiKey:
    id: UUID
    userId: UUID (foreign key)
    keyPrefix: string ("sk_live_a1b2c3d4")
    keyHash: string (SHA-256 hash)
    name: string ("Production Key")
    createdAt: timestamp
    lastUsedAt: timestamp
    revokedAt: timestamp (null = active)

When a user creates a key:

  1. Generate 24 random bytes using secure random
  2. Base64 encode to get 32 alphanumeric chars
  3. Prepend “sk_live_” to create full key
  4. Compute SHA-256 hash
  5. Store prefix and hash only
  6. Return full key to user once
  7. Warn them to save it. They won’t see the key again, but creating a new one is easy.

Validating API keys

validateApiKey(rawKey):
    1. Compute SHA-256 hash of the provided key
    2. Look up by hash where revokedAt is null
    3. If not found: return null (invalid)
    4. Update lastUsedAt timestamp
    5. Return the ApiKey record

API key authentication filter

When a user makes a request, we inspect the API requests and validate the key:

For each request to /api/v1/**:
    1. Extract "X-API-Key" header

    2. If missing:
       Return 401 with {"error": {"code": "unauthorized", "message": "Missing API key"}}

    3. Validate the key

    4. If invalid:
       Return 401 with {"error": {"code": "unauthorized", "message": "Invalid API key"}}

    5. Attach apiKey and userId to request context

    6. Continue to actual endpoint

Rate limiting

We’re using a simple token bucket rate limiter (note: below numbers can vary, these are just for illustrative purposes):

PlanRate LimitRefill Rate
Free100 tokens/hour~1.7/minute
Pro1,000 tokens/hour~17/minute
Scale10,000 tokens/hour~167/minute
Bucket:
    tokens: number (current tokens available)
    lastRefill: timestamp

checkRateLimit(key, maxTokens, refillRate):
    1. Get or create bucket for this key
    2. Calculate tokens to add since lastRefill (elapsed time × refillRate)
    3. Add tokens, but cap at maxTokens
    4. Update lastRefill to now
    5. If tokens < 1: return false (rate limited)
    6. Subtract 1 token
    7. Return true (allowed)

The token bucket approach is nice because it allows short bursts while enforcing overall limits. A user can spend their tokens quickly, then wait for refill.

We’ll move to Redis-based rate limiting when we need distributed rate limits across multiple servers. For now, single-server, in-memory is fine.

The signup flow

  1. User clicks “Sign in with Google” (or GitHub)
  2. OAuth flow happens (see above)
  3. On the first login, we create their account
  4. Automatically create their first API key so it can be used in the playground and API
  5. Redirect to dashboard showing the key (with “copy” button and warning to save it)
onOAuthSuccess(oauthUser):
    1. Find or create user from OAuth profile

    2. If new user:
       - Create "Default Key" API key
       - Store raw key in session temporarily for display

    3. Create authenticated session

    4. Redirect to dashboard
       - If new user: show welcome modal with API key
       - If existing user: show normal dashboard

What we built today

  • OAuth login with Google and GitHub
  • Session-based dashboard auth
  • API key generation and secure storage
  • API key validation
  • Basic rate limiting

Users can now sign up and get API keys! Try it out here: https://dashboard.allscreenshots.com

Tomorrow: the dashboard

On day 14 we’ll extend the dashboard where users manage keys and see their API usage.

Book of the day

OAuth 2 in Action by Justin Richer and Antonio Sanso

While a bit of an older book, this book passed the test of time well. If you’re implementing OAuth (like we did today), this book explains the protocol thoroughly. It covers the authorization code flow, token handling, and common security pitfalls.

The first few chapters give you a solid mental model of how OAuth works and why it’s designed the way it is. The later chapters dive into building both clients and servers. Even if you’re using a framework that handles most of it (like Spring Security), understanding the underlying flow helps you debug issues and make better security decisions.


Day 13 stats

Hours
█████░░░░░░░░░░
35h
</> Code
██████░░░░░░░░░
2,000
$ Revenue
░░░░░░░░░░░░░░░
$0
Customers
░░░░░░░░░░░░░░░
0
Hosting
████░░░░░░░░░░░
$5.5/mo
Achievements:
[✓] OAuth login working [✓] API keys generated [✓] Rate limiting added
╔════════════════════════════════════════════════════════════╗
E

Erik

Building Allscreenshots. Writes code, takes screenshots, goes diving.

Try allscreenshots

Screenshot API for the modern web. Capture any URL with a simple API call.

Get started