Skip to content

Add enterprise IdP plugin with OIDC and SAML 2.0 support#9

Open
Tbsheff wants to merge 8 commits intovercel-labs:mainfrom
Tbsheff:feature/enterprise-idp-plugin
Open

Add enterprise IdP plugin with OIDC and SAML 2.0 support#9
Tbsheff wants to merge 8 commits intovercel-labs:mainfrom
Tbsheff:feature/enterprise-idp-plugin

Conversation

@Tbsheff
Copy link
Copy Markdown

@Tbsheff Tbsheff commented Mar 24, 2026

Summary

Add a new first-class idp service plugin that acts as a local enterprise identity provider for end-to-end testing of OIDC/OAuth 2.0 and SAML 2.0 authentication flows entirely offline.

Motivation

Teams integrating enterprise login (SSO) need to test real auth behavior locally and in CI without network access. The existing Google plugin is a lightweight OAuth/OIDC shim (HS256, stub JWKS) — not suitable for enterprise auth testing. This plugin provides a production-grade local IdP with asymmetric signing, proper JWKS, refresh tokens, SAML assertions, and configurable claim mappings.

What's included

OIDC / OAuth 2.0 Provider (9 endpoints)

Endpoint Description
GET /.well-known/openid-configuration OpenID Connect discovery
GET /jwks.json JSON Web Key Set (RS256)
GET /authorize Authorization page with browser sign-in picker
POST /authorize/callback User selection, generates authorization code
POST /token Token exchange (authorization_code + refresh_token grants)
GET /userinfo User claims with scope-based filtering
POST /revoke Token revocation (RFC 7009)
GET /logout End session with redirect
GET /_debug/state Debug introspection (disabled in strict mode)

Key features:

  • RS256 JWT signing with auto-generated or seeded RSA keys
  • PKCE support (S256 and plain)
  • Refresh token rotation (offline_access scope)
  • Custom claim mappings via dot-path resolution (e.g., "attributes.department")
  • client_secret_basic and client_secret_post authentication
  • Groups, roles, and arbitrary user attributes

SAML 2.0 Identity Provider (3 endpoints)

Endpoint Description
GET /saml/metadata IdP metadata XML with X.509 signing certificate
GET /saml/sso SP-initiated SSO entry point (decodes SAMLRequest, shows user picker)
POST /saml/sso/callback Builds signed SAMLResponse, auto-POSTs to SP ACS URL

Key features:

  • Signed SAML assertions (RSA-SHA256) via xml-crypto
  • Self-signed X.509 certificate generation via @peculiar/x509
  • Microsoft Entra ID claim URI presets out of the box
  • Configurable attribute_mappings and name_id_format per service provider
  • HTTP-POST binding to Assertion Consumer Service
  • RelayState roundtrip support

Shared capabilities

  • Strict mode: Enforces client/SP validation, requires PKCE, disables debug endpoint
  • Seeded users: Groups, roles, and custom attributes with zero-config defaults
  • Browser sign-in picker: Same green-on-black UX as existing services
  • CLI integration: emulate --service idp, emulate list, emulate init --service idp
  • Programmatic API: createEmulator({ service: "idp" })

New dependencies

Package Size Purpose
xml-crypto ^6 285KB XML signing with Exclusive C14N (same lib samlify uses)
@peculiar/x509 ^2 539KB Self-signed X.509 certificate generation

Both are pure JS with zero native dependencies.

Usage

CLI

emulate --service idp --port 4003

Programmatic (Vitest/Jest)

import { createEmulator } from "emulate";

const idp = await createEmulator({ service: "idp", port: 4003, seed: {
  idp: {
    users: [
      { email: "alice@example.com", name: "Alice", groups: ["admins"] },
    ],
    oidc: {
      clients: [{ client_id: "app", client_secret: "secret", redirect_uris: ["http://localhost:3000/cb"] }],
    },
    saml: {
      service_providers: [{ entity_id: "http://localhost:3000", acs_url: "http://localhost:3000/api/auth/saml/callback" }],
    },
  },
}});

Seed config (YAML)

idp:
  users:
    - email: alice@example.com
      name: Alice Example
      groups: [engineering, admins]
      roles: [owner]
      attributes:
        department: Engineering
  oidc:
    clients:
      - client_id: app-local
        client_secret: secret-local
        redirect_uris: [http://localhost:3000/callback]
  saml:
    service_providers:
      - entity_id: http://localhost:3000
        acs_url: http://localhost:3000/api/auth/sso/saml2/callback/local-idp

Testing

  • 67 plugin-specific tests written TDD (red-green-refactor):
    • Crypto unit tests (key generation, JWT signing, PKCE, path resolution)
    • OIDC integration tests (full auth code flow, PKCE, refresh tokens, userinfo, strict mode)
    • SAML integration tests (metadata, SSO flow, signed assertions, attribute mapping)
  • 145 total tests passing across the monorepo
  • All existing services (vercel, github, google) unaffected

Files

New package: packages/@internal/idp/

  • src/entities.ts — IdpUser, IdpGroup, IdpClient, IdpSigningKey, IdpServiceProvider
  • src/store.ts — Typed collections with field indexes
  • src/crypto.ts — RSA keygen, RS256 JWT signing, X.509 cert generation, PKCE
  • src/helpers.ts — Ephemeral data accessors, utility functions
  • src/saml-constants.ts — Entra ID claim URIs, SAML namespace constants
  • src/saml-xml.ts — XML builders for metadata, response, signing, auto-post form
  • src/routes/oidc.ts — 9 OIDC endpoints
  • src/routes/saml.ts — 3 SAML endpoints
  • src/index.ts — Plugin export, seed config, seed functions

Modified files

  • packages/emulate/src/commands/start.ts — Register idp in SERVICE_PLUGINS
  • packages/emulate/src/api.ts — Add idp to programmatic API
  • packages/emulate/src/commands/list.ts — Add idp to service descriptions
  • packages/emulate/src/commands/init.ts — Add idp default config template
  • packages/emulate/package.json — Add @internal/idp workspace dependency
  • packages/emulate/tsup.config.ts — Fix ESM bundle for CJS deps using Node built-ins

New first-class `idp` service plugin providing a local enterprise identity
provider for end-to-end testing of OIDC and SAML SSO flows.

OIDC Provider:
- OpenID Connect discovery, JWKS, authorize, token, userinfo, revoke, logout
- RS256 JWT signing with auto-generated or seeded RSA keys
- Authorization code flow with PKCE (S256/plain)
- Refresh token support with rotation (offline_access scope)
- Custom claim mappings via dot-path resolution
- client_secret_basic and client_secret_post authentication
- Browser sign-in picker matching existing emulate UX

SAML 2.0 IdP:
- IdP metadata endpoint with X.509 signing certificate
- SP-initiated SSO with HTTP-Redirect binding
- Signed SAML assertions (RSA-SHA256) via xml-crypto
- Auto-submit POST form to Assertion Consumer Service
- Microsoft Entra ID claim URI presets (nameidentifier, emailaddress, etc.)
- Configurable attribute_mappings and name_id_format per service provider
- RelayState roundtrip support

Shared:
- Strict mode: enforces client/SP validation, requires PKCE, disables debug
- Seeded users with groups, roles, and custom attributes
- Debug endpoint for inspecting sessions, codes, and tokens
- CLI integration: start, list, init, programmatic API
- Zero-config boot with sensible defaults

Dependencies:
- xml-crypto ^6 (XML signing with Exclusive C14N)
- @peculiar/x509 ^2 (self-signed X.509 certificate generation)

Testing:
- 67 plugin tests (TDD: crypto, seed, OIDC integration, SAML integration)
- All 160+ monorepo tests passing
Copilot AI review requested due to automatic review settings March 24, 2026 02:13
@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Mar 24, 2026

@Tbsheff is attempting to deploy a commit to the Vercel Labs Team on Vercel.

A member of the Team first needs to authorize it.

@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@socket-security
Copy link
Copy Markdown

socket-security bot commented Mar 24, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedreflect-metadata@​0.2.21001001008080
Added@​xmldom/​xmldom@​0.8.119910010081100
Addedxml-crypto@​6.1.29910010086100
Added@​peculiar/​x509@​2.0.09910010089100
Addedhono@​4.12.9991009796100

View full report

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new first-class idp service plugin to the emulate toolchain, providing an offline enterprise Identity Provider emulator with OIDC/OAuth2 and SAML2 flows for E2E testing.

Changes:

  • Introduces @internal/idp package implementing OIDC endpoints (discovery/JWKS/authorize/token/userinfo/revoke/logout/debug) and SAML endpoints (metadata/SSO/callback) with signing/cert generation.
  • Wires the new idp plugin into CLI (start, list, init) and programmatic API (createEmulator), plus adds a basic API test.
  • Updates bundling (tsup) and lockfile to accommodate new dependencies.

Reviewed changes

Copilot reviewed 22 out of 23 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
pnpm-lock.yaml Adds @peculiar/x509, xml-crypto, @xmldom/xmldom and related transitive deps; also includes unrelated peer-resolution churn.
packages/emulate/tsup.config.ts Adjusts tsup config (node platform + banner shim) for Node/CJS interop in ESM bundles.
packages/emulate/src/commands/start.ts Registers idp service plugin and seeds it from config.
packages/emulate/src/commands/list.ts Adds idp to the CLI service listing.
packages/emulate/src/commands/init.ts Adds default seed template for idp service.
packages/emulate/src/api.ts Adds idp to createEmulator programmatic API and seeding.
packages/emulate/src/tests/api.test.ts Adds a smoke test for IdP discovery advertising RS256.
packages/emulate/package.json Adds @internal/idp workspace dependency.
packages/@internal/idp/tsup.config.ts Build configuration for the new internal IdP package.
packages/@internal/idp/tsconfig.json TypeScript config for the new IdP package.
packages/@internal/idp/src/store.ts Defines typed collections for IdP entities in the core Store.
packages/@internal/idp/src/saml-xml.ts SAML XML builders + XML-DSig signing + auto-post HTML form.
packages/@internal/idp/src/saml-constants.ts SAML constants and Entra ID default claim URI mappings.
packages/@internal/idp/src/routes/saml.ts Implements /saml/metadata, /saml/sso, /saml/sso/callback.
packages/@internal/idp/src/routes/oidc.ts Implements OIDC/OAuth endpoints including token/refresh/revoke/userinfo/logout/debug.
packages/@internal/idp/src/index.ts Exposes plugin entrypoint and seed schema/logic.
packages/@internal/idp/src/helpers.ts Strict-mode/issuer helpers + ephemeral in-store maps for codes/tokens/sessions/SAML requests.
packages/@internal/idp/src/entities.ts Defines IdP entity types: users, groups, clients, signing keys, SPs.
packages/@internal/idp/src/crypto.ts RSA keygen/JWK export, JWT signing, PKCE, X.509 self-signed cert generation, dot-path resolution.
packages/@internal/idp/src/tests/saml.test.ts Unit/integration tests for SAML metadata/SSO/signing/form/mappings.
packages/@internal/idp/src/tests/idp.test.ts OIDC integration tests (auth code, PKCE, refresh rotation, revoke, debug, JWKS verification).
packages/@internal/idp/src/tests/crypto.test.ts Unit tests for keygen/import, JWT signing, path resolution, PKCE.
packages/@internal/idp/package.json Declares the new internal package and its runtime/test deps.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Tbsheff added 3 commits March 23, 2026 21:34
- Fix require() in ESM module — use static import
- Validate redirect_uri before constructing URL (prevents 500)
- Validate redirect_uri and client_id match in token exchange
- Strict mode requires PKCE code_challenge
- Fix TokenMap id type (use entity id, not uid string)
- Validate client_id on refresh token exchange
- Escape clientName in HTML output (XSS prevention)
- XML-escape all interpolated SAML response parameters
- Add xs namespace declaration for xsi:type attribute
- Add TTL expiry check for pending SAML requests
- Strict mode rejects unknown SPs even when none configured
- Update service list label to include SAML
- Type grant handler context as Context instead of any
- Add comment explaining reflect-metadata import
- Add test: JSON Content-Type on /token endpoint
- Add test: unsupported_grant_type rejection
- Add test: invalid SAML request ref rejection
- Use timingSafeEqual for PKCE verification
…d code

- Add eviction cap on revokedTokens and tokenClients (10k max)
- Remove dead sessions code (never written to)
- Validate response_type=code in /authorize (reject unsupported types)
- Wrap new URL() in try/catch in /logout endpoint
- Fix client_id binding: reject mismatch when code was bound to a client
- Only issue refresh token on refresh grant if offline_access in scope
- HTML-escape user inputs in error messages (XSS prevention)
- Remove dead getCertificatePem from crypto.ts
- Move @xmldom/xmldom to devDependencies
- Consolidate duplicate @internal/core imports in saml.ts
@ctate
Copy link
Copy Markdown
Collaborator

ctate commented Mar 24, 2026

Thanks for this excellent contribution — the scope and quality here is really impressive. I found a few issues I'd like addressed before merging:

  1. Lockfile out of sync@xmldom/xmldom was added as a devDependency but the lockfile wasn't regenerated. CI will fail on pnpm install --frozen-lockfile.

  2. Token revocation is a no-op/revoke deletes the token from the token map, but the core auth middleware's fallback user path accepts any unknown token. So revoked tokens still work at /userinfo. I verified this end-to-end.

  3. Silent fallback to wrong user — When a uid from a pending code doesn't match any user, the code falls back to the first user instead of returning an error. This silently authenticates as the wrong person.

  4. Tests don't exercise uid lookup — Tests always submit fake uids that miss, triggering the fallback from item 3. Once that's fixed, these tests will need real user uids.

  5. No test coverage for /logout — Given it has conditional validation logic, a few basic cases would be good.

Everything else — SAML flow, sign-in picker, OIDC discovery, JWKS, build — works great. Happy to re-review once these are addressed!

1. Regenerate lockfile to fix frozen-lockfile CI failures
2. Check revoked tokens in /userinfo so revocation is enforced
3. Return error for unknown uid instead of silently falling back to first user
4. Update all tests to use real user uids from the store
5. Add /logout endpoint test coverage (5 cases)
6. Add test verifying revoked tokens are rejected at /userinfo
@Tbsheff
Copy link
Copy Markdown
Author

Tbsheff commented Mar 24, 2026

Thanks for the thorough review! All 5 items addressed in commit 28a4fb2:

  1. Lockfile — Regenerated via pnpm install; @xmldom/xmldom now properly resolved.
  2. Token revocation/userinfo now checks the revoked tokens set before processing the request, so revoked tokens are properly rejected (added test confirming this).
  3. Silent fallback — Removed the first-user fallback in both auth-code and refresh-token handlers. Unknown uid now returns invalid_grant / "User not found" error.
  4. Test uid lookup — All tests now look up the real uid from the store via a getUserUid() helper instead of passing fake strings.
  5. Logout tests — Added 5 test cases covering: no redirect (signed-out page), valid redirect with state, invalid URI (400), unregistered URI in strict mode (400), and registered URI in strict mode (302).

Tbsheff added 2 commits March 24, 2026 11:10
…idp-plugin

# Conflicts:
#	packages/emulate/package.json
#	packages/emulate/src/api.ts
#	packages/emulate/src/commands/init.ts
#	packages/emulate/src/commands/start.ts
#	pnpm-lock.yaml
- CRITICAL: Remove SAML user fallback (auth bypass) — unknown uid now returns error
- Add memory cap on pendingSamlRequests and pendingCodes maps
- Increase eviction threshold to 50k with safety-valve documentation
- Add test for response_type validation
- Add test for PKCE strict mode enforcement
- Document intentional ID token claim inclusion
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants