From Legacy Auth to Modern OIDC Link to heading

In December 2022, I volunteered to help with an identity migration project. What seemed like a straightforward system upgrade became a deep exploration into modern authentication patterns, OIDC compliance, and enterprise identity management.

The Legacy Authentication Challenge Link to heading

Many enterprises face similar authentication challenges:

  • Custom-built authentication systems developed years ago
  • Non-standard token implementations
  • Lack of OIDC compliance for modern integrations
  • Inconsistent validation logic across services
  • Difficulty integrating with third-party providers
  • Technical debt from years of incremental changes

These legacy systems present common problems:

  • Security concerns: Non-standard implementations are harder to audit
  • Integration challenges: Modern services expect OIDC compliance
  • Maintenance overhead: Custom systems require constant updates
  • User experience: Inconsistent authentication flows
  • Compliance requirements: Industry prefers standard protocols

Understanding Migration Complexity Link to heading

Moving to a modern OIDC-compliant solution involves significant complexity:

  • Multiple token issuers: Systems must handle both legacy and modern tokens during transition
  • Zero downtime requirement: Users expect seamless authentication
  • Cross-service compatibility: Every service needs updated validation logic
  • Gradual rollout: Migrations happen incrementally over months

The Technical Challenge Link to heading

The core challenge was ensuring seamless token validation across systems:

// token-validator.ts
import { JwtPayload, verify } from "jsonwebtoken"
import { Request } from "express"

interface TokenValidationResult {
  valid: boolean
  payload?: JwtPayload
  issuer?: "legacy" | "ping"
  error?: string
}

class DualTokenValidator {
  private legacyPublicKey: string
  private pingJwksUri: string
  private pingJwksCache: Map<string, string> = new Map()

  constructor(legacyKey: string, pingJwksUri: string) {
    this.legacyPublicKey = legacyKey
    this.pingJwksUri = pingJwksUri
  }

  async validateToken(token: string): Promise<TokenValidationResult> {
    // Try legacy validation first (faster, no network call)
    const legacyResult = this.validateLegacyToken(token)
    if (legacyResult.valid) {
      return { ...legacyResult, issuer: "legacy" }
    }

    // Try Ping validation
    const pingResult = await this.validatePingToken(token)
    if (pingResult.valid) {
      return { ...pingResult, issuer: "ping" }
    }

    return {
      valid: false,
      error: "Token validation failed for both legacy and Ping issuers",
    }
  }

  private validateLegacyToken(token: string): TokenValidationResult {
    try {
      const payload = verify(token, this.legacyPublicKey) as JwtPayload

      // Legacy tokens have non-standard claims structure
      if (!payload.user_id || !payload.permissions) {
        return { valid: false, error: "Invalid legacy token structure" }
      }

      return { valid: true, payload }
    } catch (error) {
      return { valid: false, error: `Legacy validation failed: ${error.message}` }
    }
  }

  private async validatePingToken(token: string): Promise<TokenValidationResult> {
    try {
      // Extract kid from token header
      const header = JSON.parse(Buffer.from(token.split(".")[0], "base64").toString())
      const kid = header.kid

      if (!kid) {
        return { valid: false, error: "No kid in token header" }
      }

      // Get public key from JWKS (with caching)
      const publicKey = await this.getPublicKeyFromJwks(kid)
      if (!publicKey) {
        return { valid: false, error: "Public key not found in JWKS" }
      }

      const payload = verify(token, publicKey, {
        issuer: process.env.PING_ISSUER,
        audience: process.env.PING_AUDIENCE,
      }) as JwtPayload

      // Ping tokens follow OIDC standard claims
      if (!payload.sub || !payload.scope) {
        return { valid: false, error: "Invalid OIDC token structure" }
      }

      return { valid: true, payload }
    } catch (error) {
      return { valid: false, error: `Ping validation failed: ${error.message}` }
    }
  }

  private async getPublicKeyFromJwks(kid: string): Promise<string | null> {
    if (this.pingJwksCache.has(kid)) {
      return this.pingJwksCache.get(kid)!
    }

    try {
      const response = await fetch(this.pingJwksUri)
      const jwks = await response.json()

      const key = jwks.keys.find((k: any) => k.kid === kid)
      if (key) {
        const publicKey = this.jwkToPem(key)
        this.pingJwksCache.set(kid, publicKey)
        return publicKey
      }
    } catch (error) {
      console.error("Failed to fetch JWKS:", error)
    }

    return null
  }

  private jwkToPem(jwk: any): string {
    // Convert JWK to PEM format
    // Implementation details omitted for brevity
    return "" // Placeholder
  }
}

// Express middleware for dual token validation
export const authMiddleware = (validator: DualTokenValidator) => {
  return async (req: Request, res: Response, next: NextFunction) => {
    const authHeader = req.headers.authorization
    if (!authHeader || !authHeader.startsWith("Bearer ")) {
      return res.status(401).json({ error: "No token provided" })
    }

    const token = authHeader.substring(7)
    const result = await validator.validateToken(token)

    if (!result.valid) {
      return res.status(401).json({ error: result.error })
    }

    // Normalise user data regardless of issuer
    req.user = {
      id: result.issuer === "legacy" ? result.payload!.user_id : result.payload!.sub,
      permissions: result.issuer === "legacy" ? result.payload!.permissions : result.payload!.scope?.split(" ") || [],
      issuer: result.issuer,
    }

    next()
  }
}

Migration Strategy Patterns Link to heading

Successful identity migrations typically follow a phased approach:

Phase 1: Foundation Link to heading

  • Set up modern identity platform
  • Configure OIDC applications
  • Establish secure channels
  • Create comprehensive monitoring

Phase 2: Dual Support Link to heading

  • Implement multi-issuer validation
  • Add feature flags for gradual rollout
  • Create data normalisation layers
  • Extensive testing across scenarios

Phase 3: Incremental Migration Link to heading

  • Start with low-risk services
  • Monitor metrics carefully
  • Gradually expand scope
  • Maintain fallback options

Phase 4: Cleanup Link to heading

  • Remove legacy support
  • Decommission old infrastructure
  • Update all documentation

Enterprise Identity Considerations Link to heading

Working on identity in regulated environments teaches valuable lessons:

Compliance Requirements Link to heading

  • Audit trails: Every authentication event needs logging
  • Token expiration: Strict timeout requirements
  • Multi-factor authentication: Essential for production systems
  • Identity verification: Various regulatory requirements

User Experience Balance Link to heading

  • Zero downtime: Users expect continuous availability
  • Session management: Existing sessions must persist
  • Device compatibility: Mobile and web need different approaches
  • Error handling: Security through obscurity in error messages

Growing Domain Expertise Link to heading

Taking ownership of identity systems leads to rapid learning:

Technical Deep Dive Link to heading

  • OIDC specification: Understanding claims, flows, and standards
  • Identity platforms: Learning administration and APIs
  • Token debugging: Troubleshooting validation across services
  • Key management: Implementing rotation procedures
  • Federation: Connecting various identity providers

Expanding Responsibilities Link to heading

  • Technical consultation: Supporting other teams
  • Security participation: Contributing to assessments
  • Vendor management: Working with identity providers
  • Documentation: Creating developer guides
  • Knowledge sharing: Running internal workshops

Operational Lessons Link to heading

  • Validation failures: Understanding common issues
  • Service resilience: Building fallback mechanisms
  • Time synchronisation: Handling distributed system challenges
  • Certificate management: Planning for rotations

The Technical Deep End Link to heading

Some challenges were uniquely complex in the financial services context:

Multi-Issuer Token Validation Link to heading

Handling tokens from different issuers requires careful validation logic. APIs need to accept:

  • Legacy tokens with custom claims
  • Modern OIDC tokens with standard claims
  • Service-to-service authentication
  • Various refresh token patterns

Claim Mapping and Translation Link to heading

Different token issuers meant different claim structures. We needed middleware to normalise:

interface NormalisedUser {
  id: string // Legacy: user_id, Ping: sub
  permissions: string[] // Legacy: permissions array, Ping: scope string
  email?: string // Legacy: email, Ping: email
  roles: string[] // Legacy: roles array, Ping: groups claim
}

Session Management Complexity Link to heading

Financial applications require sophisticated session handling:

  • Sliding expiration: Extend session on user activity
  • Absolute timeout: Force re-authentication after maximum time
  • Concurrent session limits: Prevent too many active sessions
  • Device fingerprinting: Detect suspicious login patterns

Long-term Impact Link to heading

Identity migrations often become multi-year journeys with lasting effects:

Professional Development Link to heading

  • Domain expertise: Becoming a subject matter expert
  • Cross-functional skills: Working with various teams
  • Technical leadership: Guiding architectural decisions
  • Vendor relationships: Managing external providers

Organisational Benefits Link to heading

  • Security improvements: Modern authentication standards
  • Compliance simplification: Easier audits and assessments
  • Developer efficiency: Standardised patterns
  • User experience: Seamless authentication flows

Career Growth Link to heading

Volunteering for challenging projects outside your comfort zone often leads to unexpected opportunities. Identity and authentication expertise becomes valuable across many initiatives, opening doors to architectural discussions and strategic planning.

The key lesson: taking ownership of complex migrations can establish you as a domain expert and create long-term career opportunities.

Lessons Learned Link to heading

The identity migration taught me several important lessons about working in enterprise environments:

Volunteer Strategically Link to heading

Offering to help on projects outside your main domain can lead to unexpected career growth. The key is choosing projects that align with business priorities and offer learning opportunities.

Migration Complexity in Financial Services Link to heading

Financial services migrations are uniquely complex due to regulatory requirements, zero-downtime constraints, and the need for perfect accuracy. What seems simple in theory becomes intricate in practice.

Expertise Through Deep Dive Link to heading

Becoming a subject matter expert often happens by accident. By diving deep into the identity migration, I inadvertently became the company’s go-to person for authentication and authorization.

Standards Matter Link to heading

Moving from custom implementations to industry standards (OIDC) provided immediate benefits: better security, easier integrations, simplified audits, and reduced maintenance overhead.

Long-term Thinking Link to heading

Decisions made during the migration had implications years later. Building flexible, standards-compliant systems from the start pays dividends as the company grows and evolves.

Looking Forward Link to heading

The identity migration was just the beginning. It opened doors to other complex projects and established my reputation as someone who could tackle challenging technical initiatives. More importantly, it taught me the value of volunteering for work that pushes your boundaries and expands your expertise beyond your comfort zone.

But that’s a story for another post.


Have you ever volunteered for a project that completely changed your career trajectory? What identity and authentication challenges have you faced in enterprise environments?