| → / Space | Next slide |
| ← | Previous slide |
| Home | First slide |
| End | Last slide |
| Swipe | Touch navigation |
The token exchange mechanics, the two architecture choices, and how to implement it.
External users need governed access, without a Databricks account and without a fragile app-level ACL layer.
The two-leg token exchange mechanic, and the two architecture choices it supports.
Scale, security, and compliance — concretely, not hand-wavy.
Actors, prerequisites, federation policy config, and the gotcha that trips up almost every deployment.
What to verify before this goes live.
Channel partners, external auditors, and consultants need to query Databricks-backed data through an app or dashboard. They authenticate through their own identity provider — Auth0, Okta, Entra, or whatever their organization uses. They are not in your corporate directory, and in most cases they never will be.
Guest-account lifecycle management for hundreds or thousands of external people, across organizations you don’t control, that requires cross-tenant directory federation. Doesn’t scale past a few dozen users.
A custom ACL layer in the app that decides what each external user can see. Fragile by construction — one bug exposes another customer’s data, and there’s no independent enforcement layer behind it.
What’s needed: the external IdP handles authentication, Databricks handles authorization and enforcement, and the human never sees a second login screen or a Databricks credential.
The user authenticates to their own IdP via authorization_code + PKCE. The app receives an IdP-signed JWT. This is ordinary SSO. Databricks is not involved yet — and the human’s IdP token never leaves the browser or the app’s trust boundary.
/oidc/v1/tokenThe app (or a backend it trusts) sends the IdP JWT to Databricks using grant type token-exchange. Databricks validates the JWT against a federation policy (issuer, audience, subject) and returns a Databricks token. This is where per-user identity enters Databricks.
Unity Catalog enforces at query time — row filters, column masks, connection grants — then audit records the external user’s identity from the verified JWT claims.
| Per-user OBO | Role-based service principal | |
|---|---|---|
| Identity model | Each external user is provisioned as a Databricks identity (SCIM) | N users map to O(roles) SPs — no Databricks provisioning |
| UC evaluates | current_user() — the real person | is_member() — the SP’s group membership |
| Scales to | Hundreds of provisioned users | Thousands of external users, unlimited — scales with roles, not people |
| Individual attribution | Native — current_user() is the email | Via JWT-tagged audit table, not current_user() |
| Best fit | Known users you can SCIM-provision, need native per-user row filters | Partners, auditors, consultants — never becoming Databricks identities |
Do not mix the two rules. Row filters written with current_user() only work when the caller genuinely is that user. Row filters written with is_member() only work when the caller is a role-scoped SP. Using the wrong function for the pattern in place fails silently — same result for every user, no error.
Claims below are Auth0-shaped, for illustration. The exchange itself is IdP-agnostic — Okta, Entra, or any OIDC-compliant provider slots into the same three federation-policy fields. Only the claim names and formats shift per provider (see Gotchas).
{
"iss": "https://idp/",
"aud": "https://workspace",
"sub": "{client_id}@clients",
"groups": ["sales-west"],
"email": "sarah@partner.com"
}
Signed RS256/ES256, verifiable via the IdP’s JWKS. HS256 blocks this pattern entirely — no shared-secret verification path.
iss → issuer · aud → audience · sub → subject — the same three federation-policy fields as slide 12, whichever IdP is on the other end.
{
"access_token": "eyJ...",
"expires_in": 3600
}
// opaque; scoped to the
// user or role-SP
Not decodable by the caller. Encodes either the user’s identity (OBO) or the role-SP’s identity, depending on which pattern is in play.
-- fires automatically, -- no app code needed WHERE current_user() = owner -- or WHERE is_member('west')
Row filters and column masks fire at the SQL engine, per token. Can’t be bypassed by the application, the IdP, or the query.
Provisioning a guest account per external person, syncing directories across organizations you don’t control, and managing the lifecycle of every one of those accounts as people join and leave partner firms. None of that is required — identity is anchored in the role, not the individual account.
Standard SSO to their own IdP — nothing else. The token exchange happens entirely server-side, in under 100ms. No separate Databricks credential is ever issued to or held by the end user.
Access is revocable with a single REVOKE or by removing the SP from a workspace group — instant, no redeploy. The federation policy is the only thing that trusts the IdP; everything downstream (UC grants, row filters) is completely IdP-agnostic.
Swap the IdP by updating three fields — issuer, audience, subject — on the federation policy. No application code changes.
Auth failures (401/403) are loud and visible. UC governance enforcement is silent by design: 0 rows returned or a masked NULL column is governance working correctly, not a bug.
Whether identity is anchored via current_user() or preserved via JWT-tagged audit records, the external user’s email, role, and action are recoverable — joinable to system.access.audit and MLflow traces.
“Show me what this external user touched last week” is answerable from one audit trail, not a custom application log that may or may not have captured it.
Human → SPA → Backend → Databricks → MCP/Genie Agents
↑ ↑
IdP (login) IdP (M2M token)
IdP, browser app (SPA), backend server (holds secrets, brokers the exchange), Databricks (token endpoint + UC), and the tool server (SQL/Genie Agents/MCP) that runs the governed query.
| IdP side (5) | SPA app, M2M app, API resource server, claims-injection hook, test users with role metadata |
| Databricks side (4) | Service principals (one per role), federation policy per SP, SP group memberships, warehouse access |
| Backend side (3) | M2M client_id, M2M client_secret (secret store only), role-to-SP mapping table |
POST /accounts/{account_id}/servicePrincipals /{sp_id}/federationPolicies { "oidc_policy": { "issuer": "https://{idp}/", "audiences": ["https://{workspace}"], "subject": "{jwt-sub-claim}" } }
POST /oidc/v1/token grant_type=urn:ietf:params:oauth: grant-type:token-exchange subject_token={idp_jwt} subject_token_type= urn:ietf:params:oauth:token-type:jwt scope=all-apis # client_id present -> role-SP token # client_id omitted -> per-user token
Get the exact values from a real, decoded JWT — never guess them. Trailing slashes on the issuer, @clients suffixes, and GUID-vs-name subject formats all vary by IdP and all cause silent mismatches.
RETURN IF(current_user() IN (SELECT email FROM approved), val, NULL);
Works because the caller genuinely is that user. Using is_member() here evaluates the execution service’s identity, not the human’s — every OBO user sees the same result.
WHERE is_member('sales_west') AND region = 'WEST'
Works because the token is scoped to a role-SP with real group membership. Using current_user() here just returns the SP’s application_id UUID — not useful for per-person filtering.
| Issue | Detail |
|---|---|
| HS256-signed IdP tokens | Shared-secret signing can’t be validated by Databricks without the secret. Token exchange is unavailable — fall back to SSO login for that IdP. |
| Subject claim format varies by IdP | Auth0: {client_id}@clients. Okta: {client_id}, no suffix. Entra: the app’s Object ID (a GUID), not the Application ID. Decode a real token before writing the policy. |
| Trailing slash on issuer | Must match the JWT’s iss claim exactly. Most IdPs include it; some don’t. A mismatch here is the single most common cause of exchange failures. |
Including client_id changes the outcome | Present → token is scoped to that service principal. Omitted → token is scoped to the calling user. Pick deliberately, not by accident. |
| 5-issuer account limit | Databricks allows up to five federated token issuers per account. More IdPs require consolidation or the SSO fallback for the extras. |
| Wrong row-filter function for the pattern | See previous slide — the most consequential mistake on this list. |
Confirmed with the IdP team, along with the JWKS URL. This alone determines whether the pattern is possible.
Issuer, audience, subject — copied exactly, never guessed.
current_user() for per-user OBO, is_member() for role-based SPs — not mixed.
Every IdP role has a mapping entry; unmapped roles are denied, not defaulted.
Never in code, config files, or git history.
Confirmed different data returned before this goes to production.