TranscendCode

I Built an EU-Compliant OAuth2 Provider for €20/Month (Auth0 Wanted €200+)

Bryce Mc Williams
#aws#oauth2#serverless#lambda#dynamodb#gdpr#compliance#cloudflare#authentication#saas

The Problem: Compliance Meets Cost Reality

I needed an OAuth2 identity provider for my SaaS projects that met EU Consumer Act and GDPR requirements. When I looked at existing solutions, the pricing shocked me: Auth0 started at €200+ per month for the features I needed, and AWS Cognito’s complexity made me nervous about properly configuring compliance settings.

As a solo developer with AWS experience and tight budget constraints, I asked myself: could I build this for less while maintaining full control over EU compliance? The answer turned out to be yes.

The result is idp8.com—a fully functional OAuth2 provider running on AWS Lambda and DynamoDB, protected by Cloudflare’s anti-bot features, handling thousands of authentication requests monthly for approximately €20/month.

This article walks through the technical architecture, compliance implementation, and cost breakdown of building your own identity provider when Auth0’s pricing doesn’t make sense.

Why Not Use Auth0 or Cognito?

Before committing to building a custom solution, I evaluated the obvious alternatives.

Auth0 Pricing Reality

Auth0’s pricing model breaks down like this:

For a bootstrapped SaaS with uncertain growth, committing to €200/month before product-market fit felt risky. The free tier limitations (no custom domains, basic auth only) made it unsuitable for production use.

AWS Cognito Considerations

Cognito has attractive pay-per-use pricing but comes with tradeoffs:

I wanted full control over how user data was handled, stored, and made available for GDPR requests.

The Build vs Buy Decision

I decided to build when I realized:

For teams without AWS experience or those expecting rapid, unpredictable scale, buying still makes more sense. For my use case, building was the right call.

EU Consumer Act and GDPR Requirements

Before writing any code, I mapped out exactly what compliance meant for an identity provider operating in the EU.

Core Requirements

Right of Access (GDPR Article 15): Users must be able to request and receive all personal data you hold about them in a structured, machine-readable format.

Right to Erasure (GDPR Article 17): Users can request deletion of their data, and you must comply within 30 days unless you have legitimate grounds to refuse.

Data Portability (GDPR Article 20): Users must be able to export their data in a common format (JSON, CSV) for transfer to another service.

Consent Management: You must track explicit consent for data processing, allow withdrawal of consent, and document the legal basis for processing.

Data Residency: For EU users, personal data should be stored and processed within EU regions to simplify compliance.

Breach Notification: You must detect, document, and notify users of data breaches within 72 hours.

Data Protection by Design: Build security and privacy into the architecture from the start, not as an afterthought.

How I Implemented Each Requirement

Data Export Endpoint: I created an API endpoint /users/{user_id}/export that returns all user data as JSON: profile information, authentication history, consent records, and session logs.

Account Deletion Workflow: The /users/{user_id}/delete endpoint marks accounts for deletion, triggers a cleanup Lambda that removes personal data from DynamoDB, anonymizes audit logs, and sends confirmation email within 24 hours.

Consent Tracking: A separate DynamoDB table stores consent records with timestamps, purpose descriptions, and consent status. Every authentication flow checks and logs current consent state.

Data Residency: All AWS resources (Lambda, DynamoDB, S3) are deployed exclusively in eu-west-1 (Ireland) and eu-central-1 (Frankfurt) regions.

Audit Logging: Every authentication event, data access, and administrative action is logged to S3 with immutable object locking enabled for compliance audits.

Encryption: DynamoDB encryption at rest is enabled by default, Lambda environment variables are encrypted with KMS, and all API traffic uses TLS 1.2+.

Technical Architecture Overview

The system uses a serverless architecture to minimize fixed costs while maintaining scalability.

High-Level Flow

[User Browser]

[Cloudflare (anti-bot, rate limiting)]

[API Gateway (HTTPS endpoints)]

[Lambda Functions (business logic)]

[DynamoDB (user data, tokens, consent)]

[S3 (audit logs, backups)]

Core Components

API Gateway: Exposes REST endpoints for OAuth2 flows: /authorize, /token, /userinfo, /revoke. Handles TLS termination, request validation, and integrates with Lambda.

Lambda Functions: Separate functions for each concern: authorization handler, token generator, user management, consent tracking, data export. Written in Node.js for fast cold starts.

DynamoDB: Three tables: Users (profile, credentials), Clients (OAuth2 client registrations), Tokens (access tokens, refresh tokens, authorization codes). On-demand billing mode for unpredictable traffic.

Cloudflare: Sits in front of API Gateway custom domain, provides bot protection, rate limiting, DDoS mitigation, and caching for static responses. Free tier covers basic needs.

AWS KMS: Generates and manages signing keys for JWT tokens, encrypts sensitive Lambda environment variables (database connection strings, API secrets).

S3: Stores compliance audit logs with lifecycle policies (retain 2 years, then archive to Glacier). Also used for user data export files.

CloudWatch: Logs all Lambda execution, API Gateway requests, and custom application metrics for monitoring authentication success rates and error patterns.

Why Serverless Works for Auth

Authentication traffic is spiky: high during business hours, low overnight. Serverless pricing aligns perfectly with this pattern:

For a service handling 50,000 requests per month, serverless costs roughly €20/month. A constantly running EC2 instance would cost €30-50/month even if idle 90% of the time.

OAuth2 Implementation Details

I implemented the most commonly used OAuth2 flows with security best practices.

Authorization Code Flow with PKCE

This is the primary flow for web and mobile applications.

Flow steps:

  1. Client initiates authorization request to /authorize with client_id, redirect_uri, scope, and PKCE parameters (code_challenge, code_challenge_method)
  2. User authenticates via idp8.com login page (protected by Cloudflare Turnstile)
  3. User grants consent for requested scopes
  4. System generates authorization code, stores in DynamoDB with 10-minute TTL
  5. Client exchanges code for tokens at /token endpoint, providing PKCE code_verifier
  6. System validates code, verifier, and issues access token (JWT) and refresh token

Why PKCE matters: PKCE (Proof Key for Code Exchange) prevents authorization code interception attacks, especially important for mobile and single-page applications. I made it mandatory for all authorization flows.

Token Structure and Security

Access Tokens (JWT):

I use JWT format for access tokens with this structure:

{
  "iss": "https://idp8.com",
  "sub": "user_123456",
  "aud": "client_abc",
  "exp": 1643723400,
  "iat": 1643719800,
  "scope": "read:profile write:data",
  "tenant_id": "tenant_xyz"
}

Signed with RS256 (RSA SHA-256) using KMS-managed private keys. Public keys are exposed at /.well-known/jwks.json for clients to verify signatures.

Token Expiration:

Refresh Token Rotation: Each time a refresh token is used to obtain a new access token, I issue a new refresh token and invalidate the old one. This limits the damage if a refresh token is compromised.

Scope Management

Scopes define what permissions a client has:

Scopes are stored in the Clients table during client registration and validated during token issuance.

Client Credentials Flow

For machine-to-machine authentication (backend services), I implemented the simpler client credentials flow:

  1. Service sends client_id and client_secret to /token endpoint
  2. System validates credentials against Clients table
  3. Issues access token with scope limited to client’s permissions
  4. No user context, no refresh tokens

This is used for automated jobs, API integrations, and service accounts.

DynamoDB Schema Design

I chose a multi-table design over single-table for clarity and simplicity at my scale.

Users Table

Primary Key: user_id (partition key)

Attributes:

GSI: email-index (for login lookups)

Clients Table

Primary Key: client_id (partition key)

Attributes:

Tokens Table

Primary Key: token_id (partition key)

Attributes:

TTL: expires_at (DynamoDB automatically deletes expired tokens)

GSI: user_id-index (for listing user’s active tokens)

Primary Key: consent_id (partition key)

Attributes:

GSI: user_id-index (for consent record queries)

Why Not Single-Table Design?

Single-table design optimizes for DynamoDB’s pricing model by reducing the number of tables. For my use case:

I prioritized clarity and compliance auditability over marginal cost savings.

Cloudflare Integration for Security

Cloudflare sits in front of the entire system and provides critical security features that would be expensive to implement in AWS.

Anti-Bot Protection

The login page at login.idp8.com uses Cloudflare Turnstile (their CAPTCHA replacement) to prevent automated credential stuffing attacks.

How it works:

  1. User visits login page
  2. Cloudflare Turnstile widget loads (invisible challenge)
  3. User enters credentials
  4. Form submission includes Turnstile token
  5. Lambda function validates token with Cloudflare API before checking password
  6. Only valid human requests proceed to authentication

This catches 99%+ of bot traffic before it reaches AWS, saving Lambda invocations and DynamoDB reads.

Rate Limiting

Cloudflare’s rate limiting rules protect critical endpoints:

These rules run at Cloudflare’s edge, blocking malicious traffic before it incurs AWS costs.

DDoS Protection

Cloudflare’s network absorbs volumetric DDoS attacks automatically. During a recent 50,000 request/second attack (likely testing), Cloudflare blocked 99.8% of traffic. My AWS bill showed no spike—normal €20 for the month.

Without Cloudflare, that attack would have cost hundreds of euros in Lambda invocations and DynamoDB writes.

Custom Domain and SSL

Cloudflare provides the custom domain (idp8.com) with free SSL certificates. This saves the cost and complexity of managing Route 53 and ACM certificates in AWS.

Setup:

  1. Domain registered with Cloudflare Registrar
  2. DNS points to API Gateway custom domain endpoint
  3. Cloudflare issues SSL certificate automatically
  4. Full (strict) SSL mode encrypts traffic Cloudflare ↔ AWS

Why Cloudflare + AWS?

Combining Cloudflare (free tier) with AWS serverless gives you:

It’s the best of both worlds: AWS’s flexible serverless compute with Cloudflare’s security and global edge network.

Security Implementation Deep Dive

Security for an OAuth2 provider must be airtight. Here’s how I locked down each layer.

Password Security

Hashing: bcrypt with cost factor 12 (adjustable as hardware improves)

Storage: Only password hashes stored in DynamoDB, never plaintext

Complexity requirements: Minimum 8 characters, must include uppercase, lowercase, number, special character (enforced at registration and password change)

Breach detection: Passwords checked against Have I Been Pwned API during registration (5 million+ compromised passwords blocked)

Token Signing with KMS

Key management: RSA 2048-bit key pair generated and stored in AWS KMS

Signing process:

  1. Lambda constructs JWT payload
  2. Calls KMS Sign API with SHA-256 hash of payload
  3. KMS signs with private key (never leaves KMS)
  4. Lambda combines payload and signature into JWT

Verification: Clients download public key from /.well-known/jwks.json and verify signature locally (no KMS call needed)

Key rotation: Automated annual rotation with overlap period where both old and new keys are valid

Secrets Management

Lambda environment variables: Encrypted with KMS customer-managed keys

Client secrets: Hashed with bcrypt before storing in Clients table (same as passwords)

API keys: Stored in AWS Secrets Manager with automatic rotation every 90 days

No hardcoded secrets: All configuration loaded from environment variables or Secrets Manager at runtime

Input Validation

Every API endpoint validates input rigorously:

Rate Limiting Layers

Cloudflare: First line of defense, blocks at edge

API Gateway: 10,000 requests per second burst limit, 5,000 steady-state per account

Application logic: Custom rate limiting in Lambda tracks failed login attempts per user/IP and implements exponential backoff

Failed login attempts:

Audit Logging

Every security-relevant event is logged to S3:

Logs are immutable (S3 Object Lock enabled), retained for 2 years, then archived to Glacier for 5 more years.

Vulnerability Scanning

Dependency scanning: npm audit runs in CI/CD, blocks deployment if high/critical vulnerabilities found

Lambda runtime updates: Automated monthly updates to latest Node.js LTS runtime

Penetration testing: Annual third-party pen test (required for some client contracts)

Cost Breakdown: €20/Month

Here’s the actual monthly cost breakdown from my AWS bill:

AWS Lambda:

DynamoDB:

API Gateway:

S3:

AWS KMS:

CloudWatch:

Route 53:

Cloudflare:

Total Monthly Cost: €18.78

Rounded to €20/month for variability.

Cost Comparison vs Auth0

For equivalent features:

FeatureAuth0 Professionalidp8.com (AWS)
Basic OAuth2€200/month€20/month
10,000 MAU€200/month€20/month
Custom domainIncluded€0.50/month (Route 53)
Bot protectionAdd-on €50/monthFree (Cloudflare)
Audit logsIncluded€0.51/month (S3)
Total€250/month€20/month

Savings: €230/month or €2,760/year

At 100,000 monthly active users, Auth0 pricing jumps to €500+/month. My AWS costs would increase to approximately €40/month (still 12x cheaper).

When Does Auth0 Make Sense?

Auth0 becomes competitive when:

For bootstrapped founders with technical chops, building is clearly cheaper.

Challenges and Lessons Learned

Building an OAuth2 provider taught me plenty through trial and error.

Challenge 1: OAuth2 Edge Cases

Problem: The OAuth2 spec has many edge cases not obvious from reading the RFC.

Examples:

Solution: I studied Auth0 and Okta’s documentation extensively, read OAuth2 security best practices (RFC 8252, RFC 8628), and implemented the strictest reasonable interpretation of each requirement.

Challenge 2: DynamoDB Query Patterns

Problem: DynamoDB’s query limitations bit me multiple times.

Example: I needed to look up users by email (not the primary key). My first attempt did a full table scan on every login—absurdly slow and expensive.

Solution: Added a Global Secondary Index on email. Lesson learned: design GSIs upfront based on access patterns, not as an afterthought.

Challenge 3: Lambda Cold Starts

Problem: First request after idle period took 800ms (unacceptable for login flows).

Solutions:

Trade-off: Provisioned concurrency adds cost but improves user experience. I enable it 9am-6pm CET, disable overnight.

Challenge 4: Testing OAuth2 Flows Locally

Problem: OAuth2 requires HTTPS, redirect URIs, and proper domain resolution—hard to replicate locally.

Solution:

Challenge 5: Compliance Documentation

Problem: EU compliance isn’t just technical—it requires documentation, privacy policies, terms of service, and data processing agreements.

Solution:

Lesson: Technical implementation is 60% of compliance. Documentation and legal review are the other 40%.

Challenge 6: Multi-Tenancy Isolation

Problem: I wanted multiple SaaS projects to use idp8.com, but isolated from each other.

Solution:

This was more complex than single-tenant but essential for reusability across projects.

When to Build vs Buy

After building and operating idp8.com for several months, I have clear opinions on when building makes sense.

Build If:

Buy (Auth0/Okta/etc) If:

For solo developers and small teams with technical skills, building is almost always more cost-effective. For larger teams or non-technical founders, buying makes sense.

Monitoring and Observability

You can’t manage what you don’t measure. Here’s how I monitor idp8.com in production.

Key Metrics

Authentication success rate: % of /token requests that succeed (target: >99.5%)

Latency percentiles: p50, p95, p99 for each endpoint (target: <200ms p95)

Error rates: 4xx and 5xx errors per endpoint (target: <0.1% 5xx)

Token issuance volume: Requests/minute to detect unusual spikes

Failed login attempts: Track brute force attempts per IP/user

DynamoDB throttling: Read/write capacity exceeded events (should be zero on on-demand)

Monitoring Stack

CloudWatch Dashboards: Real-time graphs of Lambda invocations, DynamoDB operations, API Gateway requests

CloudWatch Alarms: Alert via SNS (email/SMS) when:

CloudWatch Logs Insights: Query Lambda logs for debugging, filter by user_id or client_id to trace issues

X-Ray: Distributed tracing for complex requests that touch multiple Lambda functions and DynamoDB tables

Incident Response

When an alarm fires:

  1. CloudWatch SNS sends alert to my phone
  2. I open CloudWatch dashboard to see affected metrics
  3. Check Logs Insights for error messages and stack traces
  4. Reproduce issue locally if possible, or in staging environment
  5. Deploy hotfix via automated CI/CD pipeline
  6. Document incident in runbook for future reference

Average time to resolve P1 incidents: 15 minutes.

Future Improvements

idp8.com meets my current needs, but there’s always room to expand.

Planned Features

Social login: Add “Sign in with Google” and “Sign in with GitHub” for better UX

Passwordless authentication: Implement magic link and WebAuthn/passkey support

Multi-factor authentication: SMS, TOTP, and hardware key support

User management UI: Web console for admins to manage users, clients, and view audit logs

Terraform configuration: Replace manual AWS console setup with infrastructure-as-code

Automated penetration testing: Integrate security scanning into CI/CD pipeline

Scaling Considerations

Current architecture handles ~1 million requests/month comfortably. To scale to 10 million+:

At 100 million requests/month, costs would rise to approximately €150/month—still 50% cheaper than Auth0’s enterprise tier.

Conclusion

Building idp8.com proved that you don’t need a huge budget or enterprise SaaS to run a production-grade OAuth2 provider that meets EU compliance requirements. With serverless AWS architecture and Cloudflare’s free security features, I achieved:

The key lessons:

For solo developers and bootstrapped startups, this pattern is repeatable: use AWS serverless for compute/storage, Cloudflare for security/edge, and invest time in understanding the domain (OAuth2, compliance) rather than paying for a managed service.

If you’re building a SaaS and need authentication, consider whether Auth0’s convenience is worth 10-12x the cost. For many technical founders, the answer is no—and building might be the better path.

Now go build your own OAuth2 provider (or adapt this pattern for your authentication needs). Your wallet and compliance auditors will thank you.

← Back to Blog