Files
mcp-gateway-nexus/docs/modules/06-authentication.md
drjones 8d3ffef920 docs: initial architecture and design for MCP Nexus
MCP Nexus is a self-hosted control plane for MCP servers — "Kubernetes for
MCP." Agents connect to one endpoint; Nexus discovers services, installs the
right MCP servers, aggregates them behind a namespaced router, secures access
with auth/RBAC, and heals/updates them via a reconcile loop.

This first commit is design-phase only (no runnable code yet):
- README.md            project front door + module map
- docs/ARCHITECTURE.md target design: tenets, two-plane split, reconcile
                       loop, domain types, storage, security, deployment
- docs/ROADMAP.md      phased delivery (foundations -> walking skeleton ->
                       discover+install -> secure -> operate -> extend/scale)
- docs/modules/01-20   one design doc per module, all cross-linked

Backend stack decision: Go (single static binary, embedded SQLite + embedded
React dashboard). Repo initialized in /root with a whitelist .gitignore so
only project files are tracked.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 04:38:27 +00:00

10 KiB

Authentication

Module 06 · Plane: Data · Roadmap phase: 3 Part of MCP Nexus architecture.

Purpose

Authentication turns an inbound connection at the API Gateway into a verified principal — a stable identity for either a human user (dashboard/API operator) or an AI agent (Claude, Cursor, …). It answers exactly one question: "Who is calling?" It does not decide what they may do; that is RBAC. AuthN is the seam through which every request passes before it can be scoped, routed, or audited (Architecture §5, §9). It is pluggable by tenet #3: API keys first (Phase 3), OAuth2/OIDC and JWT sessions next, and LDAP/Active Directory/SAML (enabling SSO) later.

Responsibilities

  • Own a set of pluggable AuthN providers that each attempt to resolve request credentials into a Principal. Starter provider: API keys. Follow-ons: OAuth2, OpenID Connect (authorization-code + PKCE), JWT session validation, then LDAP, Active Directory, and SAML (SSO/enterprise IdP federation).
  • Extract credentials from the request in a provider-appropriate way: Authorization: Bearer <token>, X-API-Key, a session cookie, or an MCP initialize handshake carrying an agent credential.
  • Establish and validate sessions: mint short-lived JWT access tokens (with a refresh path) after an interactive login; validate their signature, expiry, audience, and issuer on every request.
  • Issue, hash, store, list, and revoke API keys; keys are stored only as salted hashes, shown in plaintext exactly once at creation.
  • Attach the resolved Principal (with its provider, credential id, and asserted identity claims) to the request context so downstream RBAC, Router, and Audit all read one canonical identity.
  • Map an agent's presented credential to its AgentProfile identity so profile→role resolution can proceed.
  • Emit an audit event for every authentication decision — success, failure, and revocation — with principal, provider, and source address (Architecture §9).

Non-goals

  • Authorization. Deciding which tools/namespaces a principal may see or call is RBAC; this module only proves identity.
  • The agent-facing identity binding. Modeling per-agent policy objects is AI Agent Profiles; AuthN merely resolves a credential to the profile's identity.
  • Credential storage for upstream services. MCP-server secrets live in the Secrets Manager; AuthN secures access to Nexus, not Nexus's access to backends.
  • Transport security. TLS termination is the Gateway / Security baseline.
  • Plugin transport. Out-of-process auth-backend plugins are wired via the Plugin System in Phase 5; this module defines only the in-process interface.

Interfaces

// A resolved, verified identity. Attached to every request context after AuthN.
type Principal struct {
    ID       string            // stable identity key, e.g. "user:alice", "agent:claude-01"
    Kind     PrincipalKind     // KindUser | KindAgent | KindService
    Provider string            // "apikey", "oidc", "jwt", "ldap", "saml"
    Claims   map[string]string // sub, email, groups, agent_id, ...
    Expires  time.Time         // zero for non-expiring credentials (e.g. API key)
}

type PrincipalKind string

const (
    KindUser    PrincipalKind = "user"
    KindAgent   PrincipalKind = "agent"
    KindService PrincipalKind = "service"
)

// A pluggable authentication backend (tenet #3).
type Provider interface {
    Name() string // "apikey", "oidc", ...
    // Authenticate inspects the request; ok=false if this provider does not
    // recognize the credential, err for a malformed/invalid one.
    Authenticate(ctx context.Context, r *http.Request) (Principal, bool, error)
}

// The authenticator runs providers in order and returns the first match.
type Authenticator interface {
    Register(p Provider) error
    Resolve(ctx context.Context, r *http.Request) (Principal, error) // ErrUnauthenticated if none match
}

// API-key lifecycle. Keys are stored hashed; plaintext is returned once.
type KeyManager interface {
    Issue(ctx context.Context, subject string, opts KeyOpts) (plaintext string, id string, err error)
    Verify(ctx context.Context, presented string) (Principal, error)
    Revoke(ctx context.Context, id string) error
    List(ctx context.Context, subject string) ([]KeyMeta, error)
}

HTTP/API surface (behind the Gateway; TLS-terminated):

  • POST /api/v1/auth/login — interactive login → sets session cookie / returns JWT.
  • POST /api/v1/auth/refresh — exchange a refresh token for a fresh access token.
  • POST /api/v1/auth/logout — invalidate the current session.
  • GET /api/v1/auth/oidc/callback — OAuth2/OIDC authorization-code redirect target.
  • GET /api/v1/auth/saml/acs — SAML assertion consumer service (SSO, later).
  • POST /api/v1/auth/keys · GET /api/v1/auth/keys · DELETE /api/v1/auth/keys/{id} — API-key CRUD (RBAC-gated).

MCP handshake: an agent presents its credential in the initialize request (bearer token or API key header on the Streamable HTTP transport); the resolved Principal is pinned to the MCP session for its lifetime.

Request-resolution flow (data plane, per request):

1. Gateway receives request (MCP over Streamable HTTP, or dashboard/API call).
2. Authenticator.Resolve iterates registered providers in priority order:
     apikey → jwt/session → oidc → (ldap/ad/saml later)
3. First provider that recognizes the credential returns (Principal, true).
     - none recognize it            → ErrUnauthenticated → 401 (audited)
     - one recognizes but it's bad  → error → 401 (audited, no fall-through)
4. Principal attached to request context (id, kind, provider, claims).
5. For agents: Principal.ID feeds AgentProfile.Identify (Module 14).
6. RBAC (Module 08) reads the Principal to scope the request.
7. Audit records the decision: {principal, provider, source_ip, outcome}.

Data

  • Reads/writes the Identity store (Architecture §8): Users, AgentProfiles, Roles, and API keys (stored as {id, subject, hash, salt, created, last_used, expires, revoked}).
  • Reads OIDC/SAML provider config (issuer URL, client id, JWKS endpoint, metadata) from the Config store; the OAuth client secret is a ref into the Secrets Manager, never inlined.
  • Writes authentication events to the Audit store (append-only).
  • JWT signing keys live in the Secrets store; the JWKS for verifying externally issued tokens is fetched and cached from the IdP.

Dependencies

  • Sits behind the Gateway, which invokes Resolve as the first middleware on the data-plane hot path.
  • Feeds the Principal to RBAC for authorization and to AI Agent Profiles for identity→profile binding.
  • OAuth client secrets and JWT signing keys come from Secrets.
  • Every decision is recorded per Security §9 audit rules.
  • Backend providers are plugin points via the Plugin System (Phase 5).

Failure modes & handling

Failure Behavior
No provider recognizes the credential ErrUnauthenticated → 401; audited as a failed attempt with source IP.
Malformed/expired JWT 401; client directed to /auth/refresh or re-login; never falls through to another provider as valid.
IdP (OIDC/SAML) unreachable Interactive login fails closed; already-issued, still-valid JWTs continue to work until expiry (no data-plane outage).
JWKS fetch fails Serve from cached keys; refuse tokens signed by unknown kid rather than trusting blindly.
API key leaked/compromised Immediate Revoke; hash comparison is constant-time; keys are rate-limited and can carry an expiry.
Brute-force credential guessing Per-source and per-subject rate limiting (Security §9) + exponential backoff; repeated failures alert via Notifications.
Clock skew on token validation Bounded leeway (e.g. ±60s) on nbf/exp; beyond that, reject.

Security notes

Honors Architecture §9. Credentials are never logged and never written into DiscoveredResource, config, or audit payloads — only credential ids and outcomes are recorded. API keys are stored as salted hashes (argon2id/bcrypt), compared in constant time, and shown in plaintext once. JWTs are short-lived, signed with a key from Secrets, and validated for signature, iss, aud, and expiry on every request. OAuth flows use authorization-code + PKCE; client secrets are secret-refs. All auth traffic is TLS-only (fail closed if terminated plaintext). Sessions bind to the Principal for their lifetime so a mid-session privilege change forces re-resolution. Fail closed everywhere: an unresolvable request is unauthenticated, never anonymous-with-access.

Open questions

  • Should agent credentials be first-class API keys scoped to an AgentProfile, or a distinct credential type with its own lifecycle?
  • mTLS as an additional agent authentication provider for zero-shared-secret deployments?
  • Session store: stateless JWT only, or a server-side session table to enable instant global revocation (at a hot-path lookup cost)?
  • How to federate group/role claims from multiple IdPs into one RBAC role space without collisions?
  • Do we support step-up auth (re-authentication) for high-risk control-plane mutations?

Milestone

Delivered in Phase 3 (Secure — data-plane hardening). Thin slice first: API-key authentication end to end — issue a key, present it on the MCP handshake and the control API, resolve a Principal, and audit the decision — so two differently keyed agents can be told apart. OIDC/OAuth + JWT sessions follow within the phase; LDAP/AD/SAML SSO are deferred (Roadmap "not early"). Contributes to the Phase 3 exit proof: two agents with different identities connect and are distinguished before RBAC scopes them.