Repository navigation
Add federated-authorization example - #48
Conversation
A self-contained demo of federated authentication with centralized fine-grained authorization in SpiceDB: users log in through different identity providers (Keycloak OIDC and GitHub OAuth), each external account binds to a canonical internal user, and a single SpiceDB instance enforces document access control across all of them. Signed-off-by: Sohan Maheshwar <1119120+sohanmaheshwar@users.noreply.github.com>
Signed-off-by: Sohan Maheshwar <1119120+sohanmaheshwar@users.noreply.github.com>
Add document-start marker and quote all string values to satisfy the repo's quoted-strings yamllint rule. Signed-off-by: Sohan Maheshwar <1119120+sohanmaheshwar@users.noreply.github.com>
The edit/share/unshare handlers interpolated the user-controlled doc_id path parameter straight into a redirect Location, which CodeQL flagged as URL redirection from a remote source. Document ids are always uuid.uuid4(), so normalize doc_id through uuid.UUID() at the top of each handler: invalid ids now return 404, and the redirect target can only contain hex digits and hyphens, so it cannot escape the /documents/ path. Signed-off-by: Sohan Maheshwar <1119120+sohanmaheshwar@users.noreply.github.com>
e4c2d14 to
1d43f54
Compare
SpiceDB's REST gateway listens on :8443 inside the container, but the port was mapped from host 8090 to container 8080, so the documented http://localhost:8090 endpoint was unreachable (connection refused). Map 8090:8443 so the REST API is actually exposed. Signed-off-by: Sohan Maheshwar <1119120+sohanmaheshwar@users.noreply.github.com>
Signed-off-by: Sohan Maheshwar <1119120+sohanmaheshwar@users.noreply.github.com>
Replace the diagram with the corrected version (identity bindings shown stored in SpiceDB, resolved via a LookupSubjects bound_to call) and update the image alt text to match.
…on README The image diagram (architecture.png) is the source of truth; the ASCII version still showed the old framing (bound_to binding inside the app rather than in SpiceDB).
|
I was testing this sample and stumbled upon a bug which I think could use some discussion. Basically there is a duplication of users if you restart the container, this is because in-memory SpiceDB loses IdP bindings on restart while SQLite persists → duplicate rows. (this also shows up when a user can share a document with themself). To fix this, claude recommends: I think for a sample app Option 1 is better. Would this resemble a production-ish pattern? AppDB owns identity mapping and SpiceDB does only authZ. I think @tstirrat15 's comment on the docs was something to this effect. imo, the postgres option is overkill for a demo. |
|
What's wrong with postgres?
It would still be self-contained and zero-setup. You can have a docker-compose that does all of that for you. We have one that you can use, I use it all the time :) https://github.com/authzed/spicedb/blob/main/docker-compose.postgres.yaml |
Yep, like for the RAG demos I use a vector db in a docker container as well. just wondering if in this case, having a postgres instance to do state management takes away from the central teaching concept. Like the architecture diagram gets one more block, and there's a new section in the docs etc. OTOH, the 'add a new field' is a classic LLM way to solve a problem although in this particular case it maybe ok? Not sure which is the better approach here. |
- FAQ: drop the trailing slash from the link to the Zanzibar concepts page. authzed.com serves the trailing-slash URL with a 200 instead of redirecting, so the page's relative links resolved one level too deep. - Update moved SpiceDB source links (gc.go, schemadsl lexer), the F1 paper PDF, and LangChain's FAISS entry. - Remove links to the retired Motia docs and to the unmerged federated-authorization example (authzed/examples#48). - Skip URLs that fail for the CI crawler but work in a browser: Akamai/Cloudflare challenges, x.com, hackmd uploads, nbviewer, and dbfiddle, plus mcp.authzed.com, an MCP endpoint rather than a page. linkcheck exits non-zero on warnings as well as errors, so each of these kept the daily full check red. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Clearing the app session cookie alone left Keycloak's browser SSO session alive, so the next "Log in with Keycloak" silently re-authenticated and logout appeared to do nothing. Store the Keycloak id_token on login and, on logout, redirect the browser to Keycloak's end-session endpoint (front-channel, RP-initiated) with id_token_hint + post_logout_redirect_uri. Register the post-logout redirect URIs on the realm client so Keycloak accepts the redirect back to the app.
SpiceDB ran with --datastore-engine=memory, so restarting the stack wiped every relationship. Because the app's SQLite users table persisted, a returning user found no bound_to binding, minted a fresh UUID, and became a duplicate user row; documents also silently lost their grants. Switch SpiceDB to the postgres datastore: - add a postgres service (with track_commit_timestamp=on, SpiceDB's recommended setting) and a persistent volume - add a one-shot 'spicedb datastore migrate head' that runs before serve (service_completed_successfully) so SpiceDB never starts unmigrated - point 'spicedb serve' at the postgres conn URI; the app now waits for spicedb to be healthy Bindings and document grants now persist across 'docker compose restart' and down/up, fixing the duplicate-user bug at the source with no application changes. README documents the datastore and its rationale. Verified: after restarting SpiceDB, a returning login resolves to the same user UUID (one users row, not two) and a previously-owned document is still viewable.
… via env Logout (finding #1): the id_token was minted over the internal address (keycloak:8080) so its iss was http://keycloak:8080/..., but the end-session redirect targets the public address (localhost:8080). Keycloak rejects an id_token_hint whose issuer doesn't match the request host, so the SSO session was never ended and the next login silently re-authenticated. Pin the realm's frontendUrl to http://localhost:8080 so every token carries iss=localhost:8080, matching the end-session endpoint, and fix the misleading code comment. docker-compose (findings #4/#5): move the SpiceDB datastore engine and connection URI into a shared YAML anchor passed as environment (SPICEDB_DATASTORE_*). The Postgres credential is now defined once and no longer appears on the process command line (visible via docker inspect), matching the canonical docker-compose.postgres.yaml. Verified live: a token minted over keycloak:8080 now has iss=http://localhost:8080/realms/org-a; SpiceDB connects via env with no conn URI in argv.
…iew checks
Audit follow-up (spicedb-best-practices):
- check_permission now fails CLOSED: a SpiceDB error is logged and returned as
'no permission' instead of propagating to a 500, so an outage can never fall
open into granting access. Every call site is 'if not check(...): deny'.
- Add check_permissions_bulk and use it in view_document: view/edit/share are
resolved in a single CheckBulkPermissions round-trip (was three sequential
CheckPermission calls). It also fails closed — every permission defaults to
denied, and a per-item or request-level error leaves it denied.
- lookup_viewable_documents and read_document_relationships no longer swallow
errors silently ('except Exception: pass'): they log and return an empty list,
so a SpiceDB outage shows nothing rather than a misleading partial listing.
Verified live against Postgres-backed SpiceDB: owner -> all granted, stranger ->
all denied, and an invalid permission is logged and left denied while valid
permissions in the same request still resolve.
- Pin authzed/spicedb to v1.56.1 (the version :latest currently resolves to) for both the migrate and serve services, matching the already-pinned keycloak:24.0 and postgres:16-alpine. Avoids non-reproducible pulls now that the migrate step applies schema migrations to a persistent Postgres volume. - README: say 'datastore migrate head' to match the compose command (the older 'migrate head' alias still works, but the docs should match what runs).
Replace the two tables (relationships written during the walkthrough, and API calls made by the app) with a single 'How the App Uses SpiceDB' section: a Mermaid diagram mapping user actions to writes vs. reads/checks and the two relationship kinds (bold heading per box), plus two summary bullets. Remove the File Layout section.
Regenerate architecture.png with a PostgreSQL datastore cylinder beneath the
SpiceDB spine ('persists relationships', plus a one-shot migrate chip) and a
caption noting that bindings and grants survive a restart. Update the image alt
text to match.
Resolved the duplicate-users discussion: back SpiceDB with PostgresPicked this back up and went with @miparnisari's suggestion — switched SpiceDB from the in-memory engine to a persistent Postgres datastore. That turned out to be the real root cause, and it's broader than just duplicate users: with The major change, at a glanceBefore — in-memory; a restart wipes everything: flowchart LR
U["User's Browser"]
subgraph IdP["Identity Providers"]
KC["Keycloak (Org A)<br/>OIDC"]
GH["GitHub (Org B)<br/>OAuth"]
end
A["FastAPI app"]
DB[("SQLite<br/>profiles + doc metadata")]
S["SpiceDB"]
MEM[/"in-memory datastore<br/>lost on restart"/]:::warn
U <--> A
U -->|login| KC
U -->|login| GH
A -->|token exchange| KC
A -->|token exchange| GH
A -->|gRPC| S
A --> DB
S --- MEM
classDef warn fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#7c2d12;
After — Postgres-backed; bindings and grants survive a restart: flowchart LR
U["User's Browser"]
subgraph IdP["Identity Providers"]
KC["Keycloak (Org A)<br/>OIDC"]
GH["GitHub (Org B)<br/>OAuth"]
end
A["FastAPI app<br/>(unchanged)"]
DB[("SQLite<br/>profiles + doc metadata")]
S["SpiceDB"]
PG[("PostgreSQL<br/>durable relationship store")]:::new
MIG["datastore migrate head<br/>one-shot on startup"]:::new
U <--> A
U -->|login| KC
U -->|login| GH
A -->|token exchange| KC
A -->|token exchange| GH
A -->|"gRPC: Check / Lookup / Write"| S
A --> DB
S -->|reads + writes| PG
MIG -.->|creates schema| PG
classDef new fill:#dcfce7,stroke:#16a34a,stroke-width:2px,color:#14532d;
The whole delta is the two green blocks — a Verified: after Also in this update
|
What
Adds a new
federated-authorizationexample: a self-contained demo of federated authentication with centralized fine-grained authorization in SpiceDB.Users log in through different identity providers (Keycloak OIDC and GitHub OAuth). Each external account (
keycloak_account,github_account) binds to a canonical internaluser, and a single SpiceDB instance enforcesdocumentaccess control across all of them — so users from different IdPs can share resources with each other.Contents
app.py) with Keycloak + GitHub login flowsspicedb/schema.zed) with the*_account → bound_to → userbinding patterndocker-compose.ymlbringing up SpiceDB, Keycloak, and the appEverything runs locally via
docker compose. Secrets are demo placeholders only; no real credentials are included.Notes
spicedb/tutorials/federated-authorization.