SSO & Identity Guide

Enterprise Single Sign-On
for AI Requests

Smartflow lets your users authenticate with the identity system they already have — Entra ID, SAML, Kerberos, or a corporate proxy — and carries their identity, groups, and role into every AI request. No extra accounts. No API key distribution.

Entra ID / OIDC SAML 2.0 / ADFS Kerberos SPNEGO Proxy Headers Group Sync
How SSO Passthrough Works

When a user calls the Smartflow proxy, they include whatever token their corporate identity provider already issued — an OIDC id_token from Entra ID, a SAML assertion from ADFS, a Kerberos SPNEGO ticket from AD, or simply trusted headers injected by your existing reverse proxy. Smartflow validates the token cryptographically, extracts the user's identity and group membership, and issues a short-lived Smartflow-internal JWT. All subsequent AI calls carry that JWT, allowing the policy engine to enforce per-user and per-team rules without the user ever registering a Smartflow account.

User's app → Authorization: Bearer <Entra ID id_token> POST /api/auth/sso/signin ─────────────────────────────────────────────────────────────────────► Smartflow APIvalidates OIDC sig, checks iss/aud/exp extracts username, email, groups[ ] maps groups → admin/user role◄───────────────────────────────────────────────────────────────────── { token: "sf-jwt-...", expires_in: 3600 } ─────────────────────────────────────────────────────────────────────► POST /v1/chat/completions Authorization: Bearer <sf-jwt> policy engine reads username + groups enforces team budget, model allow-list applies guardrails for this user's team

The Smartflow JWT contains sub (short username), email, groups (comma-separated), department, roles (admin or user), and auth_method so every trace and log entry is attributed to a real person, not a shared key.

Choosing the Right SSO Method
Method Best for Client sends Config needed Complexity
OIDC / Entra ID Azure AD / Microsoft 365 orgs, Okta, any OIDC IdP Authorization: Bearer <id_token> Tenant ID, Client ID, JWKS URI Low
SAML 2.0 ADFS, legacy enterprise, Entra SAML apps X-SAML-Assertion: <b64> IdP metadata URL or signing cert Medium
Kerberos SPNEGO On-prem AD, intranet deployments Authorization: Negotiate <spnego> Service principal, keytab path Medium
Trusted Proxy Headers Nginx/F5/Zscaler already handling SSO upstream x-remote-user: jsmith Trusted IP CIDR list Zero
Method 1 — Entra ID / OIDC (Most Common)
1
Microsoft Entra ID (Azure AD) + OIDC
Works with any OIDC-compliant provider: Entra ID, Okta, Auth0, PingFederate, Keycloak
OIDC

This is the recommended method for most enterprise customers. Your Entra ID (Azure AD) tenant issues an id_token JWT when a user signs in to your app. You forward that token to Smartflow, which validates the signature against the Entra JWKS endpoint, checks the iss, aud, and exp claims, then extracts the user's identity and group memberships.

What Smartflow extracts

Username from emailupnpreferred_username (in priority order, @ suffix stripped). Group GUIDs or CNs from the groups claim. Display name from name. Department from department. The aud and iss claims are strictly validated.

Step 1 — Register a Smartflow app in Entra ID

In the Azure Portal → App registrations → New registration:

  • Name: Smartflow Proxy
  • Redirect URI: https://<your-domain>/api/auth/sso/callback
  • Under Token configuration → add optional claim groups (security groups)
  • Note the Application (client) ID and Directory (tenant) ID

Step 2 — Configure via the Dashboard UI

Navigate to your Smartflow dashboard → SSO Configuration (or go directly to /dashboard/sso_config.html):

Dashboard UI — SSO Config Page

Select Microsoft Entra ID from the Identity Provider dropdown. Enter your Tenant ID (e.g. contoso.onmicrosoft.com or the GUID). Enter your Client ID from the app registration. The JWKS URI auto-fills to https://login.microsoftonline.com/<tenant>/discovery/v2.0/keys. Set Redirect URI to https://<your-smartflow-domain>/api/auth/sso/callback. Click Save Configuration, then Test Connection to verify.

Step 2 (alternative) — Configure via CLI / curl

# POST your SSO config to the API (use your admin key)
curl -X POST https://<your-domain>/api/auth/sso/config \
  -H "Authorization: Bearer <admin-key>" \
  -H "Content-Type: application/json" \
  -d '{
    "tenant_id":  "contoso.onmicrosoft.com",
    "client_id":  "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "jwks_uri":   "https://login.microsoftonline.com/contoso.onmicrosoft.com/discovery/v2.0/keys"
  }'

# Verify the config was stored
curl https://<your-domain>/api/auth/sso/config \
  -H "Authorization: Bearer <admin-key>"

Step 3 — Exchange a token

# Your app gets an id_token from Entra ID via MSAL/ADAL, then:
curl -X POST https://<your-domain>/api/auth/sso/signin \
  -H "Authorization: Bearer <entra-id-token>"

# Response:
{
  "token":       "eyJhbGci...",   // Smartflow JWT — use for all AI calls
  "expires_in":  3600,
  "token_type":  "Bearer",
  "user": {
    "username":     "jsmith",
    "email":        "jsmith@contoso.com",
    "groups":       ["Engineering", "AI-Users"],
    "auth_method":  "oidc:EntraID"
  }
}

Step 4 — Use the Smartflow JWT for AI calls

# All AI requests now go through the policy engine with jsmith's identity
curl -X POST https://<your-domain>/v1/chat/completions \
  -H "Authorization: Bearer <smartflow-jwt>" \
  -H "Content-Type: application/json" \
  -d '{"model":"gpt-4o","messages":[{"role":"user","content":"Hello"}]}'
What this unlocks

Every AI call is attributed to jsmith and his groups. MAESTRO policy engine can enforce team-specific model allow-lists, token budgets, and guardrail profiles. Audit logs show real usernames. If Engineering is in the admin_groups list, jsmith gets admin access automatically.

Browser Login Flow (Dashboard) — for human users signing into the Smartflow dashboard:

User visits /dashboard/sso_login.html
Sees the Sign in with Microsoft button. Clicking it calls GET /api/auth/sso/login which builds the Entra ID authorize URL using the stored config and redirects the browser.
Entra ID authenticates the user
Microsoft handles MFA, Conditional Access policies, and issues an authorization code back to /api/auth/sso/callback.
Smartflow exchanges the code
GET /api/auth/sso/callback?code=... exchanges the authorization code for an id_token using OIDC_CLIENT_SECRET, validates it, and mints the Smartflow JWT.
JWT stored in browser, dashboard loads
The Smartflow JWT is returned to the browser and stored in localStorage. All subsequent dashboard API calls use it.

Required environment variables on the container (for the auth-code flow only):

OIDC_CLIENT_SECRET=<your-app-secret-from-azure>
OIDC_REDIRECT_URI=https://<your-domain>/api/auth/sso/callback
Method 2 — SAML 2.0 / ADFS
2
SAML 2.0 — ADFS or Entra ID SAML App
For legacy enterprise deployments already running ADFS, or Entra ID configured in SAML mode
SAML

If your organisation uses Active Directory Federation Services (ADFS) or has a SAML enterprise app in Entra ID, your app can POST the base64-encoded SAML assertion directly to Smartflow. Smartflow fetches the IdP signing certificate from the federation metadata XML, verifies the RSA-SHA256 signature, checks NotOnOrAfter expiry, and extracts the NameID, email, groups, and department from well-known SAML attribute URNs.

Configure via CLI

# ADFS federation metadata URL
curl -X POST https://<your-domain>/api/auth/sso/config \
  -H "Authorization: Bearer <admin-key>" \
  -H "Content-Type: application/json" \
  -d '{
    "saml_idp_metadata_url": "https://adfs.contoso.com/FederationMetadata/2007-06/FederationMetadata.xml",
    "saml_sp_entity_id":    "https://smartflow.contoso.com"
  }'

Exchange a SAML assertion

# Get a SAML assertion from ADFS (your SAML SP logic does this)
# Then pass it to Smartflow in the X-SAML-Assertion header:
curl -X POST https://<your-domain>/api/auth/sso/signin \
  -H "X-SAML-Assertion: <base64-encoded-saml-assertion>"
SAML attribute mapping

Smartflow recognises standard SAML attribute URNs automatically: ...claims/emailaddress, ...claims/upn, displayName, memberOf, department, and the Microsoft groups claim ...ws/2008/06/identity/claims/groups. Any attribute containing "group" or "role" in its name is also captured.

Method 3 — Kerberos SPNEGO (On-Prem AD)
3
Kerberos / SPNEGO — On-Premises Active Directory
For intranet deployments where Windows clients have Kerberos tickets from domain logon
KERBEROS

Domain-joined Windows machines already hold a Kerberos Ticket Granting Ticket (TGT) from AD logon. When a Kerberos-aware client application calls Smartflow, it presents a SPNEGO token in Authorization: Negotiate <token>. Smartflow decodes the ASN.1 GSSAPI wrapper, extracts the client principal (e.g. jsmith@CONTOSO.COM), strips the realm, and returns a Smartflow JWT. An optional LDAP enrichment step can add group memberships and department.

Configure via config.yaml (server-side)

sso:
  kerberos:
    enabled:            true
    service_principal:  "HTTP/smartflow.contoso.com@CONTOSO.COM"
    keytab_path:        "/etc/smartflow/smartflow.keytab"
  admin_groups:
    - "Domain Admins"
    - "Smartflow-Admins"

Client call (browser or app with SSPI/GSSAPI)

# Windows PowerShell — browser does this automatically on intranet
$token = [System.Convert]::ToBase64String(
    (New-Object System.Net.Http.HttpClient).DefaultRequestHeaders.GetValues("Authorization")
)
curl -X POST https://smartflow.contoso.com/api/auth/sso/signin `
  -H "Authorization: Negotiate $token"
Zero-prompt SSO on intranet

Once configured, domain users on the corporate network authenticate silently — no username/password prompt. The browser or client app negotiates Kerberos transparently. After the Smartflow JWT is issued, group enrichment via LDAP attributes the request to the correct team's policy profile.

Method 4 — Trusted Proxy Headers (Zero Config)
4
Trusted Proxy Header Passthrough
For deployments where Nginx, F5, Zscaler, or AWS ALB already handles SSO upstream
PROXY

If your infrastructure already has an SSO-aware reverse proxy sitting in front of Smartflow (e.g. Nginx with auth_request, an F5 APM, Zscaler App Connector, or AWS ALB with OIDC), configure Smartflow to trust identity headers injected by that proxy. No tokens are validated by Smartflow — it trusts the upstream proxy completely, but only from IPs in your trusted CIDR list.

Configure via config.yaml (or environment variable)

sso:
  trusted_proxy_cidrs:
    - "10.0.0.0/8"        # internal corporate network
    - "172.16.0.0/12"     # private range
    - "192.168.0.0/16"    # private range

Your upstream proxy injects these headers

# Nginx example — after auth_request validates the session cookie:
proxy_set_header  x-smartflow-user-id         $authenticated_user;
proxy_set_header  x-smartflow-user-email      $user_email;
proxy_set_header  x-smartflow-user-groups     $user_groups;     # comma-separated
proxy_set_header  x-smartflow-user-department  $department;

Supported header names (checked in priority order)

# User identity (any of these)
x-smartflow-user-id  |  x-remote-user  |  x-forwarded-user  |  x-authenticated-user  |  x-username

# Supplementary attributes
x-smartflow-user-email       # or x-user-email
x-smartflow-user-groups      # comma-separated CNs — or x-user-groups
x-smartflow-user-department  # or x-user-department
x-user-name                  # or x-display-name
Security note

Headers from IPs not in trusted_proxy_cidrs are silently stripped and ignored — clients cannot impersonate other users by injecting these headers themselves. Keep this CIDR list tight. If the list is empty, the proxy-header path is disabled entirely.

Group-to-Role Mapping & Team Auto-Provisioning
5
Admin Group Detection & Role Assignment
Groups from the IdP map automatically to Smartflow roles
ROLES

After any SSO validation path succeeds, Smartflow inspects the user's group list against the configured admin_groups patterns. Members of an admin group receive roles ["admin","user"]; everyone else gets ["user"]. Patterns support * wildcards.

Configure admin groups via the Dashboard UI

In SSO ConfigurationAuto-provisioned Teams defaults section → enter the group names in the Admin Groups field (one per line, wildcards supported).

Configure via CLI

# Override default admin groups [Smartflow-Admins, IT-Admins, Domain Admins]
curl -X POST https://<your-domain>/api/auth/sso/config \
  -H "Authorization: Bearer <admin-key>" \
  -H "Content-Type: application/json" \
  -d '{
    "tenant_id":    "contoso.onmicrosoft.com",
    "client_id":    "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "admin_groups": ["IT-Admins", "AI-Platform-Admins", "*-SuperAdmin"]
  }'

Default admin groups (if not overridden):

"Smartflow-Admins"  |  "IT-Admins"  |  "Domain Admins"
Group sync on every sign-in

When Sync groups on every login is enabled, the user's group membership is re-evaluated every time they exchange a token. If someone is removed from an AD group, their next sign-in reflects the new role — no manual admin action needed. The Traces dashboard shows the auth_method and groups for every request.

Real-World Use Case Examples
A
Use Case: Cursor IDE / VS Code with Entra ID
Developers use their Microsoft login — no API keys managed per-dev
COMMON

Your company has 200 developers on Entra ID. You want all Cursor or VS Code Copilot traffic routed through Smartflow, with each developer's AI usage tracked and their team's budget enforced — without issuing 200 API keys.

Setup

Register app in Entra ID, configure Smartflow SSO
Dashboard → SSO Config → enter Tenant ID + Client ID → Save. Set OIDC_CLIENT_SECRET and OIDC_REDIRECT_URI on the container.
Developer signs in to the Smartflow SSO login page
Visit /dashboard/sso_login.html → click Sign in with Microsoft → Microsoft MFA completes → browser redirected to /api/auth/sso/callback → Smartflow JWT returned.
Developer configures Cursor with their Smartflow JWT
In Cursor settings: OpenAI Base URL = https://<domain>/v1, API Key = their Smartflow JWT. All completions flow through Smartflow under their identity.
Policy engine enforces team rules
Developers in Engineering get GPT-4 + Claude access. Contractors in External-AI-Limited are restricted to GPT-4o-mini with a lower monthly token budget. All enforced automatically from their AD group.
B
Use Case: Internal Web App with Existing SSO Proxy
Your app already sits behind Nginx/F5 with SSO — zero code changes needed
COMMON

You have an internal web application that calls an LLM API. The app sits behind an Nginx reverse proxy that already validates SAML sessions and injects x-remote-user. You want AI calls attributed to real users without changing the app at all.

Nginx config snippet

# Nginx already handles auth_request — add these lines to your location block
location /v1/ {
    auth_request     /auth-validate;
    auth_request_set $authed_user $upstream_http_x_auth_user;

    proxy_pass       http://smartflow:7775;
    proxy_set_header x-remote-user   $authed_user;
    proxy_set_header x-user-groups   $authed_groups;   # set by your auth module
}

# Smartflow trusts this Nginx IP — config.yaml:
sso:
  trusted_proxy_cidrs: ["10.1.0.0/16"]   # IP of Nginx

No changes to the app. Nginx injects the headers → Smartflow reads them → every AI call is logged under the real user's name with their team's policy applied.

What this unlocks

Zero application code changes. Users are attributed instantly. Full audit trail in Traces dashboard. Per-team model restrictions and budget controls activate based on the groups Nginx passes through.

C
Use Case: Python / Node SDK with OIDC Token Exchange
Backend service authenticates once, reuses the Smartflow JWT until expiry
COMMON

A Python microservice runs as a service principal in Entra ID. It uses the client-credentials flow to get an access token, then exchanges it for a Smartflow JWT once per hour and reuses it for all LLM calls during that period.

## Python example — service principal OIDC token exchange
import msal, openai, time

# 1. Acquire Entra ID token via client credentials
app = msal.ConfidentialClientApplication(
    client_id="<app-client-id>",
    client_credential="<app-secret>",
    authority="https://login.microsoftonline.com/<tenant-id>"
)
result = app.acquire_token_for_client(scopes=["api://<app-client-id>/.default"])
entra_token = result["access_token"]

# 2. Exchange for Smartflow JWT
import requests
resp = requests.post(
    "https://<your-domain>/api/auth/sso/signin",
    headers={"Authorization": f"Bearer {entra_token}"}
)
sf_token = resp.json()["token"]
expires_at = time.time() + resp.json()["expires_in"] - 60  # 1 min buffer

# 3. Use for all OpenAI-compatible calls through Smartflow
client = openai.OpenAI(
    api_key=sf_token,
    base_url="https://<your-domain>/v1"
)
response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "Summarise this contract..."}]
)
Verify Configuration & Troubleshoot

Check current SSO config

curl https://<your-domain>/api/auth/sso/config \
  -H "Authorization: Bearer <admin-key>"

# Returns: { "success": true, "config": { "tenant_id": "...", "client_id": "..." } }
# If not configured: { "success": false, "error": "Not configured" }

Generate an SSO login URL (test the config is wired correctly)

curl https://<your-domain>/api/auth/sso/login
# Returns: { "auth_url": "https://login.microsoftonline.com/..." }
# Paste the auth_url in a browser — should redirect to Microsoft login

Test token exchange manually

# Decode any Smartflow JWT to inspect claims (no secret needed for base64):
echo "eyJhbGci..." | cut -d. -f2 | base64 -d 2>/dev/null | python3 -m json.tool

# Should show: sub, username, email, groups, roles, auth_method, exp

Common issues

ErrorCauseFix
Token rejected by all OIDC providers Wrong aud or iss in token Ensure client_id matches the app registration. Check the token's aud claim matches exactly.
SAML assertion expired Clock skew or stale assertion SAML assertions are short-lived. Re-acquire a fresh one. Check server NTP sync.
Proxy headers from untrusted IP — ignored Source IP not in trusted_proxy_cidrs Add the upstream proxy IP/CIDR to trusted_proxy_cidrs in config.
JWKS fetch error JWKS URI unreachable from container Verify network egress from the container to login.microsoftonline.com. Check firewall/proxy rules.
Groups not appearing in JWT Entra ID app not configured to emit group claims In Azure Portal → App registration → Token configuration → Add optional claim → groups (Security groups). For large directories, consider Graph API enrichment.
Not configured from config endpoint SSO config not saved to Redis POST the config via the dashboard UI or curl. Verify Redis connectivity on the api-server container.