diff --git a/docs/core/authorization.md b/docs/core/authorization.md index a0154c4..34db54a 100644 --- a/docs/core/authorization.md +++ b/docs/core/authorization.md @@ -172,6 +172,13 @@ identities with `type service_account` and admit it in relevant type restriction (`viewer: [user, service_account]`) to put them in the same graph as humans; see the [DSL construct reference](./fga-guide#direct-assignment--type-restrictions). +An **AI agent acting for a user** (an RFC 8693 delegated token) is checked +against *both* `agent:` and `user:`, and needs both to be +allowed — its effective authority is `perms(agent) ∩ perms(user)`. This applies +to `check_permissions` and `list_permissions` alike, and turns on by declaring +`type agent` in your model; deployments without it are unaffected. See +[Agent Identity & Permissions](../enterprise/agent-identity). + ### `check_permissions` — one or many questions A single check is simply a list of one. Results come back **in order** and echo @@ -315,7 +322,14 @@ cookie is used automatically. - **Metrics.** The engine exports Prometheus metrics — `authorizer_fga_checks_total` (allow/deny/error), `authorizer_fga_check_duration_seconds`, and `authorizer_fga_operations_total` — for adoption tracking and denial/error alerting. - See [Metrics & Monitoring → Authorization (FGA) Metrics](./metrics-monitoring#authorization-fga-metrics). + Delegated (agent) callers additionally report + `authorizer_fga_delegated_checks_total{operation,outcome}`, whose `outcome` + label is the only thing that distinguishes *the agent lacks a grant* + (`denied_by_agent`) from *the user genuinely lacks access* (`denied_by_user`) — + and `not_enforced`, which means agent traffic is arriving unconstrained + because the model declares no `agent` type. See + [Metrics & Monitoring → Authorization (FGA) Metrics](./metrics-monitoring#authorization-fga-metrics) + and [Agent Identity & Permissions](../enterprise/agent-identity#observability). - **Learn the model language.** See the OpenFGA docs: [modeling guide](https://openfga.dev/docs/modeling/getting-started) and [configuration language](https://openfga.dev/docs/configuration-language). diff --git a/docs/core/metrics-monitoring.md b/docs/core/metrics-monitoring.md index 6fe6dd3..9c20394 100644 --- a/docs/core/metrics-monitoring.md +++ b/docs/core/metrics-monitoring.md @@ -146,6 +146,7 @@ once. See [Authorization (FGA)](./authorization). | `authorizer_fga_checks_total` | Counter | `operation`, `result` | Access decisions from `check_permissions`. The headline metric for adoption and denial/error alerting. | | `authorizer_fga_check_duration_seconds` | Histogram | `operation` | Latency of the client-facing FGA engine reads. | | `authorizer_fga_operations_total` | Counter | `operation`, `result` | Non-decision FGA operations (model/tuple management, enumeration, reset) by outcome. | +| `authorizer_fga_delegated_checks_total` | Counter | `operation`, `outcome` | Decisions for **delegated** (agent-acting-for-user) callers, and which side of the intersection refused. Ordinary callers never appear here. | **`authorizer_fga_checks_total` labels:** @@ -154,6 +155,32 @@ once. See [Authorization (FGA)](./authorization). | `operation` | `check_permissions` (each supplied pair is counted individually) | | `result` | `allowed` · `denied` · `error` (the engine call failed — fail-closed, so the caller was denied) | +**`authorizer_fga_delegated_checks_total` labels:** + +| Label | Values | +|---|---| +| `operation` | `check_permissions` · `list_permissions` (see note below) | +| `outcome` | `allowed` · `denied_by_agent` · `denied_by_user` · `not_enforced` | + +The `outcome` label is what makes an intersection denial diagnosable: + +- **`denied_by_agent`** — the agent has no grant of its own. The user may well + have access. Fix: grant the **agent** a tuple. +- **`denied_by_user`** — the agent had its grant, the delegating user does not. + This is the Confused Deputy being stopped. Do **not** widen the agent; it + cannot help, and the user genuinely lacks access. +- **`not_enforced`** — a delegated caller arrived but the active model declares + no `agent` type, so the request was authorized as the **user alone**. + +Only `check_permissions` emits `allowed` / `denied_by_agent` / `denied_by_user`. +`list_permissions` intersects object *sets* rather than folding a per-check +decision, so it can only ever report `not_enforced`. + +`not_enforced` is the one to alert on. It is the only outcome that reports a +security property *not* being enforced, and it is silent by construction: the +request succeeds and nothing in the response says the agent was unconstrained. +See [Agent Identity & Permissions](../enterprise/agent-identity#observability). + **`authorizer_fga_check_duration_seconds`** `operation`: `check_permissions` · `list_permissions`. The histogram's `_count` also gives you a call rate per operation for free. **`authorizer_fga_operations_total`** `operation`: `get_model` · `write_model` · `read_tuples` · `write_tuples` · `delete_tuples` · `list_users` · `expand` · `list_permissions` · `reset`. `result`: `success` · `error`. diff --git a/docs/core/security.md b/docs/core/security.md index 7cbfff4..04b79db 100644 --- a/docs/core/security.md +++ b/docs/core/security.md @@ -313,13 +313,39 @@ No flags. The protection applies to: OTP and TOTP secrets are now protected at rest: - **OTPs (email/SMS one-time codes):** stored as HMAC-SHA256 digests - keyed by `--jwt-secret`. The verifier hashes the candidate and + keyed by `--encryption-key`. The verifier hashes the candidate and compares constant-time against the stored digest. The plaintext is only ever held in memory long enough to send the email/SMS body. - **TOTP shared secrets (authenticator app):** encrypted at rest with - AES-256-GCM (using HKDF-derived keys from `--jwt-secret`) and stored + AES-256-GCM (using HKDF-derived keys from `--encryption-key`) and stored with an `enc:v1:` prefix. +:::danger Security advisory — affected versions 2.2.1 through 2.4.0-rc.13 + +In those releases the at-rest key was **not** a separate input. It derived from +`--jwt-secret`, and when a deployment used an asymmetric JWT algorithm +(`RS*`/`ES*` with `--jwt-private-key`/`--jwt-public-key`) and set **no** +`--jwt-secret`, the derivation ran over empty keying material and produced a +**constant compiled into the open-source binary**. Anyone who can read the +repository can compute it. + +**If you ran an affected version with `RS*`/`ES*` and no `--jwt-secret`,** a +copy of your database yields recoverable TOTP seeds and every outstanding OTP +digest — including password-reset codes, whose 10⁶ search space is trivial once +the key is known. + +Remediation, in order: + +1. Set `--encryption-key` to a fresh random value (`openssl rand -hex 32`). +2. Force TOTP re-enrolment for all users — existing ciphertext was written + under the old key and **cannot** be decrypted with the new one. There is no + re-encryption path. +3. Invalidate outstanding password-reset and verification OTPs. + +From 2.4.0 the key is its own input and an `RS*`/`ES*` deployment without one +**refuses to start** rather than falling back silently. +::: + ### Migration There is **no flag** for OTP/TOTP migration. The handlers transparently @@ -357,16 +383,23 @@ this is a non-issue — there are no other replicas to disagree with. ### Key rotation -Both OTP and TOTP at-rest protection are keyed by `--jwt-secret`. -Rotating `--jwt-secret` will lock out every user with an enrolled TOTP -authenticator until they re-enrol, because the existing ciphertext can -no longer be decrypted. If you must rotate the JWT secret, plan a -TOTP re-enrolment campaign (or a temporary fallback path) before doing so. +Both OTP and TOTP at-rest protection are keyed by `--encryption-key`. +Rotating it will lock out every user with an enrolled TOTP authenticator +until they re-enrol, because the existing ciphertext can no longer be +decrypted. If you must rotate, plan a TOTP re-enrolment campaign (or a +temporary fallback path) before doing so. + +`--encryption-key` falls back to `--jwt-secret` when it is not set, which keeps +HMAC (`HS*`) deployments working unchanged — but it means rotating +`--jwt-secret` on such a deployment silently rotates the at-rest key too. Set +`--encryption-key` explicitly to decouple the two; the server warns at startup +when both are set to the same value. + The server logs an explicit error on every TOTP validation that fails to decrypt: ``` -failed to decrypt stored TOTP secret; check that --jwt-secret has not changed since enrollment +failed to decrypt stored TOTP secret; check that --encryption-key (or --jwt-secret, if no encryption key is set) has not changed since enrollment ``` --- diff --git a/docs/core/server-config.md b/docs/core/server-config.md index 4fefe32..f634c92 100644 --- a/docs/core/server-config.md +++ b/docs/core/server-config.md @@ -220,11 +220,32 @@ Additional flags: - **`--refresh-token-expires-in`** (default `2592000`, 30 days): refresh-token lifetime in seconds. Previously hardcoded — now operator-configurable. +- **`--encryption-key`** (no default): the key used to encrypt secrets **at + rest** — TOTP shared secrets and OTP digests. Separate from the JWT signing + material. + In v2, the `_generate_jwt_keys` mutation is deprecated and returns an error; configure keys **only via flags**. -> **Note on key rotation:** `--jwt-secret` is also used to encrypt TOTP shared -> secrets at rest and to HMAC OTPs. Rotating it will lock out every user with -> an enrolled TOTP authenticator until they re-enrol. See +:::warning Breaking change in 2.4.0 — `--encryption-key` + +At-rest encryption no longer derives from `--jwt-secret`. **A deployment using +an asymmetric JWT algorithm (`RS*`/`ES*`) with no `--jwt-secret` will refuse to +start until `--encryption-key` is set.** + +HMAC deployments (`HS256`/`HS384`/`HS512`) are unaffected: the key still falls +back to `--jwt-secret`, so no change is required. + +This closes a vulnerability in **2.2.1 through 2.4.0-rc.13** where that +fallback produced a publicly known constant for asymmetric deployments — read +the [security advisory](./security#otp-and-totp-at-rest) before upgrading, as +remediation includes forcing TOTP re-enrolment. +::: + +> **Note on key rotation:** `--encryption-key` protects TOTP shared secrets at +> rest and HMACs OTPs. Rotating it will lock out every user with an enrolled +> TOTP authenticator until they re-enrol. When it is unset it falls back to +> `--jwt-secret`, so on those deployments rotating the JWT secret rotates the +> at-rest key too. See > [OTP and TOTP at rest](./security#otp-and-totp-at-rest). --- diff --git a/docs/enterprise/agent-identity.md b/docs/enterprise/agent-identity.md new file mode 100644 index 0000000..40c7048 --- /dev/null +++ b/docs/enterprise/agent-identity.md @@ -0,0 +1,249 @@ +--- +sidebar_position: 8 +title: Agent Identity & Permissions +--- + +# Agent Identity & Permissions + +[Token exchange](./token-exchange) gives you a token that carries **both** identities — `sub` is the user, `act.sub` is the agent. This page is about what Authorizer then *does* with that second identity when the agent asks a permission question. + +The short version: an agent's effective authority is the **intersection** of its own permissions and its user's. + +``` +effective authority = perms(agent) ∩ perms(user) +``` + +Evaluated per action, at request time, on both `check_permissions` and `list_permissions`. + +## Why an intersection + +Give an agent a token and it holds the user's authority. Give it its own grants and it holds those. Neither alone is safe: + +- **Only the user's authority** — a compromised or confused agent can do anything its user can. The classic [Confused Deputy](https://en.wikipedia.org/wiki/Confused_deputy_problem): a calendar-reading agent tricked into reading payroll, because its user *can* read payroll. +- **Only the agent's authority** — the agent acts on resources its user was never allowed near, and "on behalf of Alice" becomes a fiction. + +Intersecting both means an agent can only ever do things that **it** is trusted with **and** that its **user** could have done themselves. Neither identity can widen the other. This matches how WorkOS, Auth0 FGA and OpenFGA model agentic access. + +## Turning it on + +**Declare `type agent` in your authorization model.** That is the whole opt-in — there is no flag. + +```dsl +model + schema 1.1 + +type user +type agent + +type document + relations + define viewer: [user, agent] + define can_view: viewer +``` + +Declaring the type IS the opt-in because the feature is meaningless without a model that can express agent grants, and because of how OpenFGA fails: checking `agent:x` against a model with **no** `agent` type does not return `false`, it **errors** — and permission checks fail closed on errors. A flag that could be switched on against an unprepared model would deny every permission check for every delegated caller: a total authorization outage, not a graceful degradation. Auto-detection makes that state unreachable. + +Detection is cached per authorization-model id, so writing a new model version takes effect immediately. The model-id lookup itself still runs per delegated request; only the type enumeration is cached. + +:::note Before you declare the type +The moment `type agent` appears in your model, every delegated caller must ALSO satisfy the agent half. Grant your agents before you deploy the model, or their calls start being denied. The [`denied_by_agent` metric](#observability) tells you exactly this is happening. +::: + +## Granting an agent + +The agent's subject is `agent:` — the `client_id` of the [`service_account`](../core/client-registry) that authenticated the exchange, which is the same value that appears as `act.sub` on the token. Grant it like any other subject: + +```bash +# Alice can view the document +curl … -d '{"query":"mutation { _fga_write_tuples(params: { tuples: [ + { user: \"user:alice-id\", relation: \"viewer\", object: \"document:q4-plan\" } +]}) { message } }"}' + +# The calendar agent may view it too +curl … -d '{"query":"mutation { _fga_write_tuples(params: { tuples: [ + { user: \"agent:calendar-agent-client-id\", relation: \"viewer\", object: \"document:q4-plan\" } +]}) { message } }"}' +``` + +Now `check_permissions` with a delegated token returns `allowed: true` only while **both** tuples exist. Delete either one and it is `false`. + +### One user, many agents + +Agents are independent subjects, so a user can delegate to as many as they like and each carries its own, separately revocable reach: + +| Grant | Effect | +|-------|--------| +| `agent:calendar-bot` → `viewer` on `document:*` | that agent may read documents Alice can read | +| `agent:finance-bot` → nothing | that agent can do nothing for Alice, whatever her own access | + +Revoking one agent's tuple does not touch the user or any other agent. + +### Agent-to-agent (multi-hop) + +Only the **immediate** actor participates in the decision. In a chain `app → agent → sub-agent`, the check is `perms(sub-agent) ∩ perms(user)` — prior actors nested deeper in `act` are recorded for audit but never grant or deny. They were asserted upstream, not verified here, so treating them as authority would let a middle hop vouch for itself. + +Since scope attenuation is monotonic across hops (see [token exchange](./token-exchange#scope-attenuation)), a longer chain can only ever be narrower. + +## Calling Authorizer's own API + +A delegated token is bound to exactly one `resource` (RFC 8707) and is only accepted there. To let an agent ask **Authorizer** about its own authority, exchange for Authorizer's own URL: + +```bash +curl -X POST $AUTHORIZER_URL/oauth/token \ + -u "$AGENT_CLIENT_ID:$AGENT_CLIENT_SECRET" \ + -d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \ + -d "subject_token=$USER_ACCESS_TOKEN" \ + -d "subject_token_type=urn:ietf:params:oauth:token-type:access_token" \ + -d "actor_token=$AGENT_TOKEN" \ + -d "actor_token_type=urn:ietf:params:oauth:token-type:access_token" \ + -d "resource=$AUTHORIZER_URL" # ← Authorizer itself +``` + +Present the result as a normal bearer token: + +```graphql +query { + check_permissions(params: { + checks: [{ relation: "can_view", object: "document:q4-plan" }] + }) { + results { relation object allowed } # intersected: agent AND user + } +} +``` + +A token exchanged for `https://calendar.example` will **not** authenticate here, and vice versa. That is the audience binding doing its job, not a misconfiguration. + +## What an agent may do: scope + +The intersection above decides **which resources** an agent may reach. A second, +independent gate decides **which operations** it may call at Authorizer's own +API — the token's `scope` claim, enforced per operation. + +| Operation | Required scope | +|---|---| +| `check_permissions`, `list_permissions` | `openid` | +| `profile`, `meta` | `openid` | +| `update_profile` | `authorizer:profile:write` | +| `deactivate_account` | `authorizer:account:delete` | +| anything else | **denied** — see below | + +Read-only identity and permission queries need only `openid`, which every token +carries, so the questions an agent must be able to ask to function at all just +work. That set is exactly the [MCP](../core/mcp) tool surface. + +Mutating operations need a scope **no client requests by default**. Since a +delegated token's scope is `subject_token.scope ∩ agent.allowed_scopes`, granting +one takes *both* parties: + +1. the delegating user's own token must carry the scope (request it at login), **and** +2. an admin must add it to the agent's `allowed_scopes` ceiling. + +Neither can widen an agent alone. A refusal is `insufficient_scope` +([RFC 6750 §3.1](https://www.rfc-editor.org/rfc/rfc6750#section-3.1)). + +:::note Fail closed by default +Any operation not in that table is **denied to delegated callers**, whatever +scope they hold. New operations are therefore unreachable by agents until +someone deliberately clears them — the opposite of an allowlist that quietly +widens when a contributor forgets to update it. +::: + +### Why first-party tokens are not gated + +A first-party `scope` is whatever the client asked for at login — it is not +validated against an allow-list — so it is a hint, not a boundary. Enforcing it +would break existing clients and buy no security. + +A delegated token is different: its ceiling comes from `allowed_scopes` on the +agent's `service_account`, which only an admin can set. That is what makes the +same claim a real boundary, and it is the same asymmetry Microsoft draws between +delegated and application permissions. + +### The subject cannot be changed + +`check_permissions` and `list_permissions` accept an optional `user`. For a delegated caller it may only ever be the caller's own subject — supplied or not, the agent half is still applied. Naming any other subject is rejected outright, even if the request also carries an admin credential. An agent must not be able to shed its own constraint, or probe access that neither half of its intersection has. + +## Revocation + +| Lever | Stops a delegated token at Authorizer's API? | Stops it at a downstream resource server? | +|-------|---|---| +| Its 5-minute TTL expiring | Yes | Yes | +| User logs out (that session) | Yes | No | +| Password reset / email change | Yes | No | +| Admin wipes the user's sessions | Yes | No | +| User revoked or deprovisioned ([SCIM](./scim) `active:false`) | Yes | No | +| Service-account subject deactivated | Yes | No | +| Deleting the agent's FGA tuples | Yes — the next check denies | n/a | +| **The agent's own service account deactivated** | **No** — see below | No | + +:::warning Deactivating the agent does not stop tokens it already holds +Only the **subject** is checked for liveness at validation time; the acting +agent is not. Deactivating an agent's service account blocks the **next** +exchange, but a token it minted moments earlier keeps working until its +5-minute TTL expires. Do not treat "disable the agent" as immediate +containment — to cut an agent off now, delete its FGA tuples, which the very +next check honours. +::: + +A delegated token carries an opaque `sid` naming the session it was derived from, so at Authorizer's own API it is exactly as revocable as the credential that seeded it. A **downstream resource server** verifies the token offline against [`/.well-known/jwks.json`](../core/oauth2-oidc) and cannot see any of that — there, the short TTL remains the only bound. Keep it that way: do not build a resource server that treats a delegated token as long-lived. + +## Audit + +A delegated action is recorded as the **agent**, with the user preserved alongside: + +| Field | Value | +|-------|-------| +| `actor_id` | the agent's `client_id` | +| `actor_type` | `agent` | +| `actor_email` | *(empty — an agent has no mailbox)* | +| `metadata` | gains `delegated_user_id=…` and, when known, `delegated_user_email=…` | + +Without this an agent's actions are indistinguishable from the user's own — same id, same type, no trace anything automated was involved. RFC 8693 §1.1 draws exactly this line: delegation is "A representing B" with A keeping its identity, as against impersonation where A is indistinguishable from B. It cannot be reconstructed after the fact, because the information was never written. + +## Observability {#observability} + +`authorizer_fga_delegated_checks_total{operation, outcome}` counts every delegated decision. The `outcome` label is what makes an intersection denial diagnosable: + +| `outcome` | Meaning | What to do | +|-----------|---------|-----------| +| `allowed` | both halves permitted | — | +| `denied_by_agent` | the agent has no grant; the user may well have access | grant the **agent** a tuple | +| `denied_by_user` | the agent had its grant, the user does not have access | **do not** widen the agent — this is the Confused Deputy case working as intended | +| `not_enforced` | a delegated caller arrived but the model declares no `agent` type, so it was authorized as the **user alone** | declare `type agent` and grant your agents | + +`not_enforced` is the one to alert on: it is the only outcome that reports a security property *not* being enforced, and it is silent by construction — the request succeeds and nothing in the response says the agent was unconstrained. + +`allowed`, `denied_by_agent` and `denied_by_user` are emitted by +**`check_permissions` only**. `list_permissions` intersects object *sets* +rather than folding a per-check decision, so the only outcome it can ever +report is `not_enforced`. + +Ordinary (non-delegated) callers do not appear in this series at all; they are counted in `authorizer_fga_checks_total` exactly as before. + +## What does not change + +Delegation is additive. If your model has no `agent` type, every one of these behaves precisely as it did before: + +- OIDC, SAML, SCIM, OAuth 2.1 flows and `client_credentials` (M2M). +- `check_permissions` / `list_permissions` for user and service-account callers. +- A machine token still resolves to `service_account:`; a delegated token always resolves to `user:` and never to a service account, regardless of what it carries. + +## Failure modes + +Everything fails closed: + +| Situation | Result | +|-----------|--------| +| The model cannot be read to detect the agent type | the delegated request is **denied** (ordinary callers are unaffected) | +| The engine errors on any check | the whole call is denied | +| The agent's `client_id` has an unexpected shape | denied — it is never concatenated into a subject string unchecked | +| The delegating user cannot be confirmed active | denied | +| The originating session is gone | denied | + +## See also + +- [Token Exchange & Delegation](./token-exchange) — how the token is minted, scoped and chained +- [Authorization (FGA)](../core/authorization) — the model, tuples and the permission APIs +- [Client Registry](../core/client-registry) — registering an agent as a `service_account` +- [`with-agent-permissions`](https://github.com/authorizerdev/examples/tree/main/with-agent-permissions) — **runnable demo of everything on this page**: the intersection, enumeration, the explicit-`user` gate, per-agent revocation, and the model-declares-agent opt-in +- [`with-agent-delegation`](https://github.com/authorizerdev/examples/tree/main/with-agent-delegation) — runnable multi-hop example of minting the token itself (scope attenuation, `act` chain) diff --git a/docs/enterprise/token-exchange.md b/docs/enterprise/token-exchange.md index 39a6451..8dd4a24 100644 --- a/docs/enterprise/token-exchange.md +++ b/docs/enterprise/token-exchange.md @@ -98,8 +98,12 @@ A runnable 4-hop example (`orchestrator → research-agent → crm-reader → ex ## Resource binding & validation - `aud` on the delegated token is the single `resource` — it is only valid at that resource server. -- Delegated tokens are **stateless**: the downstream resource server validates them via local JWT verification against [`/.well-known/jwks.json`](../core/oauth2-oidc) (checking `iss`, `aud`, `exp`, `scope`) and/or `POST /oauth/introspect`. -- They are **not refreshable**. TTL is fixed at **5 minutes**; the agent re-exchanges when it needs more time. The short TTL is the baseline revocation story — revoking the user stops the *next* exchange immediately (see below), and in-flight tokens die within the window. +- Delegated tokens are **stateless at the resource server**: it validates them via local JWT verification against [`/.well-known/jwks.json`](../core/oauth2-oidc), checking `iss`, `aud`, `exp` and `scope`. +- **`/oauth/introspect` cannot be used for a delegated token.** Introspection answers only for a token whose `aud` is the *authenticated caller's* `client_id`, and a delegated token's `aud` is the RFC 8707 resource URI — so a resource server presenting its own credentials always gets `{"active": false}`. That is correct per RFC 7662 §2.2 (it must not become an oracle), but it means introspection is useless as a revocation signal here: a resource server that treats `active: false` as "revoked" will reject every valid delegated token. +- They are **not refreshable**. TTL is fixed at **5 minutes**; the agent re-exchanges when it needs more time. At the resource server the short TTL is the entire revocation story — revoking the user stops the *next* exchange immediately (see below), and in-flight tokens die within the window. +- The token carries an opaque `sid` naming the session it was derived from. At **Authorizer's own API** that makes it as revocable as the credential that seeded it — logout, password reset and admin session wipes all stop it on the next call. A resource server verifying offline cannot see this; do not design one that assumes it can. + +To let an agent call **Authorizer itself** (e.g. to ask `check_permissions` about its own authority), exchange with `resource` set to your Authorizer URL. What happens then — the `perms(agent) ∩ perms(user)` intersection — is covered in [Agent Identity & Permissions](./agent-identity). ## Security invariants diff --git a/docs/enterprise/workload-identity.md b/docs/enterprise/workload-identity.md index 6ab47ce..c770097 100644 --- a/docs/enterprise/workload-identity.md +++ b/docs/enterprise/workload-identity.md @@ -1,5 +1,5 @@ --- -sidebar_position: 8 +sidebar_position: 9 title: Workload Identity --- diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index ba5764a..1199872 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -185,12 +185,12 @@ See [Server Configuration](../core/server-config) for all flags and hardening op When talking to a v2 server, use: -- **`@authorizerdev/authorizer-js` v3** (`^3.0.0-rc.1` or compatible v3) -- **`@authorizerdev/authorizer-react` v2** (`^2.0.0-rc.1` or compatible v2) +- **`@authorizerdev/authorizer-js` v3** (`^3.3.0` or compatible v3) +- **`@authorizerdev/authorizer-react` v2** (`^2.0.0` or compatible v2) ```bash -npm install @authorizerdev/authorizer-js@^3.0.0-rc.1 \ - @authorizerdev/authorizer-react@^2.0.0-rc.1 +npm install @authorizerdev/authorizer-js@^3.3.0 \ + @authorizerdev/authorizer-react@^2.0.0 ``` If you used types directly from `authorizer-js`, rename them for v2: diff --git a/docs/introduction.md b/docs/introduction.md index 9572a62..1d1866d 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -97,15 +97,15 @@ See [Databases](./core/databases) for connection string formats. ### Frontend SDKs - [JavaScript / TypeScript](https://github.com/authorizerdev/authorizer-js) — v3.2.1; user + admin client; GraphQL + REST protocols -- [React](https://github.com/authorizerdev/authorizer-react) — v2.1.0; `protocol` prop; pre-built login/signup/MFA components +- [React](https://github.com/authorizerdev/authorizer-react) — v2.x; `protocol` prop; pre-built login/signup/MFA components - [Vue](https://github.com/authorizerdev/authorizer-vue) - [Svelte](https://github.com/authorizerdev/authorizer-svelte) -- [Flutter](https://github.com/nickolasgomez/authorizer-flutter-sdk) (community) +- [Flutter](https://github.com/authorizerdev/authorizer-flutter-sdk) — not released yet; no package on pub.dev ### Backend SDKs - [Go](https://github.com/authorizerdev/authorizer-go) — user + admin client; protocol selection (gRPC / REST / GraphQL); FGA helpers -- [Python](https://github.com/authorizerdev/authorizer-python) — v0.2.0; sync + async; admin API (`pip install authorizer-py`) +- [Python](https://github.com/authorizerdev/authorizer-python) — v0.3.0 pre-release; sync + async; admin API (`pip install --pre authorizer-py`) - [Node.js](https://github.com/authorizerdev/authorizer-js) — same package as the frontend SDK, works server-side See the [SDK reference](./sdks/authorizer-js) for usage docs. diff --git a/docs/migration/v1-to-v2.md b/docs/migration/v1-to-v2.md index 203ad78..711d54e 100644 --- a/docs/migration/v1-to-v2.md +++ b/docs/migration/v1-to-v2.md @@ -571,14 +571,14 @@ Or use a script inside the image that maps env to flags and then runs `./authori Upgrade: ```bash -npm install @authorizerdev/authorizer-js@^3.0.0-rc.1 +npm install @authorizerdev/authorizer-js@^3.3.0 # or -pnpm add @authorizerdev/authorizer-js@^3.0.0-rc.1 +pnpm add @authorizerdev/authorizer-js@^3.3.0 ``` ### `@authorizerdev/authorizer-react` (v2) -- **Version:** use **authorizer-react** `2.0.0-rc.1` (or compatible v2) with **authorizer-js** v3. +- **Version:** use **authorizer-react** `^2.0.0` (or compatible v2) with **authorizer-js** v3. - **Breaking:** build system (tsdx to tsup), output paths (for example `dist/index.cjs`, `dist/index.mjs`), and **Node.js >= 18**. - **Types:** same renames as authorizer-js (for example `SignUpRequest`, `LoginRequest`). diff --git a/docs/sdks/authorizer-flutter/index.md b/docs/sdks/authorizer-flutter/index.md index 5a77031..a7e2a7f 100644 --- a/docs/sdks/authorizer-flutter/index.md +++ b/docs/sdks/authorizer-flutter/index.md @@ -5,8 +5,11 @@ title: Getting Started # Getting Started -The Authorizer Flutter SDK documentation is coming soon. - -In the meantime, you can find the source code and usage instructions on the GitHub repository: - -[https://github.com/authorizerdev/authorizer-flutter-sdk](https://github.com/authorizerdev/authorizer-flutter-sdk) +The Authorizer Flutter SDK has not been released yet. The +[repository](https://github.com/authorizerdev/authorizer-flutter-sdk) is a placeholder — it +contains no source code, and there is no package on pub.dev. + +Until it ships, call the server directly from Dart: every SDK operation is available over +the [REST endpoints](/core/endpoints) and the [GraphQL API](/core/graphql-api), which work +with any HTTP client (`package:http`, Dio, and so on). For browser-based OAuth flows, see +[OAuth 2.0 / OIDC](/core/oauth2-oidc). diff --git a/docs/sdks/authorizer-go/admin.md b/docs/sdks/authorizer-go/admin.md index 4b1dbd6..0580a6b 100644 --- a/docs/sdks/authorizer-go/admin.md +++ b/docs/sdks/authorizer-go/admin.md @@ -77,12 +77,11 @@ for _, u := range res.Users { ``` Request/response types for the proto-backed methods come from the generated package -`authorizerv1 "github.com/authorizerdev/authorizer-proto-go/authorizer/v1"`. -It lives under `internal/`, so Go's internal-package rule means it can only be imported by -code whose import path is rooted at `github.com/authorizerdev/authorizer-go` — e.g. this -module's own `examples/` tree, not an external application module. The Go-native admin -operations (organizations, org SSO connections, SCIM, org domains — see below) avoid this -by declaring their request/response types directly in the top-level `authorizer` package +`authorizerv1 "github.com/authorizerdev/authorizer-proto-go/authorizer/v1"`. It is a +public module of its own, so application code can import it directly — it is pulled in +transitively when you `go get github.com/authorizerdev/authorizer-go/v2`. The Go-native +admin operations (organizations, org SSO connections, SCIM, org domains — see below) +declare their request/response types directly in the top-level `authorizer` package instead. ### Admin client options diff --git a/docs/sdks/authorizer-go/example.md b/docs/sdks/authorizer-go/example.md index 4c38b35..a5f2693 100644 --- a/docs/sdks/authorizer-go/example.md +++ b/docs/sdks/authorizer-go/example.md @@ -14,7 +14,7 @@ import ( "net/http" "strings" - "github.com/authorizerdev/authorizer-go" + authorizer "github.com/authorizerdev/authorizer-go/v2" "github.com/gin-gonic/gin" ) diff --git a/docs/sdks/authorizer-go/index.md b/docs/sdks/authorizer-go/index.md index 09cfa36..4323878 100644 --- a/docs/sdks/authorizer-go/index.md +++ b/docs/sdks/authorizer-go/index.md @@ -7,7 +7,7 @@ title: Getting Started ## Official Documentation -For detailed explanation of each function, check the authorizer-go [pkg.go.dev docs](https://pkg.go.dev/github.com/authorizerdev/authorizer-go). +For detailed explanation of each function, check the authorizer-go [pkg.go.dev docs](https://pkg.go.dev/github.com/authorizerdev/authorizer-go/v2). **Prerequisite**: You need an Authorizer instance running. See the [deployment guides](../../deployment/) for setup options. diff --git a/docs/sdks/authorizer-python/admin.md b/docs/sdks/authorizer-python/admin.md index 0a9e753..0ba66fa 100644 --- a/docs/sdks/authorizer-python/admin.md +++ b/docs/sdks/authorizer-python/admin.md @@ -39,7 +39,7 @@ token = client.login(LoginRequest(email="user@example.com", password="Abc@123")) gRPC requires the optional extra: ```bash -pip install 'authorizer-py[grpc]' +pip install --pre 'authorizer-py[grpc]' ``` It listens on its own port, separate from the HTTP URL. When `grpc_endpoint` is omitted, diff --git a/docs/sdks/authorizer-python/index.md b/docs/sdks/authorizer-python/index.md index 9e2f8df..f938b16 100644 --- a/docs/sdks/authorizer-python/index.md +++ b/docs/sdks/authorizer-python/index.md @@ -49,9 +49,18 @@ Note the `--client-id` value — you will need it in the SDK configuration below ## Step 3: Install the package ```bash -pip install authorizer-py +pip install --pre authorizer-py ``` +:::warning Use the 0.3.0 pre-release with server 2.4.0 + +The latest stable release on PyPI is `0.2.0`, which predates server 2.4.0: it lacks +`skip_mfa_setup` and still sends the old `PaginatedRequest` GraphQL type for paginated +admin queries (renamed to `PaginationRequest` in 2.4.0). Install with `--pre` to pick up +the `0.3.0` pre-release until it goes stable. + +::: + The import name is `authorizer`: ```python diff --git a/sidebars.ts b/sidebars.ts index f856202..4d83e98 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -45,6 +45,7 @@ const sidebars: SidebarsConfig = { 'enterprise/saml-idp', 'enterprise/scim', 'enterprise/token-exchange', + 'enterprise/agent-identity', 'enterprise/workload-identity', ], },