Works with Auth0 · Okta · Microsoft Entra ID · Any OIDC-compliant IDP
Channel partners need the same governed analytics your internal sales team uses. External auditors need read access to compliance data without seeing customer records. Consultants need to query project data for the three months they are engaged, then lose access the day the contract ends.
None of these people are in your corporate directory. They authenticate through their own identity providers: Auth0, Okta, Entra, whatever their organization uses. They will never have accounts on your platform.
Federation Exchange is how you bridge this gap: an external identity provider's JWT is exchanged for a scoped platform token, mapped to a role, and governed by the same Unity Catalog policies that protect your internal users. This deck takes you through exactly how the token flows, where governance is enforced, and what can go wrong.
Your channel partners, external auditors, and consultants need governed access to Databricks. They have their own IDP. They are not in your directory. Two architectural options:
Each external user is provisioned as a Databricks identity
How it works: External user authenticates → their token is exchanged for a Databricks token that represents them as an individual. current_user() returns their email. Row filters can use current_user() = email.
current_user() = real personWHERE owner = current_user())N external users map to O(roles) SPs — no Databricks provisioning
How it works: External user authenticates with their IDP → JWT exchanged for Databricks token scoped to a role-SP. Governance fires via is_member('group'). Real identity preserved in JWT claims + audit.
is_member() on SP groupscurrent_user() returns SP UUID, not individual emailis_member() groups, not current_user() = emailThe SP handles authorization. The JWT handles attribution. Together they give you governed access + full audit without provisioning a single Databricks user.
Every OIDC IDP has these three components. The names differ, the roles don't.
"Databricks exists at this address"
Declares the audience — the workspace URL. Goes into the JWT aud claim.
| Auth0 | API (Resource Server) |
| Okta | Authorization Server |
| Entra | App Reg → "Expose an API" |
// IDP config { "identifier": "https://adb-123.azuredatabricks.net" } // JWT claim produced "aud": "https://adb-123.azuredatabricks.net" // Databricks federation policy "audiences": ["https://adb-123.azuredatabricks.net"]
"The machine identity of your app server"
App server uses client_credentials grant. client_id becomes JWT sub claim.
| Auth0 | sub = {id}@clients |
| Okta | sub = {client_id} |
| Entra | sub = {app_id} |
// App server token request POST /oauth/token { "client_id": "your-m2m-client-id", "client_secret": "***", "audience": "https://adb-123...", "grant_type": "client_credentials" } // JWT sub claim produced "sub": "your-m2m-client-id@clients" // Databricks federation policy "subject": "your-m2m-client-id@clients"
"Identifies the human at the keyboard"
Browser PKCE login. IDP injects role, email, groups into ID token. Never talks to Databricks.
// You write this inside your IDP: // Auth0 → Action (Post-Login) // Okta → Token Inline Hook // Entra → Claims Mapping Policy // It runs at token issuance time. token.claims["/role"] = user.role; // → "west_sales" token.claims["/email"] = user.email; // → "sarah@partner.com" // ID token the browser receives { "email": "sarah@partner.com", "https://fed/role": "west_sales", "https://fed/groups": ["sales"] } // Your app reads role → picks SP "west_sales" → SP sp-west-sales-id
The handshake: IDP produces a JWT with iss, aud, sub. Databricks federation policy validates all three. They're mirror images — if any field doesn't match: 401 / 403.
The external user authenticates with their own IDP. Their JWT is exchanged for a Databricks OAuth token scoped to a role-specific Service Principal. UC row filters, column masks, and connection grants enforce governance. The real external identity is preserved end-to-end for audit.
/oidc/v1/token exchange endpointPKCE / Authorization Code flow
role, email, groups
"west_sales" → sp-west-sales-id
IDP JWT → scoped Databricks SP token
DB token + IDP JWT + role header
JWKS check → server-side exchange
Row filter + Column mask + USE CONNECTION
Email, role, tool, latency recorded
External IdP JWT is exchanged for a scoped Databricks SP token via RFC 8693. UC governance fires per SP group membership at every SQL query.
Issued by external IDP
{
"iss": "https://{idp-domain}/",
"aud": "https://adb-{ws}.azuredatabricks.net",
"sub": "{client_id}@clients",
"exp": 1710612000,
// Custom claims (namespace varies by IDP)
"https://ns.example.com/groups": ["sales-west"],
"https://ns.example.com/role": "sales-west",
"https://ns.example.com/email": "sarah@partner.com"
}
Algorithm: RS256 · Signed by IDP's private key · Verifiable via JWKS
From /oidc/v1/token exchange
{
"access_token": "eyJhbG...",
"token_type": "Bearer",
"expires_in": 3600
}
// The access_token is a Databricks
// OAuth token scoped to the role-SP.
// current_user() = SP application_id
// is_member() = SP's group membership
Opaque to the caller · Encodes SP identity · UC enforcement via SP's groups
Derived from SP identity at query time
-- Row filter function SELECT * FROM sales.opportunities -- UC auto-applies: WHERE is_member('west_sales') AND opp_region = 'WEST' -- Column mask function CASE WHEN is_member('finance') THEN margin_pct ELSE NULL END
Fires automatically · No application code needed · Can't be bypassed
| # | Enforcement Point | Layer | What Fires | Failure Mode |
|---|---|---|---|---|
| 1 | IDP Authentication | External IDP | PKCE / password / SSO → JWT issued | Login failed → no JWT issued |
| 2 | Claims Injection | IDP Action/Hook | Groups, role, email injected into JWT | Missing claims → role resolution fails at step 5 |
| 3 | Federation Policy Validation | Databricks /oidc/v1/token | Issuer + audience + subject checked against SP's policy | 400/401/403 → no Databricks token |
| 4 | JWT Signature Verification | API Server (Databricks Apps) | JWKS-based RS256 verification + audience + issuer | 401 → request rejected |
| 5 | Role → SP Mapping | API Server (Databricks Apps) | JWT role claim → SP application_id lookup | 403 → unknown role |
| 6 | Tool Access Control | API Server (Databricks Apps) | TOOL_ACCESS dict: role ∈ allowed_roles? | ACCESS_DENIED logged to audit |
| 7 | UC Governance | Databricks SQL Engine | Row filters (is_member()), column masks, USE CONNECTION grants | Filtered/masked silently — never errors, just restricts |
Points 1–5 are loud — they return HTTP errors and block the request. Point 6 is semi-loud — it returns a structured ACCESS_DENIED response and logs to audit. Point 7 is silent — UC row filters and column masks never error; they just restrict what data you see. A query that returns 0 rows is governance working, not a bug.
| HTTP | Source | Error | Cause | Fix |
|---|---|---|---|---|
400 | App Server (customer) | Missing tool or role | Request body missing tool or role field | Include both fields in POST body |
400 | App Server (customer) | Unknown role | Role not in ROLE_SP_MAP | Check role key matches config exactly |
400 | /oidc/v1/token | invalid_request | Malformed JWT, missing fields | Verify JWT is well-formed, all params present |
401 | /oidc/v1/token | invalid_grant | JWT signature invalid, issuer mismatch | Check JWKS keys, issuer URL (trailing slash matters!) |
401 | API Server (Databricks) | Invalid IDP token | JWKS verification failed (expired, wrong key, tampered) | Check IDP JWKS endpoint, token expiry, algorithm |
403 | /oidc/v1/token | OIDC policy validation failed | No federation policy on SP, or subject claim mismatch | Create/update federation policy: issuer + audience + subject |
403 | API Server (Databricks) | No role determined | JWT has no role claim, no X-Federation-Role header | Add role to JWT claims or pass header |
403 | API Server (Databricks) | Unknown role: '{role}' | Role not in server's ROLE_SP_MAP | Add role to ROLE_SP_MAP env var |
429 | Federation Policy API | Rate limited | Too many policy creation requests | Exponential backoff: 2n+1 seconds |
502 | App Server / API Server | Token exchange failed | Databricks rejected the exchange | Check federation policy, SP status, JWT claims |
200 | MCP Tool | ACCESS_DENIED | Role not in TOOL_ACCESS for this tool | Expected behavior — logged to audit table |
200 | SQL Engine | 0 rows returned | UC row filter excluded all rows for this SP's groups | Expected behavior — governance working correctly |
200 | SQL Engine | NULL in column | UC column mask returned NULL for non-privileged group | Expected behavior — column masking active |
Test: curl POST {idp}/oauth/token
Check: client_id, client_secret, audience, grant_type
Test: echo $JWT | cut -d. -f2 | base64 -d | jq
Check IDP's claim injection mechanism: Action, Hook, or Policy
Test: curl POST {workspace}/oidc/v1/token
Check: policy exists on SP, issuer matches (trailing slash!), audience = workspace URL, subject matches sub claim
Check: HTTP status from API endpoint
Check: JWKS endpoint accessible, RS256 algorithm, audience + issuer match server config
Check: JWT has role claim, role key exists in ROLE_SP_MAP config
0 rows = row filter active
NULL values = column mask active
ACCESS_DENIED = tool access control
Verify: SELECT is_member('group') as the SP
Register a custom API with audience = Databricks workspace URL. This tells the IDP: "JWTs for this audience are meant for Databricks."
audience: https://adb-{workspace_id}.azuredatabricks.net
For the external app server to get JWTs via client_credentials grant. This app's client_id becomes the sub claim in the JWT.
For end-users to authenticate via PKCE in the browser. Redirect URI = your app's callback URL.
Users typically already exist in the IDP. Assign them to groups that map to Databricks roles. Group membership drives which SP the user's token exchanges for.
Groups are native objects in Okta/Entra. In Auth0, groups are stored in app_metadata on each user.
Inject groups, role, email into the access token. This is the IDP-specific part:
| Auth0 | Action (post-login trigger) |
| Okta | Authorization Server custom claims |
| Entra | Claims Mapping Policy + App Roles |
One SP per role (not per user). Each SP gets a unique application_id and numeric_id.
sp-role-west-sales, sp-role-finance, etc.
Create workspace groups (e.g., west_sales) and add each SP. These groups are what is_member() checks in row filters and column masks.
On each SP, register the IDP's issuer, audience, and subject. This is what Databricks checks when validating the JWT during token exchange.
Account-level API — requires Databricks account admin.
Set up row filters (is_member('group')), column masks, and GRANT USE CONNECTION on tables. Grant SPs SELECT on data tables and SQL warehouse access.
API server on Databricks Apps (JWT verification + server-side exchange). External app anywhere (your infra). Configure both with the IDP's domain, client_id, JWKS URI, and the role → SP mapping.
| IDP Config | Produces | Databricks Field |
|---|---|---|
| IDP Tenant / Domain | iss claim | Policy issuer |
| API / Resource Server | aud claim | Policy audiences |
| M2M App client_id | sub claim | Policy subject |
| Claims injection | role, groups | App-level ROLE_SP_MAP |
// Databricks Account API POST accounts/{account_id} /servicePrincipals/{sp_id} /federationPolicies { "oidc_policy": { "issuer": "https://{idp-domain}/", "audiences": ["https://adb-{ws}...net"], "subject": "{m2m_client_id}@clients" } } // Same 3 fields on all role SPs. // Swap IDP → update these 3 → done.
The federation policy is the only config that connects the IDP to Databricks. Everything else (SPs, groups, UC governance) is IDP-independent.
Static config map — created once when you set up the role SPs. Lives in the app server's env config.
// App server config (env var) ROLE_SP_MAP = { "west_sales": "sp-west-sales-id", "east_sales": "sp-east-sales-id", "managers": "sp-managers-id", "executive": "sp-executive-id", "finance": "sp-finance-id", "admin": "sp-admin-id" } // Runtime flow: // 1. SPA login → IDP token: role="west_sales" // 2. App looks up ROLE_SP_MAP["west_sales"] // 3. Token exchange: client_id=sp-west-sales-id
These are Databricks SP application_ids — not IDP client_ids. Created during setup, static thereafter. Only changes when you add/remove roles.
| Grant | On | To |
|---|---|---|
| Federation Policy | Each role-SP | — |
| CAN_USE | SQL Warehouse | All SPs |
| USE CATALOG | Data catalog | All SPs |
| USE SCHEMA | Data schema(s) | All SPs |
| SELECT | Data tables | Role SPs |
| SELECT | VS index | App SP |
| MODIFY + SELECT | Audit table | App SP only |
| USE CONNECTION | github_bearer_token | Exec + Admin |
| CAN_QUERY | Serving endpoint | Exec + Admin |
| CAN_RUN | Genie Space | All role SPs |
| Workspace group | Group per role | Each role SP |
Currently: scope=all-apis. Minimum required per tool:
| Role SPs | sql + genie + serving |
| App SP | sql + vector-search |
Policy is scoped to a specific Service Principal. Only that SP can exchange tokens from this IDP.
POST /accounts/{account_id} /servicePrincipals/{sp_numeric_id} /federationPolicies
When to use:
Visibility: Account Console → Service principals → {SP} → Credentials & secrets → Federation policies.
Policy applies to any identity in the account. Any SP or user can exchange tokens from this IDP.
POST /accounts/{account_id}
/federationPolicies
When to use:
Visibility: Visible in Account Console → Security → Authentication → Federation policies.
Both levels validate the same 3 fields against the JWT:
{
"oidc_policy": {
"issuer": "https://{idp-domain}/",
"audiences": ["https://adb-{ws}.azuredatabricks.net"],
"subject": "{jwt-sub-claim}"
}
}
The issuer URL must exactly match the iss claim in the JWT. Most IDPs include a trailing slash: https://login.example.com/. Omitting it = policy validation failure.
| Auth0 M2M | {client_id}@clients |
| Okta Service | {client_id} |
| Entra App | {app_id} (GUID) |
| GitHub Actions | repo:{org}/{repo}:ref:refs/heads/main |
grant_type=urn:ietf:params:oauth:grant-type:token-exchange subject_token={IDP_JWT} subject_token_type=urn:ietf:params:oauth:token-type:jwt client_id={SP_APPLICATION_ID} scope=all-apis
| Endpoints | |
|---|---|
| Token | https://{tenant}.auth0.com/oauth/token |
| JWKS | https://{tenant}.auth0.com/.well-known/jwks.json |
| Issuer | https://{tenant}.auth0.com/ |
| Subject (M2M) | {client_id}@clients |
Trigger: post-login (v3)
const ns = 'https://federation.example.com'; const groups = event.user.app_metadata?.groups || []; api.accessToken.setCustomClaim( `${ns}/groups`, groups); api.accessToken.setCustomClaim( `${ns}/role`, resolveRole(groups));
access_token_authz on Resource Server{client_id}@clients| M2M | client_credentials flow for external app |
| SPA | PKCE flow for browser login |
| MGMT | Management API access (automation only) |
| API (Resource Server) | Identifier = workspace URL |
| Action | post-login: inject claims |
| Token Dialect | access_token_authz |
| Connection | Username-Password-Authentication |
| Users | app_metadata: groups, company, title |
"subject": "{M2M_CLIENT_ID}@clients"
| SPA Login | openid profile emailStandard OIDC scopes → ID token + user info |
| M2M Token | audience={workspace_url} (implicit)Auth0 M2M uses audience, not explicit scopes. Token includes all permissions granted to the M2M app for that API. |
| Token Exchange | scope=all-apisDatabricks-side scope. Grants full API access within SP's role. UC governance restricts at data layer. |
| Endpoints | |
|---|---|
| Token | https://{org}.okta.com/oauth2/{auth_server_id}/v1/token |
| JWKS | https://{org}.okta.com/oauth2/{auth_server_id}/v1/keys |
| Issuer | https://{org}.okta.com/oauth2/{auth_server_id} |
| Subject (M2M) | {client_id} (no @clients suffix) |
Admin → Security → API → Authorization Server → Claims tab
// Custom claim: "groups" Name: groups Value: user.getGroups().filter( g, g.name.startsWith("fed_")) Include in: Access Token Scope: Any scope
/oauth2/{server_id} — NOT just the org URL{client_id} (no @clients)| Service | OAuth 2.0 Service app (client_credentials) |
| SPA | OIDC SPA app (PKCE, authorization code) |
| Authorization Server | Custom server with audience = workspace URL |
| Scopes | Add custom scope if needed |
| Claims | groups, role, email (custom claims on access token) |
| Access Policies | Rule for service app + SPA app |
| Groups | Assign users to Okta groups (mapped to Databricks roles) |
"subject": "{SERVICE_APP_CLIENT_ID}" // No @clients suffix for Okta
| SPA Login | openid profile email + custom scopesCustom scopes gate which claims are included in token |
| Service App | scope={custom_scope} on Auth ServerOkta requires at least one scope. Create a custom scope on the Authorization Server (e.g., federation). Access policy rules filter by scope. |
| Token Exchange | scope=all-apisSame Databricks-side scope regardless of IDP |
| Endpoints | |
|---|---|
| Token | https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token |
| JWKS | https://login.microsoftonline.com/{tenant_id}/discovery/v2.0/keys |
| Issuer | https://login.microsoftonline.com/{tenant_id}/v2.0 |
| Subject (M2M) | {app_id} (GUID of the App Registration) |
PowerShell / MS Graph API → Claims Mapping Policy
// Claims Mapping Policy (JSON) { "ClaimsMappingPolicy": { "Version": 1, "IncludeBasicClaimSet": "true", "ClaimsSchema": [{ "Source": "user", "ID": "groups", "JwtClaimType": "groups" }] } }
.../v2.0 (v2 endpoints) vs .../ (v1) — must match exactlygroupMembershipClaims: "SecurityGroup" in manifest| App Reg (M2M) | Client credentials with secret/cert |
| App Reg (SPA) | PKCE redirect, SPA platform |
| API Scope | Expose an API → Application ID URI = workspace URL |
| Claims Policy | Assign to Service Principal |
| Security Groups | Map to Databricks roles |
"subject": "{APP_REGISTRATION_APP_ID}" // The Application (client) ID GUID // NOT the Object ID
| SPA Login | openid profile email User.ReadMS Graph delegated permissions for user info |
| M2M (App) | scope={app_id_uri}/.defaultEntra uses .default scope pattern. Requests all permissions pre-consented for the app. The app_id_uri = workspace URL. |
| Token Exchange | scope=all-apisSame Databricks-side scope regardless of IDP |
Option A: Map Entra Security Group GUIDs to roles in app config
Option B: Use App Roles (defined in App Registration manifest) — appear as roles claim
Option C: Use Claims Mapping to emit group names instead of GUIDs
| Configuration | Auth0 | Okta | Entra ID |
|---|---|---|---|
| Issuer URL | https://{tenant}.auth0.com/trailing slash |
https://{org}.okta.com/oauth2/{server_id}no trailing slash |
https://login.microsoftonline.com/{tenant}/v2.0v2.0 suffix |
| JWKS URL | .../.well-known/jwks.json |
.../v1/keys |
.../discovery/v2.0/keys |
| M2M Sub Claim | {client_id}@clients |
{client_id} |
{app_id} (GUID) |
| Claims Injection | Auth0 Action (post-login) | Authorization Server custom claims | Claims Mapping Policy + manifest |
| Namespace Required? | Yes Must be non-Auth0 URL | No Top-level claims OK | No Top-level claims OK |
| Token Dialect Fix? | Yes access_token_authz |
No Custom claims included by default | No Claims policy handles it |
| Group Format | String names (from app_metadata) | String names (Okta groups) | GUIDs (Security Groups) by default |
| M2M App Type | Machine-to-Machine Application | OAuth 2.0 Service App | App Registration + Client Secret/Cert |
| SPA App Type | Single Page Application | OIDC SPA App | App Registration (SPA platform) |
| Automation | Management API v2 | Okta Admin API | MS Graph API |
| SPA Scopes | openid profile email |
openid profile email + custom |
openid profile email User.Read |
| M2M Scopes | Implicit via audienceNo explicit scope param |
scope={custom_scope}Must create on Auth Server |
scope={app_id_uri}/.default.default = all pre-consented |
| Exchange Scope | scope=all-apis ← SAME for all IDPs (Databricks-side) |
||
| Free Tier | 25k MAU, 7.5k M2M tokens/mo | 100 MAU (Workforce), limited M2M | Part of Azure AD (included with M365) |
Create M2M + SPA apps in new IDP
Configure groups/role/email claims in new IDP's mechanism
Update issuer and subject on each SP's policy
Update IDP domain, client_id, JWKS URI, token endpoint, claims namespace
Update IDP_DOMAIN, IDP_AUDIENCE, IDP_ISSUER, JWKS_URI env vars
Create test users with groups/roles in new IDP
Same SPs, same application_ids, same group membership
Same is_member() functions, same tables
Same GRANT/REVOKE USE CONNECTION SQL
Same ROLE_SP_MAP config (roles are app-defined, not IDP-defined)
Same /oidc/v1/token with same grant_type
Same schema, same tags, same correlation
The IDP provides the JWT. Databricks validates it via federation policy. Swap the JWT issuer, update the policy — everything downstream stays the same. The governance layer (UC) is completely IDP-agnostic.
External app exchanges IDP JWT → DB token (for proxy auth). MCP server independently exchanges the same IDP JWT → DB token (for SQL execution). Both resolve to the same role-SP. The external user's identity is preserved in JWT claims, audit records, and MLflow traces.
UC row filters and column masks fire at the SQL engine level. They can't be bypassed by changing the application, the IDP, or the query. is_member() checks the SP's group membership — not the caller's headers.
Auth failures (401/403) are loud and visible. Governance enforcement (0 rows, NULL columns) is silent and correct. Know which one you're looking at before debugging.
90% of federation failures are a mismatch between the JWT's sub claim and the federation policy's subject field. Every IDP formats this differently: client_id@clients (Auth0), client_id (Okta), app_id (Entra). Decode the JWT first, then write the policy.
A working IDP token + a working federation policy can still fail if the subject claim doesn't match. Test the full chain: get IDP JWT → exchange → call /api/2.0/preview/scim/v2/Me → verify SP identity.
| Persona | Role | Group | Regions | Margins | Identity | Regions | Sales | Genie | KB | Supervisor | GitHub | Ext API | Audit |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Sarah Chen West Territory Lead | west_sales | west_sales | WEST only | NULL | Y | Y | Y | Y | Y | — | — | — | — |
| Marcus Johnson East Territory Lead | east_sales | east_sales | EAST only | NULL | Y | Y | Y | Y | Y | — | — | — | — |
| David Park Sales Manager | managers | managers | ALL | NULL | Y | Y | Y | Y | Y | — | — | — | — |
| Priya Sharma VP of Sales | executive | executives | ALL | Real | Y | Y | Y | Y | Y | Y | Y | Y | Y |
| Lisa Okafor External Auditor | finance | finance | ALL | Real | Y | Y | Y | Y | Y | — | — | — | Y |
| Raj Patel Platform Admin | admin | admin | ALL | Real | Y | Y | Y | Y | Y | Y | Y | Y | Y |
For the business: Partners, auditors, and consultants get self-service access to exactly the data they need — no IT tickets, no shared credentials, no waiting for provisioning.
For security: Every query runs through UC row filters and column masks. Every action is audited with the real external identity. Access is revocable with one SQL statement.
For engineering: Swap the IDP by updating the federation policy — no code changes. Add a new role by creating one SP and one workspace group. The architecture scales horizontally.
For compliance: External identity preserved end-to-end. Correlatable audit trail across custom tables, MLflow traces, and system.access.audit. No identity gaps.
| → / Space | Next slide |
| ← | Previous slide |
| Home | First slide |
| End | Last slide |
| Swipe | Touch navigation |