Skip to Content
SpiceDB is 100% open source. [Star us on GitHub]
SpiceDBTutorialsFederate Authorization Across Multiple Identity Providers

Tutorial: Federate Authorization Across Multiple Identity Providers

The federated authorization problem is a gap when teams rely on incompatible identity providers (IdP) that cannot be instantly unified. This creates a barrier where shared resources remain inaccessible to users, making impractical identity migrations the only traditional path forward. For example: You work at a large enterprise where one business unit logs in through Keycloak and contractors use GitHub, but they all need to share the same resources. This problem persists with the implementation of RAG pipelines and internal chatbots too.

This tutorial illustrates how you can use SpiceDB to centralize permissions by mapping disparate external accounts to a single, unified internal user model.

The central idea is: don’t point your resources at “the GitHub user” or “the Keycloak user.” Point them at an internal user you own, and bind each external account to it.

Many identity providers, one federated authorization: external accounts from swappable IdPs each bind to one internal user, and SpiceDB governs document access for internal users only

Prerequisites

  • A running SpiceDB instance. Install SpiceDB and start it.

  • The zed CLI, pointed at your instance:

    zed context set tutorial localhost:50051 "your-preshared-key" --insecure
  • An app that already authenticates users and can read each token’s stable id: the sub claim for OIDC providers like Keycloak, the numeric account id for GitHub.

Step 1: Write the schema

Give each identity provider its own object type, and have both bind to a shared user. Resources reference only that user.

definition user {} definition keycloak_account { relation bound_to: user } definition github_account { relation bound_to: user } definition document { relation owner: user relation editor: user relation viewer: user permission edit = editor + owner permission view = viewer + edit permission share = owner }

Write it:

zed schema write schema.zed

keycloak_account and github_account are separate types, so two providers can’t collide on an id. The document definition has no idea either one exists.

Step 2: Resolve each login to an internal user

This is the critical federation step, and it runs in your app on every login. We’re going to use the sub claim (the stable, unique identifier the IdP promises won’t change) from their token, and look up keycloak_account:<sub>. That resolves to an internal user:<uuid>. A GitHub login does the same thing with GitHub’s numeric account id.

keycloak_account:9f3c… #bound_to@user:7b1e… github_account:1119120 #bound_to@user:4a02…
  1. Read the stable id from the token — the sub for Keycloak, the account id for GitHub.
  2. Look up <provider>_account:<id>’s bound_to. If it resolves to a user, you’re done.
  3. If nothing is bound yet, mint a new user:<uuid> and write the binding.

Note: Key in on the immutable id but never the email. Emails change and get reassigned, so an email-keyed binding can silently point at the wrong person.

If the lookup errors, fail the login otherwise the system can swallow the error and create a new user instead, and a returning user loses all their access.

A first Keycloak login writes one relationship:

zed relationship create keycloak_account:9f3c bound_to user:7b1e

A GitHub login binds its own account to its own internal user:

zed relationship create github_account:1119120 bound_to user:4a02

Step 3: Grant access

Permissions are written as relationships. Make user:7b1e the owner of a document. The Python examples assume an already-initialized SpiceDB client from the authzed-py library.

zed relationship create document:readme owner user:7b1e

You don’t grant view, edit, and share separately. You write owner, and the schema computes the rest.

Step 4: Check a permission

SpiceDB resolves permission by answering a question in the form of “is this actor allowed to perform this action on this resource?” or in this case “can user:7b1e view the doc?“

zed permission check document:readme view user:7b1e # true

The check names a user, not a Keycloak or GitHub account. Authentication is upstream; this is downstream. They meet at the user id and nowhere else.

Step 5: Share, then list

Share the document with the GitHub-bound user as a viewer, and confirm the check flips to true:

zed relationship create document:readme viewer user:4a02 zed permission check document:readme view user:4a02 # true

To build a dashboard, ask the inverse question — which documents can this user see?

zed permission lookup-resources document view user:4a02 # document:readme

That returns everything reachable through any path (owned, editor, or viewer) without those rules living in your app.

A note on consistency

The demo reads default to minimize_latency, which is fast but can be a few seconds stale.

When a user needs to see a change they just made, take the ZedToken returned by the write and pass it on the next read as at_least_as_fresh. That gives you read-your-writes without giving up caching. See Consistency for the details.

Next steps

This pattern is provider-agnostic: add a third IdP by adding one more *_account type, and nothing downstream changes. To run this in production instead of a local container, provision a managed instance on AuthZed Cloud .

The full runnable demo for this tutorial lives in the authzed/examples repository .