We're Hiring. Check Out Our Open Positions.

January 27, 2025

Building Unified Authentication at Anon: A Tale of Provider-Agnostic Session Management

At Anon, we enable developers to create user-authenticated integrations for websites without APIs. This poses a unique challenge: how do we build a unified authentication interface that can handle diverse provider-specific flows while maintaining secure session persistence across our distributed system? Here's our journey, including the challenges we faced and the trade-offs we made.

The Challenge

When supporting automated interactions with third-party services, we encounter a wide spectrum of authentication mechanisms:

  • Traditional username/password flows
  • OAuth-based authentication with varying implementations
  • Multi-factor authentication (MFA) with provider-specific requirements
  • Anti-automation measures like CAPTCHAs and rate limits
  • Provider-specific session management quirks
  • Varying token expiration and refresh mechanisms

Each provider not only implements these mechanisms differently but also frequently updates their security measures, making it challenging to maintain a unified interface while preserving provider-specific security features.

Our Solution: Auth Protocol Abstraction

We built an abstraction layer that decouples the authentication flow from provider implementations while preserving provider-specific security features. The core interface focuses on session lifecycle management:

1interface AuthSession {
2  // Unique session identifier with rotation support
3  sessionId: string;
4  
5  // Current authentication state including MFA status
6  authState: AuthState;
7  
8  // Provider-specific security features we can't abstract
9  providerMetadata: Record<string, unknown>;
10  
11  // Session management with provider-specific refresh logic
12  refresh(options: RefreshOptions): Promise<Result<void, AuthError>>;
13  
14  // Validation with security check support
15  validate(context: ValidationContext): Promise<Result<boolean, AuthError>>;
16}

Secure Session Distribution

Our initial implementation used a centralized cache for session management, similar to Netflix's Edge Authentication Services. However, we quickly discovered challenges with global deployments and provider rate limits. Our revised approach uses a hierarchical session management system:

1class HierarchicalSessionManager {
2  async persistSession(
3    session: AuthSession,
4    context: SecurityContext
5  ): Promise<Result<string, StorageError>> {
6    // Generate rotation-enabled session token
7    const token = await this.tokenManager.create({
8      rotationPolicy: this.getRotationPolicy(context.provider),
9      metadata: context.securityMetadata
10    });
11
12    // Encrypt with region-specific keys
13    const encrypted = await this.crypto.encrypt({
14      data: session.serialize(),
15      keyring: this.getRegionalKeyring(context.region)
16    });
17
18    // Store with provider-specific constraints
19    return await this.distributedStore.set({
20      key: `session:${token}`,
21      value: encrypted,
22      constraints: this.getProviderConstraints(context)
23    });
24  }
25}

Key improvements over our initial design:

[list-check]

  • Region-aware session storage
  • Provider-specific token rotation policies
  • Granular encryption key management
  • Session replay attack prevention

Rate Limiting and Circuit Breaking

Our original circuit breaker implementation was too simplistic. The revised version handles distributed rate limiting and provider-specific behaviors:

1class ProviderAwareCircuitBreaker {
2  constructor(
3    private readonly config: CircuitBreakerConfig,
4    private readonly metrics: MetricsClient,
5    private readonly state: DistributedStateManager
6  ) {}
7
8  async execute<T>(
9    operation: () => Promise<T>,
10    context: ProviderContext
11  ): Promise<Result<T, CircuitError>> {
12    const limits = await this.state.getProviderLimits(context);
13    
14    if (await this.shouldTrip(context, limits)) {
15      this.metrics.incrementCounter('circuit_breaker.trip', {
16        provider: context.provider,
17        reason: this.getTripReason(context)
18      });
19      
20      return Err(new CircuitOpen({
21        retryAfter: this.calculateBackoff(context)
22      }));
23    }
24
25    try {
26      const result = await operation();
27      await this.recordSuccess(context);
28      return Ok(result);
29    } catch (error) {
30      await this.recordFailure(context, error);
31      return Err(this.mapError(error));
32    }
33  }
34}

Real-World Challenges

Several challenges emerged as we scaled:

Provider-Specific Features

Not everything fits neatly into our abstraction. For example, some providers require:

  • Custom CAPTCHA handling
  • Specific cookie management
  • IP-based authentication rules

We solve this through extensible provider metadata and delegated security handlers, though this increases complexity.

Monitoring and Debugging

Understanding authentication failures across providers requires extensive instrumentation:

1interface AuthMetrics {
2  // Granular failure tracking
3  recordFailure(context: AuthContext, error: AuthError): void;
4  
5  // Session lifecycle events
6  recordSessionEvent(event: SessionEvent): void;
7  
8  // Provider-specific latency tracking
9  recordProviderLatency(context: ProviderContext, duration: number): void;
10}

Security Updates

Providers frequently update their security measures. Our abstraction must balance stability with security:

  • Version provider adapters independently
  • Support rapid security patches
  • Maintain backward compatibility

Limitations and Trade-offs

Our approach has several known limitations:

1. Abstraction Cost

  • Additional latency from our abstraction layer
  • Increased complexity in debugging
  • Resource overhead from session management

2. Provider Limitations

  • Can't abstract away all provider-specific behaviors
  • Must maintain provider-specific code
  • Security features may lag behind provider updates

3. Operational Complexity

  • Managing distributed session state
  • Handling cross-region rate limits
  • Monitoring provider-specific issues

Looking Forward

We continue to evolve this system based on real-world usage:

  • Improving observability for faster debugging
  • Refining provider-specific optimizations
  • Enhancing security measure adoption

The challenge of unified authentication remains complex, but our approach provides a maintainable foundation while respecting provider-specific security requirements. We're sharing our experiences to contribute to the broader discussion of building maintainable, secure authentication systems at scale.

Future work includes

[list-todo]

  • Enhanced MFA abstraction
  • Improved security telemetry
  • More granular rate limiting controls
  • Better support for regional authentication requirements

[cta]

Note: This post describes our current approach to authentication management. Security requirements evolve rapidly, and any authentication system should be regularly reviewed and updated.

Resilient Browser Automation at Scale: How Anon Solves the Anti-Bot Challenge

Secure Credential Management at Scale: Anon's Zero-Persistence Architecture

Distributed Rate Limiting at the Edge: How Anon Coordinates Global Request Quotas

Stateful Action Replay: Building Robust User Workflow Recording at Anon

Building Reliable Browser Automation Pipelines at Scale

Dynamic Protocol Adaptation: Building a Universal Authentication Layer at Anon

Building Unified Authentication at Anon: A Tale of Provider-Agnostic Session Management

Cross-Site Schema Federation: Building a Unified API Interface Across Diverse Web Platforms