-
Notifications
You must be signed in to change notification settings - Fork 47
Add federated authorization tutorial #576
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
sohanmaheshwar
wants to merge
2
commits into
main
Choose a base branch
from
federated-authorization-tutorial
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+257
−0
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,256 @@ | ||
| import { Callout, Tabs } from "nextra/components"; | ||
|
|
||
| # 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. | ||
|
|
||
|  | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| - A running SpiceDB instance. | ||
| [Install SpiceDB](/spicedb/getting-started/install) and start it. | ||
| - The [`zed` CLI](/spicedb/getting-started/installing-zed), pointed at your instance: | ||
|
|
||
| ```sh | ||
| 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`. | ||
|
|
||
| ```zed | ||
| 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: | ||
|
|
||
| ```sh | ||
| 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. | ||
|
|
||
| <Callout type="warning"> | ||
| Key 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. | ||
| </Callout> | ||
|
|
||
| 2. Look up `<provider>_account:<id>`'s `bound_to`. If it resolves to a `user`, you're done. | ||
|
|
||
| <Callout type="warning"> | ||
| 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. | ||
| </Callout> | ||
|
|
||
| 3. If nothing is bound yet, mint a new `user:<uuid>` and write the binding. | ||
|
|
||
| A first Keycloak login writes one relationship: | ||
|
|
||
| ```sh | ||
| zed relationship create keycloak_account:9f3c bound_to user:7b1e | ||
| ``` | ||
|
|
||
| A GitHub login binds its own account to its own internal user: | ||
|
|
||
| ```sh | ||
| 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`](https://github.com/authzed/authzed-py) library. | ||
|
|
||
| <Tabs items={["zed", "Python"]}> | ||
| <Tabs.Tab> | ||
|
|
||
| ```sh | ||
| zed relationship create document:readme owner user:7b1e | ||
| ``` | ||
|
|
||
| </Tabs.Tab> | ||
| <Tabs.Tab> | ||
|
|
||
| ```python | ||
| from authzed.api.v1 import ( | ||
| WriteRelationshipsRequest, RelationshipUpdate, Relationship, | ||
| ObjectReference, SubjectReference, | ||
| ) | ||
|
|
||
| client.WriteRelationships(WriteRelationshipsRequest( | ||
| updates=[RelationshipUpdate( | ||
| operation=RelationshipUpdate.OPERATION_TOUCH, | ||
| relationship=Relationship( | ||
| resource=ObjectReference(object_type="document", object_id="readme"), | ||
| relation="owner", | ||
| subject=SubjectReference( | ||
| object=ObjectReference(object_type="user", object_id="7b1e"), | ||
| ), | ||
| ), | ||
| )], | ||
| )) | ||
| ``` | ||
|
|
||
| </Tabs.Tab> | ||
| </Tabs> | ||
|
|
||
| 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?" | ||
|
|
||
| <Tabs items={["zed", "Python"]}> | ||
| <Tabs.Tab> | ||
|
|
||
| ```sh | ||
| zed permission check document:readme view user:7b1e | ||
| # true | ||
| ``` | ||
|
|
||
| </Tabs.Tab> | ||
| <Tabs.Tab> | ||
|
|
||
| ```python | ||
| from authzed.api.v1 import CheckPermissionRequest, CheckPermissionResponse | ||
|
|
||
| resp = client.CheckPermission(CheckPermissionRequest( | ||
| resource=ObjectReference(object_type="document", object_id="readme"), | ||
| permission="view", | ||
| subject=SubjectReference( | ||
| object=ObjectReference(object_type="user", object_id="7b1e"), | ||
| ), | ||
| )) | ||
| allowed = resp.permissionship == CheckPermissionResponse.PERMISSIONSHIP_HAS_PERMISSION | ||
| # allowed == True | ||
| ``` | ||
|
|
||
| </Tabs.Tab> | ||
| </Tabs> | ||
|
|
||
| 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`: | ||
|
|
||
| <Tabs items={["zed", "Python"]}> | ||
| <Tabs.Tab> | ||
|
|
||
| ```sh | ||
| zed relationship create document:readme viewer user:4a02 | ||
| zed permission check document:readme view user:4a02 | ||
| # true | ||
| ``` | ||
|
|
||
| </Tabs.Tab> | ||
| <Tabs.Tab> | ||
|
|
||
| ```python | ||
| # Imports as in Step 3 | ||
| client.WriteRelationships(WriteRelationshipsRequest( | ||
| updates=[RelationshipUpdate( | ||
| operation=RelationshipUpdate.OPERATION_TOUCH, | ||
| relationship=Relationship( | ||
| resource=ObjectReference(object_type="document", object_id="readme"), | ||
| relation="viewer", | ||
| subject=SubjectReference( | ||
| object=ObjectReference(object_type="user", object_id="4a02"), | ||
| ), | ||
| ), | ||
| )], | ||
| )) | ||
| ``` | ||
|
|
||
| </Tabs.Tab> | ||
| </Tabs> | ||
|
|
||
| To build a dashboard, ask the inverse question — which documents can this user see? | ||
|
|
||
| <Tabs items={["zed", "Python"]}> | ||
| <Tabs.Tab> | ||
|
|
||
| ```sh | ||
| zed permission lookup-resources document view user:4a02 | ||
| # document:readme | ||
| ``` | ||
|
|
||
| </Tabs.Tab> | ||
| <Tabs.Tab> | ||
|
|
||
| ```python | ||
| from authzed.api.v1 import LookupResourcesRequest | ||
|
|
||
| for resp in client.LookupResources(LookupResourcesRequest( | ||
| resource_object_type="document", | ||
| permission="view", | ||
| subject=SubjectReference( | ||
| object=ObjectReference(object_type="user", object_id="4a02"), | ||
| ), | ||
| )): | ||
| print(resp.resource_object_id) # document:readme | ||
| ``` | ||
|
|
||
| </Tabs.Tab> | ||
| </Tabs> | ||
|
|
||
| That returns everything reachable through any path (owned, editor, or viewer) without those rules living in your app. | ||
|
|
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. General note: I think it's worth calling out the work required to associate users from different identity providers - otherwise the internal user ID is just a layer of indirection. |
||
| ## 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](/spicedb/concepts/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](https://authzed.com/cloud/signup). | ||
|
|
||
| The full runnable demo for this tutorial lives in the [authzed/examples repository](https://github.com/authzed/examples/tree/main/federated-authorization). | ||
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if this is the level of detail you want to get to, but this requires the authentication layer to have the mapping as well, because otherwise the calling application doesn't have this association. At $PREVIOUS_JOB we had a set of bindings between Auth0 and our application server that then attached this internal user ID as a custom claim on the JWT. Otherwise you do want to check using the identity provided by the IDP, and ideally you note the IDP ID associated with the user somewhere in your application for administration purposes.