Files
cve-dashboard/docs/architecture/ad-saml-integration.md
Jordan Ramos a95fd03f5e Rebrand STEAM → AEGIS, fix BU drift checker previous_bu bug
- Replace all STEAM branding with AEGIS (Advanced Engineering Group
  Intelligence System) across login, header, nav drawer, manifest, and
  browser title
- Add shield logo to login page, main header, and nav drawer
- Fix BU drift checker recording incorrect previous_bu values by
  building a previousBuMap snapshot BEFORE the upsert/delete cycle
  instead of querying the DB after rows are already gone
- Clean 526 bogus BU history entries generated by the broken logic
- Add docs and scripts from prior session
2026-06-17 14:40:38 -06:00

19 KiB

AD/SAML Integration Architecture

Overview

This document describes the architecture for integrating Active Directory (AD) authentication via SAML 2.0 into the STEAM Security Dashboard. The integration adds Single Sign-On (SSO) as the primary authentication method while retaining local password login as a break-glass fallback for administrators. AD group memberships drive automatic permission assignment and BU team scoping through a configurable mapping layer.


Authentication Model

The dashboard supports two authentication paths simultaneously:

Path Users Mechanism Session
Local Break-glass admins, service accounts Username + bcrypt password Cookie-based, PostgreSQL sessions table
SAML SSO All AD users SP-initiated SAML 2.0 via AD FS Same cookie-based session (identical to local)

Both paths produce the same session artifact — an httpOnly cookie containing a session_id that maps to a row in the sessions table. Downstream middleware (requireAuth, requireGroup) is unaware of how the session was created.


SAML 2.0 Authentication Flow

SP-Initiated Login (Success Path)

sequenceDiagram
    participant B as Browser
    participant SP as Dashboard (SP)
    participant IdP as AD FS (IdP)
    participant DB as PostgreSQL

    B->>SP: GET /api/auth/saml/login
    SP->>SP: Generate AuthnRequest XML
    SP->>B: HTTP 302 Redirect to IdP SSO URL (with AuthnRequest)
    B->>IdP: Follow redirect (user sees AD FS login page)
    IdP->>IdP: Authenticate user against AD
    IdP->>IdP: Build assertion (NameID, email, groups)
    IdP->>IdP: Sign assertion with IdP private key
    IdP->>B: HTTP 200 with auto-submit form (POST to SP callback)
    B->>SP: POST /api/auth/saml/callback (SAMLResponse in body)
    SP->>SP: Base64-decode SAMLResponse
    SP->>SP: Validate XML signature against IdP certificate
    SP->>SP: Check NotBefore/NotOnOrAfter (120s clock skew tolerance)
    SP->>SP: Extract NameID, email, displayName, group claims
    SP->>DB: Look up user by external_id (NameID)
    alt New user (no matching external_id)
        SP->>DB: INSERT into users (JIT provisioning)
        SP->>DB: INSERT audit_log (saml_user_provisioned)
    else Existing user
        SP->>DB: UPDATE user group, teams, email
        SP->>DB: INSERT audit_log (saml_user_updated) if changed
    end
    SP->>DB: INSERT into sessions (session_id, user_id, expires_at)
    SP->>DB: INSERT audit_log (saml_login)
    SP->>B: Set-Cookie: session_id=xxx; HttpOnly; SameSite=Lax
    SP->>B: HTTP 302 Redirect to /?saml_success=true
    B->>SP: GET /api/auth/me (with cookie)
    SP->>B: 200 { user: { id, username, group, teams, authSource } }

Assertion Rejection Path

sequenceDiagram
    participant B as Browser
    participant SP as Dashboard (SP)
    participant IdP as AD FS (IdP)

    B->>SP: GET /api/auth/saml/login
    SP->>B: HTTP 302 Redirect to IdP
    B->>IdP: Authenticate
    IdP->>B: POST assertion to SP callback
    B->>SP: POST /api/auth/saml/callback
    SP->>SP: Validate assertion
    alt Invalid signature
        SP->>SP: Log audit (saml_auth_failed, reason: invalid_signature)
        SP->>B: Redirect /?saml_error=Invalid+assertion+signature
    else Expired assertion
        SP->>SP: Log audit (saml_auth_failed, reason: assertion_expired)
        SP->>B: Redirect /?saml_error=Assertion+expired
    else Account disabled
        SP->>SP: Log audit (saml_auth_failed, reason: account_disabled)
        SP->>B: Redirect /?saml_error=Account+is+disabled
    end

Component Architecture

┌───────────────────────────────────────────────────────────────────┐
│                      Express Backend (port 3001)                    │
│                                                                    │
│  ┌────────────────┐   ┌─────────────────┐   ┌──────────────────┐ │
│  │ routes/saml.js │   │ routes/auth.js  │   │ middleware/auth.js│ │
│  │                │   │                 │   │                  │ │
│  │ GET  /status   │   │ POST /login     │   │ requireAuth()    │ │
│  │ GET  /login    │   │ POST /logout    │   │ requireGroup()   │ │
│  │ POST /callback │   │ GET  /me        │   │                  │ │
│  │ GET  /metadata │   │ POST /change-pw │   │ (unchanged —     │ │
│  └───────┬────────┘   └────────┬────────┘   │  reads session   │ │
│          │                     │             │  cookie only)    │ │
│          │                     │             └──────────────────┘ │
│          ▼                     ▼                                  │
│  ┌──────────────────────────────────────────────────┐            │
│  │             helpers/samlProvisioning.js            │            │
│  │                                                   │            │
│  │  resolveGroup(adGroups, config) → dashboardGroup  │            │
│  │  resolveTeams(adGroups, config) → "STEAM,..."     │            │
│  │  deriveUsername(nameId) → username                 │            │
│  │  provisionOrUpdateUser(assertion, config, ip)     │            │
│  └───────────────────────┬───────────────────────────┘            │
│                          │                                        │
│                          ▼                                        │
│  ┌──────────────────────────────────────────────────┐            │
│  │             helpers/samlConfig.js                  │            │
│  │                                                   │            │
│  │  loadGroupMappingConfig() → validated config obj  │            │
│  │  (reads config/adGroupMapping.json or env var)    │            │
│  └───────────────────────────────────────────────────┘            │
│                                                                    │
└────────────────────────────────────────────────────────────────────┘

Key Design Decisions

  1. Session reuse: SAML login creates the exact same session format as local login. No changes to requireAuth() middleware.
  2. Feature flag isolation: When SAML_ENABLED=false, SAML routes return 404 and no SAML library is loaded. Zero runtime cost when disabled.
  3. Config-driven mapping: AD group names are externalized in config/adGroupMapping.json. Changing the mapping requires only a file edit and backend restart — no code changes.
  4. JIT provisioning: Users are created on first login, updated on each subsequent login. AD is the source of truth for SSO users.
  5. Separation of concerns: The provisioning logic (samlProvisioning.js) is a pure module with no HTTP dependencies — fully unit-testable without a web server.

AD Group-to-Permission Mapping

Configuration Structure

{
    "groups": {
        "<AD-GROUP-CN>": "<Dashboard-Group>",
        "CVE-Dashboard-Admins": "Admin",
        "CVE-Dashboard-Users": "Standard_User",
        "CVE-Dashboard-Leadership": "Leadership",
        "CVE-Dashboard-ReadOnly": "Read_Only"
    },
    "teams": {
        "<AD-GROUP-CN>": "<BU-Team-ID>",
        "NTS-AEO-STEAM": "STEAM",
        "NTS-AEO-ACCESS-ENG": "ACCESS-ENG",
        "NTS-AEO-ACCESS-OPS": "ACCESS-OPS",
        "NTS-AEO-INTELDEV": "INTELDEV"
    },
    "groupPriority": ["Admin", "Standard_User", "Leadership", "Read_Only"],
    "defaultGroup": "Read_Only",
    "attributes": {
        "email": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
        "displayName": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
        "groups": "http://schemas.xmlsoap.org/claims/Group"
    }
}

Privilege Hierarchy

When a user belongs to multiple AD groups that map to different dashboard groups, the highest-privilege group wins:

Admin > Standard_User > Leadership > Read_Only

Multi-Team Assignment

When a user belongs to multiple AD groups that map to BU teams, all matching teams are assigned:

AD Groups: ["NTS-AEO-STEAM", "NTS-AEO-ACCESS-ENG", "CVE-Dashboard-Users"]
→ user_group: "Standard_User"
→ bu_teams: "ACCESS-ENG,STEAM"  (sorted alphabetically, deduplicated)

Placeholder Group Names

The AD group names in the groups and teams sections (e.g., "CVE-Dashboard-Admins") are placeholders. When real group CNs are obtained from the AD administrators, update only this configuration file. No code changes required.


Database Schema Changes

Migration: add_saml_auth_columns.js

Column Type Nullable Default Purpose
auth_source VARCHAR(10) NOT NULL 'local' Discriminates local vs SSO users
external_id VARCHAR(256) NULL NULL SAML NameID for IdP correlation

Additional changes:

  • password_hash becomes nullable (SAML users have no local password)
  • Partial unique index on external_id WHERE external_id IS NOT NULL

Impact on Existing Data

  • All existing users receive auth_source = 'local' and external_id = NULL
  • No existing functionality is affected
  • Migration is idempotent (safe to re-run)

Environment Variables

Variable Required Description Example
SAML_ENABLED Always Master feature flag for SAML authentication false
SAML_IDP_METADATA_URL When SAML_ENABLED=true AD FS federation metadata endpoint https://adfs.corp.local/FederationMetadata/2007-06/FederationMetadata.xml
SAML_SP_ENTITY_ID When SAML_ENABLED=true Unique identifier for this Service Provider http://71.85.90.6:3001
SAML_SP_CALLBACK_URL When SAML_ENABLED=true Assertion consumer service URL http://71.85.90.6:3001/api/auth/saml/callback
SAML_IDP_CERT_PATH When SAML_ENABLED=true File path to IdP signing certificate (PEM format) /etc/cve-dashboard/idp-cert.pem
SESSION_LIFETIME_HOURS Optional Session duration (1-720 hours, default: 24) 8
AD_GROUP_MAPPING_JSON Optional JSON string override for adGroupMapping.json {"groups":{...},"teams":{...}}

Startup Validation

When SAML_ENABLED=true, the server validates at startup:

  1. All required SAML env vars are set (fails with descriptive error if missing)
  2. Certificate file exists and is readable (fails if not)
  3. Group mapping config parses as valid JSON (fails if not)
  4. All mapped team names exist in KNOWN_TEAMS (fails if not)
  5. All mapped dashboard groups are valid (fails if not)

This fail-fast approach prevents silent misconfiguration in production.


Build vs Wait: Phase Breakdown

Phase 1 — Build Now (No AD Access Required)

Component File Test Strategy
Database migration backend/migrations/add_saml_auth_columns.js Run against test DB, verify columns
Group mapping config backend/config/adGroupMapping.json Startup validation tests
Config loader backend/helpers/samlConfig.js Unit test with mock JSON files
JIT provisioner backend/helpers/samlProvisioning.js Unit test all paths with mock pool
SAML routes (skeleton) backend/routes/saml.js Integration test: feature flag, status, metadata
Session lifetime server.js (startup block) Unit test env var parsing
Auth route changes backend/routes/auth.js Integration test: SAML user login rejection
User route changes backend/routes/users.js Integration test: auth_source in responses, password block
Frontend SSO button frontend/src/components/LoginForm.js Render test: button hidden when flag=false
Admin auth_source badges frontend/src/components/UserManagement.js Render test: badge displays
Architecture doc docs/architecture/ad-saml-integration.md N/A (documentation)

Phase 2 — Requires Live AD FS Connection

Component Dependency Who Provides It
SAML library installation Package selection (@node-saml/passport-saml or saml2-js) Development team
IdP metadata URL AD FS federation metadata endpoint AD administrators
IdP signing certificate Token-signing cert exported from AD FS AD administrators
SP registration Relying party trust created in AD FS console AD administrators
Real AD group names Actual CNs of permission/team groups AD administrators
Assertion parsing implementation Fill in routes/saml.js callback Development team
End-to-end flow testing Working AD user accounts AD administrators
Session lifetime tuning AD FS token lifetime policy value AD administrators

Security Considerations

Certificate Management

  • The IdP signing certificate is stored on disk at SAML_IDP_CERT_PATH
  • When the IdP rotates its certificate, replace the file and restart the backend
  • No database migration required for certificate rotation
  • Consider monitoring certificate expiry dates (AD FS certs typically rotate annually)

Assertion Replay Prevention

  • Each SAML assertion is consumed exactly once by the callback handler
  • The JIT provisioner's idempotent update pattern means replayed assertions would simply re-update the same user record (no escalation possible)
  • For additional protection in Phase 2, implement InResponseTo validation and a short-lived assertion ID cache

Trust Boundary

┌─────────────────────────────────┐     ┌─────────────────────────────────┐
│         Trust Zone A             │     │         Trust Zone B             │
│                                  │     │                                  │
│  Dashboard (SP)                  │     │  AD FS (IdP)                     │
│  - Validates assertions          │     │  - Authenticates users           │
│  - Trusts ONLY signed assertions │     │  - Signs assertions with         │
│  - Creates local sessions        │     │    private key                   │
│  - Enforces local authorization  │     │  - Asserts group memberships     │
│                                  │     │                                  │
└────────────────┬─────────────────┘     └────────────────┬────────────────┘
                 │                                         │
                 └─── SAML 2.0 over HTTPS (HTTP-POST) ────┘
  • The SP trusts assertions only when cryptographically signed by the IdP
  • Group memberships in the assertion drive permission assignment — the SP does not query AD directly
  • If the IdP is compromised, an attacker could forge assertions. Mitigate with certificate pinning and monitoring assertion patterns in audit logs.
  • The SP never sends credentials to the IdP — authentication happens entirely on the IdP side

Break-Glass Protection

  • The last local Admin account cannot be deleted or deactivated
  • If the IdP is unavailable, local Admin users can still log in with username/password
  • SAML users cannot authenticate via password (and vice versa) — the two paths are isolated per user record

Transport Security

  • Production deployments should serve the SP callback over HTTPS
  • The SAMLResponse is transmitted via HTTP-POST binding (browser-mediated, not direct server-to-server)
  • The assertion is signed — even if transmitted over HTTP, it cannot be tampered with without detection
  • For defense in depth, HTTPS prevents assertion interception by network observers

Hybrid Login User Experience

┌─────────────────────────────────────────────────────────────┐
│                     Login Page                                │
│                                                              │
│  ┌────────────────────────────────────────────────────────┐ │
│  │           ┌─────────────────────────────┐              │ │
│  │           │   Sign in with SSO           │              │ │
│  │           │   (redirects to AD FS)       │              │ │
│  │           └─────────────────────────────┘              │ │
│  │                                                        │ │
│  │           ─── or sign in with local account ───        │ │
│  │                                                        │ │
│  │           Username: [________________]                 │ │
│  │           Password: [________________]                 │ │
│  │                                                        │ │
│  │           [Sign In]                                    │ │
│  └────────────────────────────────────────────────────────┘ │
│                                                              │
│  Note: SSO button only visible when SAML_ENABLED=true       │
│  Local login always available (break-glass for admins)       │
└─────────────────────────────────────────────────────────────┘

  • .kiro/specs/ad-saml-integration/requirements.md — detailed acceptance criteria
  • .kiro/specs/ad-saml-integration/design.md — implementation design with code examples
  • .kiro/specs/group-based-access-control/requirements.md — existing RBAC system
  • .kiro/specs/multi-bu-tenancy/design.md — BU team scoping (leveraged by AD integration)