SAML to OAuth2 Bridge Solution

Context & Problem

In a legacy enterprise environment, many identity providers (IdPs) only support SAML (Security Assertion Markup Language) for single sign-on (SSO). However, modern web applications and APIs typically rely on OAuth2 or OIDC (OpenID Connect) for authorization and authentication, using JSON Web Tokens (JWTs).

At WirelessCar, we faced a challenge where we needed to connect a legacy Active Directory (SAML-only) with a modern application that required OAuth2 tokens for Role-Based Access Control (RBAC). The legacy system could not issue JWTs, and the modern application could not parse SAML assertions.

Solution Overview

To bridge this gap, we built a serverless connector using AWS Lambdas and DynamoDB. This solution acts as a mediator, exchanging a valid SAML assertion for a custom OAuth2 token that the downstream application accepts.

Key Components

  1. SAML Parser Lambda (Node.js):
    • Role: Entry point for the SAML assertion.
    • Logic: Validates the SAML signature, parses the XML to extract user attributes (UPN, email) and group memberships (roles).
    • Output: Saves the normalized user context to DynamoDB with a short Time-To-Live (TTL).
  2. Context Store (DynamoDB):
    • Role: Temporary state store.
    • Logic: Holds the user information extracted from SAML. Acts as a secure hand-off point between the parser and the minter.
  3. Token Minter Lambda (Java):
    • Role: OAuth2 Authorization Server (simplified).
    • Logic: Retrieved user context from DynamoDB. Maps AD roles to application scopes/claims. Calls internal signing endpoints or uses a keystore to generate a signed JWT.
    • Output: Returns the access token to the client.

Architecture

High-Level Flow

The client (browser) performs a SAML login with the IdP, receives an assertion, and posts it to our bridge. The bridge processes it and returns an OAuth2 token.

sequenceDiagram
    participant User as User/Browser
    participant IdP as Legacy AD (SAML)
    participant Parser as SAML Parser Lambda (Node.js)
    participant DDB as DynamoDB
    participant Minter as Token Mint Lambda (Java)
    participant App as Modern App

    User->>IdP: Authenticate
    IdP-->>User: SAML Assertion (XML)
    
    User->>Parser: POST /login/saml (Assertion)
    activate Parser
    Parser->>Parser: Parse & Validate SAML
    Parser->>Parser: Extract Roles & User Info
    Parser->>DDB: Save User Context (ID: RequestID)
    Parser->>Minter: Invoke (RequestID)
    deactivate Parser
    
    activate Minter
    Minter->>DDB: Get User Context (RequestID)
    DDB-->>Minter: { User: "...", Roles: [...] }
    Minter->>Minter: Map Roles to Scopes
    Minter->>Minter: Generate & Sign JWT
    Minter-->>User: Return Access Token
    deactivate Minter
    
    User->>App: Request with Bearer Token

Component Diagram

flowchart LR
    subgraph "External"
        IdP[Legacy Active Directory]
        Client[Web Client]
    end

    subgraph "AWS Bridge"
        direction TB
        Parser["Lambda: SAML Parser<br/>(Node.js + saml2-js)"]
        Minter["Lambda: Token Minter<br/>(Java)"]
        DDB[(DynamoDB Table)]
    end
    
    subgraph "Downstream"
        App[Modern Application]
    end

    IdP -->|SAML Assertion| Client
    Client -->|POST Assertion| Parser
    Parser -->|1. Parse & Extract| Parser
    Parser -->|2. Save Context| DDB
    Parser -->|3. Invoke| Minter
    Minter -->|4. Read Context| DDB
    Minter -->|5. Generate Token| Client
    Client --|Bearer Token|--> App

Detailed Implementation Notes

Node.js SAML Parser

We chose Node.js for the parsing layer because of its excellent ecosystem for handling JSON and XML.

  • Library: Used saml2-js (or passport-saml) to handle the complex XML parsing and signature verification.
  • Security: Verifies the IdP’s X.509 certificate to ensure the assertion is authentic.

DynamoDB Storage

  • Partition Key: A unique temporary ID (e.g., a one-time code or correlation ID) generated by the Parser.
  • Data: Stores user_principal_name, email, and a list of ad_groups.
  • TTL: Records are set to expire very quickly (e.g., 5 minutes) to prevent replay attacks or stale data usage.

Java Token Minter

We used Java here likely due to existing internal libraries or strict security compliance requirements for token signing that were already implemented in Java.

  • Token Generation: Uses standard libraries (like JJWT or Nimbus JOSE + JWT) to create signed tokens.
  • RBAC Mapping: Translates raw AD groups (e.g., CN=App_Admins,OU=...) into clean application scopes (e.g., admin:write, user:read).

Pros & Cons

Pros

  • Decoupling: The modern app doesn’t need to know anything about SAML or XML.
  • Security: The bridge isolates the legacy protocol. Token generation is centralized.
  • Scalability: AWS Lambda handles the load automatically.

Cons

  • Latency: Adding two Lambda hops and a DB write adds some latency to the login process.
  • Complexity: Managing two separate Lambdas and a state store is more complex than a direct integration.