Funky Tokens

Funky Tokens
Austin Texas

Recently, I had the opportunity to examine the internals of a production authentication system built on Auth0 and OAuth2. As someone that learned quite a bit about security systems throughout this, I thought it might make sense to explain how an example production system works.

This exploration provided some insights into how modern web applications handle user registration, email verification, and secure token management.

The Architecture of Modern Authentication

Session Management and State Preservation

Modern web applications rely heavily on session management to maintain user state across multiple requests. Typically, these are stored as session headers in api requests. The system I examined used standard HTTP session cookies combined with OAuth2's state parameter for CSRF protection:

session.headers.update({
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)...',
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
})

Browser-like session management

The state parameter serves as a critical security mechanism, threading through the entire authentication flow to prevent cross-site request forgery attacks. This is valuable because once an exploit can automate a browser of a exposed client, it becomes necessary to kill it fast, before escalation can happen.

OAuth2 Authorization Code Flow in Practice

This particular system implemented a standard OAuth2 authorization code flow with an interesting twist for email verification:

Application → Identity Provider → Application (with code) → Token Exchange

What made this unique to me was how it handled unverified users. Instead of completing the flow immediately, it would pause for email verification. This meant two things. An SMTP server had to be registered, and we had to setup a corresponding email automatically. By minting MX records on the fly, we can programmatically capture inbound requests, and store everything for future use.

Multi-Step Registration Process

The registration process was split into discrete steps, each with its own validation:

  1. Email Collection: First endpoint accepts email and CAPTCHA token
  2. Password Submission: Second endpoint accepts password with complexity requirements
  3. Verification Trigger: Deliberately not calling the resume endpoint triggers email delivery

This separation allows for progressive validation and better user experience, as errors can be caught and displayed at each step. Breaking it down this way also in general helps me, as the simpler the problem, the easier it tends to be to solve in isolation.

Email Verification Pattern

One of the other things I was also very curious about was the email verification trigger mechanism:

if 'state=' in location:
    resume_state = params.get('state', [None])[0]
    # Not calling /authorize/resume here triggers the email

Getting the resume state but not calling it triggers verification

This pattern allows the application to control when verification emails are sent, rather than automatically sending them on every registration attempt. This way, they can prevent a DDOS like attack on the email servers, and keep authentication email numbers low to protect domain authority.

A second request to authorize on it's own is all that's required to continue with the authentication flow here.

Token Lifecycle Management

Once in, the system demonstrated a sophisticated approach to token management:

# Initial token (unverified status)
token_response = session.get('/api/token')

# Refresh to get updated token with verified status
refresh_response = session.get('/api/refresh_token')

Token validation via refresh API

This two-step process ensures that tokens accurately reflect the user's current verification status, preventing access to protected resources before email confirmation. Tokens need to be refreshed, allowing total server side state control, and ease of blacklisting.

In the future, you may need to add fingerprint spoofing, via hardening protocols or otherwise, and rotating residential proxies to prevent flagging duplicate data mined from the browser.

Technical Implementation Details

CAPTCHA Integration

The final unique defense system was Cloudflare's Turnstile CAPTCHA as a bot prevention mechanism. The CAPTCHA token was required during registration and validated server-side:

data = {
    'state': state_parameter,
    'email': user_email,
    'captcha': captcha_token,
    'action': 'default'
}

Turnstile Captcha validation payload

Prefabricated questions with high difficulty of automation, and pausing / resuming make it require a very large multimodal model operating under a rotating residential proxy for programmatic access. After this though, the token is able to mint with permissions, and the auth is a fully documented procedure.

State Machine Design

As an overview, at a high level the authentication flow follows a strict state machine pattern:

  1. Initial State: Unauthenticated user
  2. Registration Started: State parameter generated
  3. Account Created: User exists but unverified
  4. Email Verified: Verification completed
  5. Authenticated: Token issued with full permissions

Each transition require specific conditions to be met, ensuring security at every step. However, by maintaining cookies, utilizing a custom SMTP server capable of creating new emails on the fly, and replicating this flow with the security measures enabled, and ML systems for the captchas, it becomes possible to replicate the browser state machine.

API Authorization Pattern

Once authenticated, all API calls used standard Bearer token authorization:

headers = {
    "Authorization": f"Bearer {access_token}",
    "Content-Type": "application/json"
}

Bearer auth protocol

The JWT token here works like a typical auth flow. It is encoded at rest, and contains claims about the user's verification status, role, and permissions, allowing for fine-grained access control.

Security Considerations

On the other side, if you are building an authentication system, it makes sense to consider some of these practices. While it is impossible to prevent automation, you can certainly de risk exploits.

CSRF Protection

The consistent use of the state parameter throughout the flow provides robust CSRF protection. Any request without the correct state is rejected, preventing malicious sites from initiating actions on behalf of users.

Email Verification Importance

The system's approach to email verification, checking the email_verified claim in JWTs ensures that unverified accounts cannot access protected resources. This pattern prevents abuse while allowing users to start the registration process easily.

Token Refresh Strategy

Finally, the token refresh mechanism serves multiple purposes:

  • Updates verification status without requiring re-authentication
  • Allows for token rotation for enhanced security
  • Provides a way to revoke access by refusing refresh requests

Lessons Learned

This exploration revealed several best practices in modern authentication:

  1. Separation of Concerns: Breaking registration into multiple steps improves error handling and user experience
  2. State Parameter Usage: Consistent state parameter validation prevents CSRF attacks
  3. Progressive Enhancement: Users can begin registration easily, with verification required only for protected resources
  4. Token Claims: Embedding verification status in tokens allows for distributed authorization decisions
  5. Explicit Verification: Requiring explicit email verification prevents automated account creation abuse

Complete Flow

As some people are visual learners, I thought it might make sense to showcase how the network sequences work here.

Conclusion

Modern authentication systems are pretty complex. But OAuth2, when properly implemented with email verification and CAPTCHA protection, creates a robust and user-friendly authentication system. And abstracting that helps create the simpler experience we love in consumer systems.

Understanding these patterns is crucial for developers building secure web applications. The techniques with state parameter threading and progressive verification represent the basic standard for web application security, and I typically try to keep them in mind while building.

The key takeaway is that when automating, attention to detail is extremely important. By carefully designing the authentication flow, applications can provide both strong security guarantees and a smooth user journey, but if it was created by someone, it can be reverse engineered by someone else.

Happy Hacking!