Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 66 additions & 17 deletions docs/core/authorization.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ This page covers:
5. [Admin GraphQL API](#5-admin-graphql-api) — the `_fga_*` operations the dashboard uses.
6. [SDKs](#6-sdks) and [operational notes](#7-operational-notes).
7. [Using FGA from your application](#8-using-fga-from-your-application) — middleware, the tuple lifecycle, list filtering.
8. [Real-world recipes](#9-real-world-recipes) — document sharing, multi-tenant SaaS, job roles, time-bound access, block lists.
8. [Real-world recipes](#9-real-world-recipes) — document sharing, multi-tenant SaaS, job roles, time-bound access, block lists, permission-aware retrieval for RAG/AI agents.
9. [Cheat sheet](#10-cheat-sheet) — app event → FGA operation.

---
Expand Down Expand Up @@ -125,12 +125,12 @@ A **relationship tuple** is a single fact: _`user` is `relation` of `object`_. T
the data that actually grants access; add and remove them any time without touching the
model.

```text
user:1b9d… viewer document:1 → this user can view document 1
user:2c8e… owner document:1 → this user owns document 1 (⇒ editor, viewer)
team:9#member viewer document:1 → every member of team:9 can view it
user:* viewer document:5 → document 5 is public
```
| Tuple | Meaning |
|-------|---------|
| `user:1b9d…` `viewer` `document:1` | This user can view document 1 |
| `user:2c8e…` `owner` `document:1` | This user owns document 1 (⇒ editor, viewer) |
| `team:9#member` `viewer` `document:1` | Every member of team:9 can view it |
| `user:*` `viewer` `document:5` | Document 5 is public |

:::info Identify users by id, not name
The subject is `user:<id>` — the **Authorizer user id** (the token's `sub` claim,
Expand Down Expand Up @@ -162,6 +162,16 @@ cookie. An optional `user` ("type:id", or a bare id treated as `user:<id>`) is
honored only when the caller is a **super-admin** or when it **equals the
caller's own token subject**; anything else is rejected, never silently ignored.

A **machine caller** (a `client_credentials` token from a `service_account` client)
has its own subject too: it resolves to `service_account:<client_id>`, not
`user:<sub>`, so a machine credential presenting its own token can self-check
`service_account` tuples the same way a human token self-checks `user` ones. An
RFC 8693 delegated/token-exchange token stays a `user:<sub>` subject regardless —
only an autonomous machine token classifies as `service_account:`. Model machine
identities with `type service_account` and admit it in relevant type restrictions
(`viewer: [user, service_account]`) to put them in the same graph as humans; see
the [DSL construct reference](./fga-guide#direct-assignment--type-restrictions).

### `check_permissions` — one or many questions

A single check is simply a list of one. Results come back **in order** and echo
Expand Down Expand Up @@ -317,16 +327,16 @@ cookie is used automatically.
Your application keeps doing what it does; Authorizer answers one extra question per
request: **"may this user do this to this object?"**

```text
┌──────────────┐ login ┌─────────────┐
Your app │ ───────► │ Authorizer │
│ (frontend) │ ◄─────── │ │
└──────┬───────┘ token │ ┌────────┐ │
│ API call + token │ │ OpenFGA│ │
┌──────▼───────┐ │ │ engine │ │
│ Your backend │ ───────► │ └────────┘ │
│ │ check_ │ │
└──────────────┘permissions└─────────────┘
```mermaid
flowchart LR
A["Your app<br/>(frontend)"] -- login --> Auth
Auth -- token --> A
A -- "API call + token" --> C[Your backend]
C -- check_permissions --> Auth

subgraph Auth[Authorizer]
D[OpenFGA engine]
end
```

There are exactly **two touchpoints**:
Expand Down Expand Up @@ -709,6 +719,45 @@ check_permissions(can_view, document:7) with 5f1b…'s token → denied

---

### Permission-aware retrieval (RAG / AI agents) {#permission-aware-retrieval-rag--ai-agents}

**Scenario.** A RAG pipeline or AI agent must never let an LLM see, cite, or summarize
a chunk the requesting user isn't allowed to read. The document-collaboration model
above is enough — the interesting part is *where* you call FGA relative to ranking.

**Two enforcement strategies:**

| | Post-filter | Pre-filter |
| --- | --- | --- |
| How | Rank the whole corpus, then batch `check_permissions` on the top-k candidates | `list_permissions` first, filter the corpus, **then** rank |
| Cost | One batched check per query, scoped to candidates only | One list call; grows with the caller's grant count |
| Failure mode | **Candidate starvation** — if every top-k hit is denied, few or zero chunks survive | Large grant lists for privileged callers (e.g. an org admin) |
| Use when | Corpus ≫ per-user access | Per-user access is small/medium |

```text
# Post-filter: rank first, then gate the candidates
candidates = bm25_rank(whole_corpus, query, k=4)
allowed = check_permissions(checks: [can_view document:<doc> for doc in candidates])
context = [c for c in candidates if allowed[c.doc]]

# Pre-filter: gate first, then rank only what's visible
visible = list_permissions(relation: can_view, object_type: document).objects
context = bm25_rank(corpus_filtered_to(visible), query, k=4)
```

As with every other FGA call, **fail closed**: an engine error aborts the query rather
than degrading to "show everything," and a `truncated: true` on `list_permissions`
must not be treated as the caller's complete allow-list (don't rank as if the tail
doesn't exist — page or fall back to post-filter instead).

This is also the pattern behind the [MCP server](./mcp)'s `check_permissions` /
`list_permissions` tools: the same two calls, invoked by the model itself instead of
your retrieval backend. See the runnable
[`with-rag-fga`](https://github.com/authorizerdev/examples/tree/main/with-rag-fga)
example (Python, both strategies, timed side by side).

---

---

## 10. Cheat sheet
Expand Down
33 changes: 32 additions & 1 deletion docs/core/client-registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ mutation {
) {
client {
id
client_id
name
allowed_scopes
is_active
Expand Down Expand Up @@ -103,7 +104,7 @@ query {
}
```

Client responses never contain a secret or secret hash — there is no `client_secret` field on the `Client` type by design.
Client responses never contain a secret or secret hash — there is no `client_secret` field on the `Client` type by design. The `Client` type does return `client_id` (the public OAuth identifier, distinct from the internal `id`) — add it to any of the queries/mutations above when your caller needs the value to present at `/oauth/token`.

## Authenticating: `client_credentials`

Expand Down Expand Up @@ -133,6 +134,36 @@ Rules:
- No `refresh_token`, no `id_token` — re-authenticate on expiry.
- Discovery (`/.well-known/openid-configuration`) advertises `client_credentials` in `grant_types_supported`.
- Success and failure are both audited (`token.client_credentials` / `token.client_credentials_failed` audit events).
- A transient registry lookup failure (e.g. a busy/unreachable database) is never reported as `invalid_client` — that would tell a legitimate caller its credentials are permanently wrong. It returns `503` with `temporarily_unavailable` instead, and is not counted as a security event.

## Service accounts as FGA subjects

A `service_account` client authenticated via `client_credentials` can itself be an [OpenFGA](../core/fga-guide) subject. When your authorization model declares a `service_account` type, the machine caller's own `check_permissions` / `list_permissions` calls resolve to the subject `service_account:<client_id>` — the client's public `client_id`, never its internal `id`:

```dsl
type service_account

type document
relations
define viewer: [user, service_account]
define can_view: viewer
```

```graphql
# Admin: grant the service account viewer on a document, keyed on its client_id
mutation {
_fga_write_tuples(params: {
tuples: [{ user: "service_account:payments-worker-client-id", relation: "viewer", object: "document:1" }]
}) { message }
}
```

```bash
# The service account's own client_credentials token then passes check_permissions
# as service_account:payments-worker-client-id — no explicit "user" needed.
```

A deactivated (`is_active: false`) service account is denied even while its already-issued token remains cryptographically valid — FGA is an additional, independent gate. A machine caller may only self-pin its own subject (`user: "service_account:<its own client_id>"` in `check_permissions`); pinning any other subject is rejected, exactly like a human caller. See the [FGA guide](../core/fga-guide) for the full model/tuple/check walkthrough.

## Secretless authentication: client assertions & trusted issuers

Expand Down
9 changes: 9 additions & 0 deletions docs/core/fga-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,21 @@ is a one-click example. Save to install.
curl http://localhost:8080/graphql \
-H 'Content-Type: application/json' \
-H 'X-Authorizer-Admin-Secret: admin' \
-H 'Origin: http://localhost:8080' \
-d '{
"query": "mutation ($params: FgaWriteModelInput!) { _fga_write_model(params: $params) { id } }",
"variables": { "params": { "dsl": "model\n schema 1.1\n\ntype user\n\ntype document\n relations\n define owner: [user]\n define editor: [user] or owner\n define viewer: [user] or editor\n define can_view: viewer\n define can_edit: editor\n define can_delete: owner" } }
}'
```

:::note CSRF: send an Origin header
`POST /graphql` is CSRF-protected — state-changing requests need a matching
`Origin` (or `Referer`) header even when called server-to-server with the
admin secret. Both example apps in the [examples repo](https://github.com/authorizerdev/examples)
(`with-fga-permissions`, `with-fga-advanced`) set `Origin` on every request
for exactly this reason.
:::

### Step 2 — Grant access (write tuples)

Priya creates document 1 and shares it: Marco can edit, Sam can view.
Expand Down
Loading