Skip to content

OAuth 2.0 / OIDC SSO Authentication Guide

This guide documents the standardized OAuth 2.0 / OIDC authentication architecture used across the entire agent-packages ecosystem. It replaces personal access tokens (PATs) with enterprise SSO-based identity propagation.

Overview

Every MCP server in the ecosystem supports three authentication patterns:

Pattern When to Use Example Agents
Full Delegation Downstream API supports OIDC token exchange (RFC 8693) GitLab, GitHub, ServiceNow, Atlassian*, EARs*, Ansible Tower*
Hybrid Agent has its own auth flow (e.g. MSAL) alongside OIDC Microsoft Agent
Identity Passthrough Downstream API uses API keys only; SSO secures MCP layer Langfuse

* = Can also use identity passthrough if the downstream service doesn't support token exchange.

Architecture

Authentication Flow

sequenceDiagram
    participant User as User / SI Client
    participant IdP as OIDC Identity Provider
    participant MCP as FastMCP Server<br/>(any agent-mcp)
    participant MW as UserTokenMiddleware
    participant Auth as auth.py<br/>(get_client)
    participant Helper as delegated_auth.py<br/>(get_delegated_token)
    participant API as Downstream API<br/>(Jira, GitLab, etc.)

    Note over User,API: Phase 1: Client authenticates with IdP
    User->>IdP: Login (SSO / device-code / client-credentials)
    IdP-->>User: Access Token (JWT)

    Note over User,API: Phase 2: Client calls MCP with IdP token
    User->>MCP: MCP Tool Call + Bearer [IdP-token]
    MCP->>MCP: OIDCProxy validates token against IdP JWKS
    MCP->>MW: Pass validated request
    MW->>MW: Bind verified token + claims → request ContextVars

    Note over User,API: Phase 3: Tool handler creates API client
    MCP->>Auth: get_client(config)
    Auth->>Helper: get_delegated_token(audience, scopes)
    Helper->>IdP: RFC 8693 Token Exchange<br/>(subject_token → downstream token)
    IdP-->>Helper: Delegated Access Token
    Helper-->>Auth: downstream_token
    Auth->>API: API call with delegated token
    API-->>MCP: Response
    MCP-->>User: Tool result

Component Architecture

graph TB
    subgraph "agent-utilities — shared infrastructure"
        SF["server_factory.py<br/>OIDCProxy + CLI parser"]
        MW["middlewares.py<br/>UserTokenMiddleware"]
        DA["delegated_auth.py<br/>get_delegated_token<br/>get_3lo_authorization_url<br/>exchange_authorization_code"]
        CH["context_helpers.py<br/>progress, elicitation, logging,<br/>state, and sampling"]
        CFG["core/config.py<br/>AgentConfig with OIDC fields<br/>(XDG config.json)"]
    end

    subgraph "Full Delegation Agents"
        GL["OS-5.1: gitlab-api/auth.py"]
        GH["OS-5.1: github-agent/auth.py"]
        SN["OS-5.1: servicenow-api/auth.py"]
        AT["OS-5.1: atlassian-agent/auth.py"]
        LX["KG-2.6: leanix-agent/auth.py"]
        AN["OS-5.1: ansible-tower-mcp/auth.py"]
    end

    subgraph "Hybrid Agent"
        MS["microsoft-agent/auth.py<br/>(MSAL + OIDC)"]
    end

    subgraph "Identity Passthrough"
        LF["langfuse-agent/auth.py<br/>(API keys + audit logging)"]
    end

    CFG --> SF
    SF --> MW
    MW --> DA
    CH -.->|"tool context utilities"| GL & GH & SN & AT & LX & AN & MS

    DA --> GL & GH & SN & AT & LX & AN & MS
    MW -.->|"user identity audit"| LF

Two-Layer Auth Architecture

Authentication is split into two independent layers:

Layer 1: MCP Transport Security (already built into agent-utilities)

The MCP server itself is protected by an OIDC/OAuth proxy. This layer validates that the caller has a valid IdP-issued token before any tool is executed.

  • Configured via --auth-type oidc-proxy on the MCP CLI
  • Uses FastMCP's built-in OIDCProxy / OAuthProxy / JWTVerifier
  • UserTokenMiddleware binds the checked Bearer token and claims to request-scoped context variables

Layer 2: Downstream API Delegation (this module)

Each agent's auth.py reads the stored user token and performs an RFC 8693 Token Exchange to obtain a service-specific token for the downstream API.

  • Centralized in agent_utilities.mcp.delegated_auth
  • Shared helper get_delegated_token() eliminates code duplication
  • Uses an explicitly configured typed credential reference when delegation is disabled

Quick Start

1. Set Environment Variables

# Required for OIDC delegation
export AUTH_TYPE=oidc-proxy
export OIDC_CONFIG_URL=https://your-idp.example.com/.well-known/openid-configuration
export OIDC_CLIENT_ID=your-client-id
export OIDC_CLIENT_SECRET_REF=vault://platform/oidc#client_secret
export ENABLE_DELEGATION=True
export AUDIENCE=https://api.downstream-service.com
export DELEGATED_SCOPES="api read write"

Or add them to the XDG config file at ~/.config/agent-utilities/knuckles-team/config.json:

{
    "oidc_config_url": "https://your-idp.example.com/.well-known/openid-configuration",
    "oidc_client_id": "your-client-id",
    "oidc_client_secret_ref": "vault://platform/oidc#client_secret",
    "enable_delegation": true,
    "delegation_audience": "https://api.downstream-service.com",
    "delegated_scopes": "api read write"
}

2. Start the MCP Server

# Any agent — the auth flags are handled by create_mcp_server()
python -m gitlab_api.mcp_server \
  --transport streamable-http \
  --auth-type oidc-proxy \
  --oidc-config-url $OIDC_CONFIG_URL \
  --oidc-client-id $OIDC_CLIENT_ID \
  --oidc-client-secret-ref "$OIDC_CLIENT_SECRET_REF" \
  --enable-delegation

3. Call Tools with Bearer Token

# The client includes the IdP-issued token
curl -X POST http://localhost:8000/mcp/tools/call \
  -H "Authorization: Bearer <your-idp-token>" \
  -H "Content-Type: application/json" \
  -d '{"name": "gitlab_projects", "arguments": {"action": "list_projects"}}'

Environment Variable Reference

Variable Required Default Description
AUTH_TYPE No none Auth type: none, oidc-proxy, oauth-proxy, jwt, remote
OIDC_CONFIG_URL For OIDC — OIDC discovery URL (.well-known/openid-configuration)
OIDC_CLIENT_ID For OIDC — OAuth 2.0 client ID from the operator's IdP
OIDC_CLIENT_SECRET_REF For OIDC — env://, vault://, or secret:// reference resolved only at runtime
ENABLE_DELEGATION No False Enable RFC 8693 token exchange for downstream APIs
AUDIENCE For delegation — Target audience for the delegated token
DELEGATED_SCOPES No api Space-separated scopes for the delegated token

Provider Credentials When Delegation Is Not Used

Provider endpoints and non-secret client identifiers remain ordinary configuration. Every token, password, cookie, and OAuth client secret is a typed SOURCE_CREDENTIALS descriptor whose secret-bearing fields contain only env://, vault://, or secret:// references:

Credential kind Typical providers Reference-bearing fields
API key / bearer GitLab, GitHub, LeanIX secret
Basic auth ServiceNow, Ansible Tower username_secret, password_secret
Cookie session Browser-backed providers secret
OAuth 2.0 Atlassian, Microsoft, Langfuse secret, refresh_token_secret, client_secret_secret

Raw token/password environment credentials are not a supported path. A connector that cannot resolve its selected credential reference fails closed.

Auth Patterns in Detail

Full Delegation (RFC 8693 Token Exchange)

Used when the downstream API accepts OIDC tokens or supports the Token Exchange grant type.

# In any agent's auth.py:
from agent_utilities.mcp.delegated_auth import (
    get_delegated_token,
    get_user_identity,
    is_delegation_enabled,
)

def get_client():
    if is_delegation_enabled():
        token = get_delegated_token(
            audience="https://gitlab.example.com",
            scopes="api read_repository",
        )
        return Api(url=instance, token=token)

    # Non-delegated mode uses the typed provider; its descriptor contains only
    # a runtime secret reference and materializes the header in memory.
    from agent_utilities.security.credential_provider import get_credential_provider

    material = get_credential_provider().get("gitlab").materialize()
    if not material.headers:
        raise RuntimeError("GitLab credential reference is unavailable")
    return Api(url=instance, headers=material.headers)

Three-Legged OAuth (3LO)

Used for services like Atlassian Cloud that require explicit user consent via the Authorization Code Grant flow.

from agent_utilities.mcp.delegated_auth import (
    get_3lo_authorization_url,
    exchange_authorization_code,
    refresh_access_token,
)
from agent_utilities.security.secrets_client import create_secrets_client

secrets = create_secrets_client()
client_secret = secrets.resolve_ref(
    "vault://platform/atlassian#client_secret"
)
if not client_secret:
    raise RuntimeError("Atlassian client-secret reference is unavailable")

# Step 1: Build authorization URL
auth_url = get_3lo_authorization_url(
    authorization_endpoint="https://auth.atlassian.com/authorize",
    client_id="your-app-client-id",
    redirect_uri="http://localhost:8080/callback",
    scopes=["read:jira-work", "write:jira-work"],
)

# Step 2: User visits auth_url, consents, gets redirected with ?code=...
# Step 3: Exchange authorization code for tokens
tokens = exchange_authorization_code(
    token_endpoint="https://auth.atlassian.com/oauth/token",
    client_id="your-app-client-id",
    client_secret=client_secret,
    code=authorization_code,
    redirect_uri="http://localhost:8080/callback",
)

# Step 4: Use access_token, refresh when expired
new_tokens = refresh_access_token(
    token_endpoint="https://auth.atlassian.com/oauth/token",
    client_id="your-app-client-id",
    client_secret=client_secret,
    refresh_token=tokens["refresh_token"],
)

Identity Passthrough

Used when the downstream API does not support OIDC at all (e.g. API key authentication only). SSO still secures the MCP server.

from agent_utilities.security.credential_provider import get_credential_provider

def get_client():
    # The provider resolves the descriptor's secret reference in memory. Do not
    # log its value or attach user identity to the credential event.
    material = get_credential_provider().get("service").materialize()
    if not material.headers:
        raise RuntimeError("Service credential reference is unavailable")
    return ServiceClient(headers=material.headers)

Hybrid (MSAL + OIDC)

Used by Microsoft Agent, which has its own MSAL device-code flow alongside standard OIDC delegation.

from agent_utilities.mcp.delegated_auth import (
    get_delegated_token,
    get_user_token,
    is_delegation_enabled,
)

async def get_client():
    # Priority 1: OIDC delegation
    if is_delegation_enabled():
        token = get_delegated_token(audience="https://graph.microsoft.com")
        auth.access_token = token
        return MicrosoftGraphApi(auth)

    # Priority 2: MSAL cached token
    token = auth.get_token()
    if token:
        return MicrosoftGraphApi(auth)

    # Priority 3: MCP user token passthrough
    user_token = get_user_token()
    if user_token:
        auth.access_token = user_token
        return MicrosoftGraphApi(auth)

Troubleshooting

"No user token available for delegation"

Cause: The MCP server received a request without a Bearer token, or UserTokenMiddleware is not configured.

Fix: Ensure: 1. The MCP server is started with --auth-type oidc-proxy 2. The client sends Authorization: Bearer <token> in the request 3. --enable-delegation is passed at MCP startup

"No token_endpoint configured"

Cause: The OIDC discovery URL wasn't resolved at startup.

Fix: Set OIDC_CONFIG_URL to the operator's IdP's well-known endpoint:

export OIDC_CONFIG_URL=https://your-idp.example.com/.well-known/openid-configuration

"Token exchange failed (HTTP 400/401)"

Cause: The IdP rejected the token exchange request.

Fix: Check: 1. OIDC_CLIENT_ID is correct and OIDC_CLIENT_SECRET_REF resolves at runtime 2. The client is authorized for the token-exchange grant type in the operator's IdP 3. The AUDIENCE matches the service registered in the operator's IdP 4. The DELEGATED_SCOPES are valid for the target service

"OIDC delegation failed"

Info: Delegation and source credentials fail closed. Correct the IdP configuration or configure an explicitly selected SOURCE_CREDENTIALS descriptor containing runtime references; the agent does not retry with raw environment credentials.


Vault & OpenBao Integration

The secrets engine (agent_utilities.security.secrets_client) integrates with HashiCorp Vault and OpenBao using the same OIDC identity infrastructure documented above. This means the SSO user token that protects the MCP server can also be used to authenticate to Vault / OpenBao without retaining a static Vault token.

How It Works

sequenceDiagram
    participant User as User / SI Agent
    participant MW as UserTokenMiddleware
    participant SC as SecretsClient
    participant VB as VaultBackend
    participant Vault as HashiCorp Vault
    participant IdP as OIDC Identity Provider

    Note over User,IdP: Phase 1: User authenticates to MCP (already done)
    User->>MW: Bearer [IdP-token]
    MW->>MW: Bind verified token + claims in request ContextVars

    Note over User,IdP: Phase 2: Agent needs a secret
    SC->>VB: get("gitlab/token")
    VB->>VB: Check: Do I have a valid Vault token?

    alt No Vault token — OIDC login
        VB->>MW: get_user_token()
        MW-->>VB: IdP JWT
        VB->>Vault: POST /auth/{auth_mount}/login (role, jwt=IdP_token)
        Vault->>IdP: Validate JWT via JWKS
        IdP-->>Vault: Valid
        Vault-->>VB: Vault Token (scoped to user policies)
        VB->>VB: Cache Vault token (TTL-aware)
    end

    VB->>Vault: GET /secret/data/{path_prefix}/gitlab/token
    Vault-->>VB: Secret value
    VB-->>SC: Resolved secret value

Config ↔ Path Mapping

The VaultBackend builds full secret paths from three components:

vault_mount:        secret          ← KV v2 secrets engine mount point
vault_path_prefix:  agents/mcp/     ← where in the mount to scope secrets
key:                gitlab/token    ← the key passed to get()/set()

Full path: secret/data/agents/mcp/gitlab/token
                │          │            │
                │          │            └── key passed to client.get()
                │          └── VAULT_PATH_PREFIX
                └── SECRETS_VAULT_MOUNT

The auth method mount is separate from the secrets path:

vault_auth_mount:   oidc            ← auth method mount (custom endpoint)
vault_role:         agent-reader    ← role bound to OIDC claims

Auth endpoint: POST /auth/oidc/login
               (supports any custom mount: 'jwt', 'my-okta-auth', etc.)

Authentication Strategies

The documented profiles use workload identity and do not retain a static Vault credential:

Method Use Case Required Config
OIDC/JWT User-scoped access via SSO token VAULT_ROLE, VAULT_AUTH_MOUNT
Kubernetes Workload-scoped pod access VAULT_ROLE, projected service-account token

Non-secret Vault Environment Variables

Variable Required Default Description
SECRETS_BACKEND No engine Set to vault to enable Vault
SECRETS_VAULT_URL For vault http://127.0.0.1:8200 Vault cluster URL
SECRETS_VAULT_MOUNT No secret KV v2 mount point
VAULT_AUTH_METHOD No auto auto, oidc, or kubernetes in documented profiles
VAULT_AUTH_MOUNT No jwt Auth method mount path (custom endpoints supported)
VAULT_ROLE For OIDC/K8s default Vault role name
VAULT_PATH_PREFIX No — Path prefix within the mount
VAULT_K8S_SA_TOKEN_PATH For K8s /var/run/secrets/kubernetes.io/serviceaccount/token SA token path

Usage Examples

# Environment
export SECRETS_BACKEND=vault
export SECRETS_VAULT_URL=https://vault.example.com
export VAULT_AUTH_METHOD=oidc
export VAULT_AUTH_MOUNT=oidc               # or 'jwt', 'my-okta-auth', etc.
export VAULT_ROLE=agent-reader
export VAULT_PATH_PREFIX=agents/mcp/
from agent_utilities.security.secrets_client import create_secrets_client

client = create_secrets_client()
# When called inside an MCP tool handler, the user's SSO token is
# automatically used to authenticate to Vault.
gitlab_token = client.get("gitlab/token")
# → reads: secret/data/agents/mcp/gitlab/token

CLI Usage

# Read a secret with OIDC auth and path prefix
secret-manager --backend vault \
  --vault-auth oidc \
  --vault-auth-mount my-okta-auth \
  --vault-role agent-reader \
  --vault-path-prefix agents/mcp/ \
  get gitlab/token

Vault Admin Setup (Prerequisites)

For OIDC/JWT authentication to work, the Vault server must have the auth method enabled and configured. This is a one-time setup performed by the Vault admin:

# 1. Enable the OIDC auth method (custom mount path supported)
vault auth enable -path=oidc oidc

# 2. Configure it to trust your IdP
vault write auth/oidc/config \
  oidc_discovery_url="https://your-idp.example.com" \
  oidc_client_id="vault-client-id" \
  default_role="agent-reader"

# For a confidential IdP client, an approved secret-aware provisioner resolves
# vault://platform/vault-oidc#client_secret and sends the value to the Vault API
# in memory. Never place the resolved value in this command or a committed file.

# 3. Create a role that maps OIDC claims to Vault policies
vault write auth/oidc/role/agent-reader \
  role_type="jwt" \
  bound_audiences="vault-client-id" \
  user_claim="sub" \
  groups_claim="groups" \
  policies="agent-secrets-read" \
  ttl="1h"

# 4. Create the policy granting KV v2 read access
vault policy write agent-secrets-read - <<EOF
path "secret/data/agents/mcp/*" {
  capabilities = ["read", "list"]
}
EOF

XDG Configuration

All Vault settings can be persisted in the XDG config file:

{
    "vault_url": "https://vault.example.com",
    "vault_mount": "secret",
    "vault_auth_method": "oidc",
    "vault_auth_mount": "oidc",
    "vault_role": "agent-reader",
    "vault_path_prefix": "agents/mcp/"
}

🔗 Generalized Authentication & Credentials Topology

The following diagram provides a complete system-wide visualization of the unified authentication flows across the entire agent-packages and agent-utilities ecosystem, illustrating the OIDC Proxy verification layer, RFC 8693 Token Delegation, Hybrid MSAL auth, Vault/OpenBao dynamic credential extraction, and the remote loopback port-forwarding flow:

graph TD
    classDef default fill:#1e1e24,stroke:#3a3a4a,stroke-width:1px,color:#d8d8d8;
    classDef client fill:#0f3b5f,stroke:#20639b,stroke-width:1.5px,color:#ffffff;
    classDef gateway fill:#4d2c5e,stroke:#7b4f91,stroke-width:1.5px,color:#ffffff;
    classDef auth fill:#1d5c3f,stroke:#32a873,stroke-width:1.5px,color:#ffffff;
    classDef backend fill:#5f2f20,stroke:#ba4a00,stroke-width:1.5px,color:#ffffff;

    subgraph UserInterface ["User Space"]
        User(["User / SI Developer"])
        Browser["Local Web Browser<br/>(Local Machine)"]
    end

    subgraph IDE_Forwarding ["Local-to-Remote Port Forwarding"]
        FWD["Local Port Forward (127.0.0.1:56121)"]
    end

    subgraph AgentWorkspace ["Secure Remote Workspace (Container / VM)"]
        subgraph MCP_Layer ["1. MCP Transport & Verification"]
            Proxy["OIDCProxy / OAuthProxy / JWTVerifier<br/>(FastMCP Server)"]
            MW["UserTokenMiddleware<br/>(Binds verified JWT to request ContextVars)"]
        end

        subgraph AuthCore ["2. agent-utilities Shared Auth Engine"]
            DA["delegated_auth.py<br/>(Token Exchange / OIDC)"]
            SC["SecretsClient / VaultBackend<br/>(Vault / OpenBao)"]
            Loopback["Callback Server<br/>(Loopback / OIDC Auth Flow)<br/>Port: 56121"]
        end

        subgraph DownstreamClients ["3. Specialized Agent Clients"]
            DelegatedAgent["Full Delegation Agents<br/>(GitLab, GitHub, Jira, ServiceNow)"]
            PassthroughAgent["Passthrough Agents<br/>(Langfuse, etc.)"]
            HybridAgent["Hybrid Microsoft Agent<br/>(MSAL / OIDC)"]
            XAgent["x-search-agent / x-ingestion-team<br/>(xAI Authentication)"]
        end
    end

    subgraph IdentityProvider ["Identity Providers (IdP)"]
        IdP["SSO Identity Provider<br/>(Okta, Entra ID, Keycloak)"]
        XAI["xAI OAuth Provider<br/>(Live X Index via xAI Auth)"]
    end

    subgraph ExternalBackends ["Secure Secrets & Services"]
        Vault["Vault / OpenBao Cluster<br/>(KV v2 Secrets Engine)"]
        APIs["Target APIs & Cloud Services<br/>(GitLab, Microsoft Graph, Langfuse)"]
    end

    %% Flows
    User -->|1. Request / Call Tool| Proxy
    User -->|2. Authorize Flow| Browser
    Browser -->|3. Redirect to 127.0.0.1:56121| FWD
    FWD -->|4. Forward Traffic| Loopback
    Loopback -->|5. Handshake & Exchange Code| XAI
    XAI -->|6. Auth Token| Loopback
    Loopback -->|7. Seed Credentials| XAgent

    Proxy -->|Pass validated JWT| MW
    MW -->|Thread-Local Token| DA
    MW -->|Thread-Local Token| SC

    %% Vault / OpenBao Auth
    SC -->|JWT Authentication| Vault
    Vault -->|Verify JWT via JWKS| IdP
    Vault -->|Issue scoped token & secrets| SC

    %% Delegation Auth
    DA -->|RFC 8693 Token Exchange| IdP
    IdP -->|Delegated Access Token| DA

    %% Specialized Agents routing
    DA -->|Downstream Token| DelegatedAgent
    SC -->|Fetched secrets| PassthroughAgent
    DA & SC -->|MSAL / OIDC| HybridAgent

    %% Target Calls
    DelegatedAgent -->|Authenticated Requests| APIs
    PassthroughAgent -->|Secured payload| APIs
    HybridAgent -->|Graph API requests| APIs
    XAgent -->|Ingest Post Workflow| XAI

    class User,Browser client;
    class Proxy,MW,Loopback gateway;
    class DA,SC,FWD auth;
    class DelegatedAgent,PassthroughAgent,HybridAgent,XAgent backend;

This file lives at ~/.config/agent-utilities/knuckles-team/config.json (following XDG Base Directory Specification).