diff --git a/docs/core/authorization.md b/docs/core/authorization.md index 6711aa8..a0154c4 100644 --- a/docs/core/authorization.md +++ b/docs/core/authorization.md @@ -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. --- @@ -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:` — the **Authorizer user id** (the token's `sub` claim, @@ -162,6 +162,16 @@ cookie. An optional `user` ("type:id", or a bare id treated as `user:`) 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:`, not +`user:`, 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:` 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 @@ -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
(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**: @@ -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: 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 diff --git a/docs/core/client-registry.md b/docs/core/client-registry.md index d1d80f3..f0b4490 100644 --- a/docs/core/client-registry.md +++ b/docs/core/client-registry.md @@ -65,6 +65,7 @@ mutation { ) { client { id + client_id name allowed_scopes is_active @@ -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` @@ -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:` — 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:"` 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 diff --git a/docs/core/fga-guide.md b/docs/core/fga-guide.md index 65e0bf5..41fb48e 100644 --- a/docs/core/fga-guide.md +++ b/docs/core/fga-guide.md @@ -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. diff --git a/docs/core/graphql-api.md b/docs/core/graphql-api.md index 4996551..af840c6 100644 --- a/docs/core/graphql-api.md +++ b/docs/core/graphql-api.md @@ -95,6 +95,7 @@ It returns `Meta` type with the following possible values | `is_google_login_enabled` | It gives information if google login is configured or not | | `is_github_login_enabled` | It gives information if github login is configured or not | | `is_facebook_login_enabled` | It gives information if facebook login is configured or not | +| `is_discord_login_enabled` | It gives information if discord login is configured or not | | `is_email_verification_enabled` | It gives information if email verification is enabled or not | | `is_basic_authentication_enabled` | It gives information, if basic auth is enabled or not | | `is_magic_link_login_enabled` | It gives information if password less login is enabled or not | @@ -110,6 +111,7 @@ query { is_google_login_enabled is_github_login_enabled is_facebook_login_enabled + is_discord_login_enabled is_email_verification_enabled is_basic_authentication_enabled is_magic_link_login_enabled @@ -665,6 +667,8 @@ mutation { Mutation to reset the password. For security reasons this is 2 step process, we send email to the registered and then the are redirect to reset password url through the link in that email. In the second step, it accepts `params` of type `ResetPasswordInput` with following keys as parameter +On success, this also synchronously revokes all of the user's existing sessions and refresh tokens — see [Refresh tokens](./security#refresh-tokens). + **Request Params** | Key | Description | Required | @@ -790,6 +794,84 @@ mutation { } ``` +### `skip_mfa_setup` + +Completes an in-progress, token-withheld MFA first-time-setup offer by recording that the caller explicitly declined it, then issues the access token that was withheld (see [Withheld-token first-time setup](../core/security#withheld-token-first-time-setup)). Accepts `params` of type `SkipMfaSetupRequest`. + +**Request Params** + +| Key | Description | Required | +| -------------- | ------------------------------------------------------------- | -------- | +| `email` | Email address of the user (resolves the pending MFA session) | false | +| `phone_number` | Phone number of the user (resolves the pending MFA session) | false | +| `state` | Authorization-code-grant state, same as `verify_otp` | false | + +Either `email` or `phone_number` is required. Returns `AuthResponse` (same shape as [`verify_otp`](#verify_otp)). Fails with an error when MFA is org-enforced (`--enforce-mfa`) — enforcement is never skippable. + +```graphql +mutation { + skip_mfa_setup(params: { email: "foo@bar.com" }) { + access_token + expires_in + message + } +} +``` + +### `lock_mfa` + +Records that the caller lost access to their only enrolled MFA factor(s), permanently locking the account until an admin recovers it (see [Lockout & admin recovery](../core/security#lockout--admin-recovery)). Only allowed when the caller has no verified email/SMS OTP fallback enrolled — use that instead of locking if one exists. Does **not** issue a token. Accepts `params` of type `LockMfaRequest`. + +**Request Params** + +| Key | Description | Required | +| -------------- | ----------------------- | -------- | +| `email` | Email address of the user | false | +| `phone_number` | Phone number of the user | false | + +Either `email` or `phone_number` is required. Returns `Response`. + +```graphql +mutation { + lock_mfa(params: { email: "foo@bar.com" }) { + message + } +} +``` + +### `email_otp_mfa_setup` + +Sends a one-time code to the caller's own email and creates an unverified email-OTP MFA enrollment; verify it with [`verify_otp`](#verify_otp) to activate. Accepts `params` of type `OtpMfaSetupRequest`. Dual-mode: with a valid bearer token/session, `params` is ignored and the code goes to the already-authenticated caller (the settings-screen "add a second factor" flow); without one, `email`/`phone_number` resolve the pending MFA session cookie (the withheld first-time-setup flow). + +**Request Params** + +| Key | Description | Required | +| -------------- | -------------------------------------------------------------------- | -------- | +| `email` | Email address (MFA-session-cookie mode only; ignored when bearer-authenticated) | false | +| `phone_number` | Phone number (MFA-session-cookie mode only; ignored when bearer-authenticated) | false | + +Returns `Response`. + +```graphql +mutation { + email_otp_mfa_setup(params: { email: "foo@bar.com" }) { + message + } +} +``` + +### `sms_otp_mfa_setup` + +Sends a one-time code to the caller's own phone number and creates an unverified SMS-OTP MFA enrollment. Same dual-mode permissions and `verify_otp` activation relationship as [`email_otp_mfa_setup`](#email_otp_mfa_setup). Accepts `params` of type `OtpMfaSetupRequest` (same fields as above). Returns `Response`. + +```graphql +mutation { + sms_otp_mfa_setup(params: { phone_number: "+10000000000" }) { + message + } +} +``` + ### `verify_totp` Mutation to verify TOTP generated by QR code. It accepts `params` of type `VerifyTOTPRequest` with following keys as parameter @@ -998,7 +1080,7 @@ query { limit: 10 } }) { - pagination: { + pagination { offset total page @@ -1052,24 +1134,6 @@ query { It returns the whole `User` object mentioned in [profile](#profile) query section -#### `_update_user` - -Mutation to update environment variables. It accepts `params` of type `UpdateEnvInput` with keys present in environment variables - -> Note: the super admin query can be access via special header with super admin secret (this is set via ENV) or `authorizer-admin` as http only cookie. - -This mutation returns `Response` type with message - -**Sample Mutation** - -```graphql -mutation { - _update_env(params: { DATABASE_URL: "data.db", DATABASE_TYPE: "sqlite" }) { - message - } -} -``` - ### `_update_user` Mutation to update the profile of users. This mutation is only allowed for super admins. It accepts `params` of type `UpdateUserInput` with following keys @@ -1171,7 +1235,7 @@ It can take optional `params` input of type `PaginatedInput` with following keys ```graphql query { - _verification_requests(params: { pagination: { limit: 10, page: 2 } }) { + _verification_requests(params: { limit: 10, page: 2 }) { pagination { limit offset @@ -1293,7 +1357,7 @@ Mutation to test webhook endpoint. This mutation is allowed for admins only. It | Key | Description | Required | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | -| `event_name` | Name of event for which webhook should be called. Currently, supports `user.login`, `user.created`, `user.signup`, `user.access_revoked`, `user.access_enabled`, `user.deleted` events only. This is a unique field, means you can have one webhook for each event. | `true` | +| `event_name` | Name of event for which webhook should be called. Currently, supports `user.login`, `user.created`, `user.signup`, `user.access_revoked`, `user.access_enabled`, `user.deleted`, `user.deactivated`, `user.provisioned`, `user.deprovisioned`, `user.scim_updated`, `group.created`, `group.updated`, `group.deleted` events only. This is a unique field, means you can have one webhook for each event. | `true` | | `endpoint` | Endpoint that needs to be called for a given event | `true` | | `headers` | JSON of key, value pair which are extra HTTP headers to be sent. Default header added is `content-type: application/json` | `false` | @@ -1339,7 +1403,7 @@ Mutation to add webhook. This mutation is allowed for admins only. It accepts `p | Key | Description | Required | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | -| `event_name` | Name of event for which webhook should be called. Currently, supports `user.login`, `user.created`, `user.signup`, `user.access_revoked`, `user.access_enabled`, `user.deleted` events only. This is a unique field, means you can have one webhook for each event. | `true` | +| `event_name` | Name of event for which webhook should be called. Currently, supports `user.login`, `user.created`, `user.signup`, `user.access_revoked`, `user.access_enabled`, `user.deleted`, `user.deactivated`, `user.provisioned`, `user.deprovisioned`, `user.scim_updated`, `group.created`, `group.updated`, `group.deleted` events only. This is a unique field, means you can have one webhook for each event. | `true` | | `endpoint` | Endpoint that needs to be called for a given event | `true` | | `enabled` | Boolean to state if the webhook is enabled or disabled | `true` | | `headers` | JSON of key, value pair which are extra HTTP headers to be sent. Default header added is `content-type: application/json` | `false` | @@ -1386,7 +1450,7 @@ Mutation to update webhook. This mutation is allowed for admins only. It accepts | Key | Description | Required | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | | `id` | Identifier of the webhook | `true` | -| `event_name` | Name of event for which webhook should be called. Currently, supports `user.login`, `user.created`, `user.signup`, `user.access_revoked`, `user.access_enabled`, `user.deleted` events only. This is a unique field, means you can have one webhook for each event. | `false` | +| `event_name` | Name of event for which webhook should be called. Currently, supports `user.login`, `user.created`, `user.signup`, `user.access_revoked`, `user.access_enabled`, `user.deleted`, `user.deactivated`, `user.provisioned`, `user.deprovisioned`, `user.scim_updated`, `group.created`, `group.updated`, `group.deleted` events only. This is a unique field, means you can have one webhook for each event. | `false` | | `endpoint` | Endpoint that needs to be called for a given event | `false` | | `enabled` | Boolean to state if the webhook is enabled or disabled | `false` | | `headers` | JSON of key, value pair which are extra HTTP headers to be sent. Default header added is `content-type: application/json` | `false` | @@ -1578,6 +1642,7 @@ Mutation to add email template that will be used while sending emails. This muta | Key | Description | Required | | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | | `event_name` | Name of event for which email template should be called. Currently, supports `basic_auth_signup`, `magic_link_login`, `update_email`, `forgot_password` events only. This is a unique field, means you can have one template for each event. | `true` | +| `subject` | Subject line for the email | `true` | | `template` | HTML template that will be used while sending emails | `true` | **Response** @@ -1591,7 +1656,7 @@ Mutation to add email template that will be used while sending emails. This muta ```graphql mutation { _add_email_template( - params: { event_name: "user.login", template: "hello world" } + params: { event_name: "basic_auth_signup", subject: "Welcome!", template: "hello world" } ) { message } @@ -1610,6 +1675,7 @@ Mutation to update email template. This mutation is allowed for admins only. It | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | | `id` | Identifier of the email template | `true` | | `event_name` | Name of event for which email template should be called. Currently, supports `basic_auth_signup`, `magic_link_login`, `update_email`, `forgot_password` events only. This is a unique field, means you can have one template for each event. | `false` | +| `subject` | Subject line for the email | `false` | | `template` | HTML template that will be used while sending emails | `false` | **Response** @@ -1622,7 +1688,7 @@ Mutation to update email template. This mutation is allowed for admins only. It ```graphql mutation { - _update_email_Template( + _update_email_template( params: { id: "123-adfa-123412-asdfasda" event_name: "update_email" @@ -1695,7 +1761,7 @@ _email_templates(params: {limit: 10, page: 1}) { offset total } - webhooks { + email_templates { id template event_name @@ -1717,13 +1783,16 @@ It can take optional `params` input with pagination and filtering options. **Request Params** -| Key | Description | Required | Default | -| ------------ | ---------------------------- | -------- | ------- | -| `page` | Number of page that you want | false | 1 | -| `limit` | Number of rows that you want | false | 10 | -| `actor_id` | Filter by actor ID | false | null | -| `action` | Filter by action | false | null | -| `resource` | Filter by resource type | false | null | +| Key | Description | Required | Default | +| ---------------- | ----------------------------------- | -------- | ------- | +| `page` | Number of page that you want | false | 1 | +| `limit` | Number of rows that you want | false | 10 | +| `actor_id` | Filter by actor ID | false | null | +| `action` | Filter by action | false | null | +| `resource_type` | Filter by resource type | false | null | +| `resource_id` | Filter by resource id | false | null | +| `from_timestamp` | Filter entries at/after this Unix timestamp | false | null | +| `to_timestamp` | Filter entries at/before this Unix timestamp | false | null | **Sample Query** @@ -1739,11 +1808,12 @@ query { page total } - entries { + audit_logs { id actor_id action - resource + resource_type + resource_id created_at } } diff --git a/docs/core/grpc.md b/docs/core/grpc.md index 87f92fe..5715286 100644 --- a/docs/core/grpc.md +++ b/docs/core/grpc.md @@ -24,6 +24,10 @@ Table of Contents - [`ResendVerifyEmail`](#resendverifyemail) - [`VerifyOtp`](#verifyotp) - [`ResendOtp`](#resendotp) + - [`SkipMfaSetup`](#skipmfasetup) + - [`LockMfa`](#lockmfa) + - [`EmailOtpMfaSetup`](#emailotpmfasetup) + - [`SmsOtpMfaSetup`](#smsotpmfasetup) - [`ForgotPassword`](#forgotpassword) - [`ResetPassword`](#resetpassword) - [`Session`](#session) @@ -76,6 +80,29 @@ Table of Contents - [`FgaListUsers`](#fgalistusers) - [`FgaExpand`](#fgaexpand) - [`FgaReset`](#fgareset) + - [Client Registry](#client-registry) + - [`CreateClient`](#createclient) + - [`UpdateClient`](#updateclient) + - [`DeleteClient`](#deleteclient) + - [`RotateClientSecret`](#rotateclientsecret) + - [`GetClient`](#getclient) + - [`Clients`](#clients) + - [Trusted Issuers](#trusted-issuers) + - [`AddTrustedIssuer`](#addtrustedissuer) + - [`UpdateTrustedIssuer`](#updatetrustedissuer) + - [`DeleteTrustedIssuer`](#deletetrustedissuer) + - [`GetTrustedIssuer`](#gettrustedissuer) + - [`TrustedIssuers`](#trustedissuers) + - [SAML IdP](#saml-idp) + - [`CreateSamlServiceProvider`](#createsamlserviceprovider) + - [`UpdateSamlServiceProvider`](#updatesamlserviceprovider) + - [`DeleteSamlServiceProvider`](#deletesamlserviceprovider) + - [`GetSamlServiceProvider`](#getsamlserviceprovider) + - [`ListSamlServiceProviders`](#listsamlserviceproviders) + - [`RotateSamlIdpCert`](#rotatesamlidpcert) + - [`RetireSamlIdpKey`](#retiresamlidpkey) + - [`ListSamlIdpKeys`](#listsamlidpkeys) + - [`ImportSamlSpMetadata`](#importsamlspmetadata) - [Calling with `grpcurl`](#calling-with-grpcurl) - [Health checks](#health-checks) - [Errors](#errors) @@ -211,6 +238,22 @@ Each message mirrors its GraphQL/REST counterpart — see the linked *Public.* Re-send the MFA OTP. Mirrors [`resend_otp`](./graphql-api#resend_otp). +### `SkipMfaSetup` + +*Public.* Completes an in-progress, token-withheld MFA offer by recording an explicit decline, then issues the withheld access token. Fails with `FAILED_PRECONDITION` when MFA is org-enforced (`--enforce-mfa`). Mirrors [`skip_mfa_setup`](./graphql-api#skip_mfa_setup). + +### `LockMfa` + +*Public.* Records that the caller lost access to their only MFA factor(s); only allowed with no verified Email/SMS OTP fallback enrolled. Does not issue a token — the account requires admin recovery afterward. Mirrors [`lock_mfa`](./graphql-api#lock_mfa). + +### `EmailOtpMfaSetup` + +*Public (dual-mode).* Sends a one-time code to the caller's own email and creates an unverified email-OTP MFA enrollment — either for an already-authenticated caller adding a second factor, or a caller in the withheld first-time-offer state identified by the MFA session cookie. Mirrors [`email_otp_mfa_setup`](./graphql-api#email_otp_mfa_setup). + +### `SmsOtpMfaSetup` + +*Public (dual-mode).* Same as `EmailOtpMfaSetup`, for SMS. Mirrors [`sms_otp_mfa_setup`](./graphql-api#sms_otp_mfa_setup). + ### `ForgotPassword` *Public.* Start password reset; emails a reset link. Mirrors [`forgot_password`](./graphql-api#forgot_password). @@ -407,6 +450,98 @@ Manage the embedded fine-grained authorization (FGA) engine. See [Authorization *Admin-only.* Delete the entire fine-grained authorization store (model, all versions, and all tuples) and start fresh. Refused if any tuples still exist. Destructive and audited. +### Client Registry + +Manage machine/workload identity clients (`service_account` and similar client types). See the [Client Registry guide](./client-registry) for the conceptual model. + +#### `CreateClient` + +*Admin-only.* Provision a new client and return the generated client secret exactly once. + +#### `UpdateClient` + +*Admin-only.* Update a client's name, description, allowed scopes, or active state. Never touches the secret. + +#### `DeleteClient` + +*Admin-only.* Delete a client by id, cascading to its trusted issuers. + +#### `RotateClientSecret` + +*Admin-only.* Replace the stored client secret with a fresh one, returned exactly once. The old secret stops validating immediately. + +#### `GetClient` + +*Admin-only.* Get a single client by id. The client secret is never surfaced. + +#### `Clients` + +*Admin-only.* List clients with pagination. Client secrets are never surfaced. + +### Trusted Issuers + +Manage external JWT issuers used for RFC 7523 `private_key_jwt` client assertions. See [Workload Identity](../enterprise/workload-identity). + +#### `AddTrustedIssuer` + +*Admin-only.* Register an external issuer for a client. `subject_claim` defaults to `sub` when omitted. + +#### `UpdateTrustedIssuer` + +*Admin-only.* Update an issuer's name, JWKS URL, expected audience, active state, or SPIFFE refresh hint. + +#### `DeleteTrustedIssuer` + +*Admin-only.* Delete a trusted issuer by id. + +#### `GetTrustedIssuer` + +*Admin-only.* Get a single trusted issuer by id. + +#### `TrustedIssuers` + +*Admin-only.* List trusted issuers, optionally filtered by client id, with pagination. + +### SAML IdP + +Manage Authorizer acting as a SAML 2.0 identity provider for downstream service providers, plus IdP signing-key rotation. See [SAML IdP](../enterprise/saml-idp). + +#### `CreateSamlServiceProvider` + +*Admin-only.* Register a downstream SP that Authorizer issues signed assertions to. + +#### `UpdateSamlServiceProvider` + +*Admin-only.* Update a downstream SP's name, endpoints, certificate, attribute mapping, or active state. + +#### `DeleteSamlServiceProvider` + +*Admin-only.* Delete a downstream SP by id. + +#### `GetSamlServiceProvider` + +*Admin-only.* Get a single downstream SP by id. + +#### `ListSamlServiceProviders` + +*Admin-only.* List downstream SPs for an org. + +#### `RotateSamlIdpCert` + +*Admin-only.* Generate a new current signing keypair for an org's SAML IdP, demoting the previous current key. + +#### `RetireSamlIdpKey` + +*Admin-only.* Retire a published-but-not-signing SAML IdP key by id. + +#### `ListSamlIdpKeys` + +*Admin-only.* List all SAML IdP signing keys for an org. + +#### `ImportSamlSpMetadata` + +*Admin-only.* Parse pasted SP metadata XML and return fields to prefill a create call. Performs no remote fetch and creates no record. + ## Calling with `grpcurl` With reflection enabled you can explore and call the service directly: diff --git a/docs/core/mcp.md b/docs/core/mcp.md index 32ced83..935d519 100644 --- a/docs/core/mcp.md +++ b/docs/core/mcp.md @@ -13,8 +13,10 @@ behalf. The headline use case: give an AI assistant the ability to ask *"is this user allowed to see this document?"* before it retrieves or summarizes content — the same -permission-aware [RAG](https://github.com/authorizerdev) pattern, but driven from inside -the model. +permission-aware RAG pattern as the +[`with-rag-fga`](https://github.com/authorizerdev/examples/tree/main/with-rag-fga) +example (see [Real-world recipes → Permission-aware retrieval](./authorization#permission-aware-retrieval-rag--ai-agents)), +but driven from inside the model instead of your backend. ## Design & security model @@ -61,11 +63,17 @@ authorizer mcp \ --client-id=YOUR_CLIENT_ID \ --database-type=sqlite \ --database-url=auth.db \ - --fga-store=file://./fga_store \ --mcp-bearer="$USER_ACCESS_TOKEN" \ --mcp-authorizer-url=https://auth.example.com ``` +With a SQLite/Postgres/MySQL `--database-type`, FGA reuses the main database +automatically — no `--fga-store` flag needed (see +[Enabling FGA](./authorization#1-enabling-fga)). Only pass `--fga-store` / +`--fga-store-url` when the main database is NoSQL (MongoDB, DynamoDB, …) or +you want FGA on a separate store; `--fga-store` takes one of `sqlite`, +`postgres`, `mysql`, or `memory` — not a URI. + The `mcp` command inherits the root server flags (database, JWT, client-id, `--fga-store`, etc.) so it can resolve identity and run the FGA engine in-process. @@ -95,7 +103,6 @@ Most MCP hosts read a JSON config that declares the command to spawn. For "--client-id", "YOUR_CLIENT_ID", "--database-type", "sqlite", "--database-url", "auth.db", - "--fga-store", "file://./fga_store", "--mcp-bearer", "USER_ACCESS_TOKEN", "--mcp-authorizer-url", "https://auth.example.com" ] @@ -115,8 +122,37 @@ so the host surfaces it to the model as a recoverable failure (not a protocol ab Typical messages mirror the gRPC status: `Unauthenticated`, `PermissionDenied`, `FailedPrecondition` (e.g. *fga is not enabled*). +## Authorizer as the authorization server protecting *your own* MCP server + +Everything above is about the **built-in stdio server** — Authorizer's own tools, +consumed by a host on your machine. The other direction is just as common: your MCP +server (streamable HTTP, hosted anywhere) needs a real OAuth 2.1 authorization server +in front of it, and Authorizer can be that AS. This is a **different pattern** — +plain OAuth, no `authorizer mcp` involved: + +| Spec | What it says | Who implements it | +| --- | --- | --- | +| OAuth 2.1 | Bearer tokens, token endpoint, client auth | **Authorizer** (`/oauth/token`, JWKS, OIDC discovery) | +| [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) (protected resource metadata) | Your resource server publishes `/.well-known/oauth-protected-resource` naming its `resource` URI and `authorization_servers`, pointed at from a `401` `WWW-Authenticate` header | **your MCP server** | +| [RFC 8707](https://datatracker.ietf.org/doc/html/rfc8707) (resource indicators) | The client passes `resource=` when requesting a token; the AS binds the token's `aud` to it | **Authorizer** binds it (see the `resource` parameter on the [Authorization Endpoint](./oauth2-oidc#authorization-endpoint) and [Token Exchange](../enterprise/token-exchange)); **your MCP server** enforces `aud` on every call | +| [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) (token exchange) | An agent gets a token that says "agent X acting for user Y" — see [Token Exchange & Delegation](../enterprise/token-exchange) | **Authorizer** (`grant_type=...token-exchange` on `/oauth/token`) | + +Authorizer also serves the [RFC 8414 alias](./oauth2-oidc#rfc-8414-alias) +`/.well-known/oauth-authorization-server` — the identical metadata document as +`/.well-known/openid-configuration` — for MCP clients that probe the OAuth-only +discovery path instead of falling back to OIDC discovery. + +The [`with-mcp`](https://github.com/authorizerdev/examples/tree/main/with-mcp) example +builds this end to end: a ~150-line Express MCP server that validates Authorizer-issued +JWTs (issuer + `aud`) and a client walkthrough that goes 401 → RFC 9728 discovery → +`client_credentials` (rejected — wrong `aud`) → RFC 8693 token exchange with +`resource=` → 200. Its own bonus section documents this page's +built-in stdio server too, for the "which one do I want" question. + ## See also - [Authorization (FGA)](./authorization) — the relationship model behind the permission tools. - [REST API](./rest-api) / [GraphQL API](./graphql-api) — the same operations over HTTP. - [FGA Guide](./fga-guide) — building an authorization model and writing tuples. +- [OAuth2 / OIDC](./oauth2-oidc) — the authorization-server surface used to protect a custom MCP server. +- [Token Exchange & Delegation](../enterprise/token-exchange) — agent-acting-for-user delegated tokens (RFC 8693). diff --git a/docs/core/oauth2-oidc.md b/docs/core/oauth2-oidc.md index 0c42526..f59a523 100644 --- a/docs/core/oauth2-oidc.md +++ b/docs/core/oauth2-oidc.md @@ -29,8 +29,9 @@ This page is the one-stop reference for every endpoint, parameter, and integrati | RFC 7523 (JWT client assertions) | Implemented | `private_key_jwt` via [trusted issuers](../enterprise/workload-identity) — K8s SA tokens, SPIFFE JWT-SVIDs | | RFC 7636 (PKCE) | Implemented | `S256` and `plain` methods; defaults to `plain` when the method is omitted (§4.2) | | RFC 7662 (Token Introspection) | Implemented | Non-disclosure for inactive tokens | +| RFC 8414 (Authorization Server Metadata) | Implemented | `/.well-known/oauth-authorization-server` — alias of the OIDC discovery document | | RFC 8693 (Token Exchange) | Implemented | [Delegation-only profile](../enterprise/token-exchange) with nested `act` chain | -| RFC 8707 (Resource Indicators) | Implemented | `resource` binding on the token-exchange grant (exactly one required) | +| RFC 8707 (Resource Indicators) | Implemented | Optional `resource` on `/authorize` + `/oauth/token` (authorization code flow); exactly one required on the token-exchange grant | **Not yet implemented** (tracked for future releases): RFC 7591 dynamic client registration, RFC 9101 JAR / Request Object, OIDC Session Management iframe, front-channel logout, automated time-based key rotation. @@ -410,9 +411,10 @@ Returns metadata so clients can auto-configure themselves. | `end_session_endpoint` | URL for `/logout` | | `response_types_supported` | `["code", "token", "id_token", "code id_token", "code token", "code id_token token", "id_token token"]` | | `grant_types_supported` | `["authorization_code", "refresh_token", "client_credentials", "implicit", "urn:ietf:params:oauth:grant-type:token-exchange"]` | -| `scopes_supported` | `["openid", "email", "profile", "offline_access"]` | +| `scopes_supported` | `["openid", "email", "profile", "phone", "offline_access"]` | | `response_modes_supported` | `["query", "fragment", "form_post", "web_message"]` | -| `code_challenge_methods_supported` | `["S256", "plain"]` | +| `code_challenge_methods_supported` | `["S256", "plain"]` — `["S256"]` only when `--oauth2-1-strict` is set | +| `resource_indicators_supported` | `true` (RFC 8707) | | `id_token_signing_alg_values_supported` | Includes configured `--jwt-type` and always `RS256` | | `token_endpoint_auth_methods_supported` | `["client_secret_basic", "client_secret_post", "none", "private_key_jwt"]` (`none` = public client with PKCE) | | `introspection_endpoint_auth_methods_supported` | `["client_secret_basic", "client_secret_post"]` | @@ -425,6 +427,18 @@ Returns metadata so clients can auto-configure themselves. curl https://your-authorizer.example/.well-known/openid-configuration ``` +#### RFC 8414 alias + +**Endpoint:** `GET /.well-known/oauth-authorization-server` + +A thin alias serving the identical document as `/.well-known/openid-configuration` — added +for clients that only know [RFC 8414](https://www.rfc-editor.org/rfc/rfc8414) (OAuth 2.0 +Authorization Server Metadata) and probe this path first without falling back to OIDC +discovery. This matters for OAuth-only clients like remote MCP hosts implementing the +[MCP Authorization spec](https://modelcontextprotocol.io), which is layered on RFC 8414/8707 +rather than full OIDC. The response also advertises `resource_indicators_supported: true` +(RFC 8707). + ### Authorization Endpoint **Endpoint:** `GET /authorize` @@ -432,6 +446,13 @@ curl https://your-authorizer.example/.well-known/openid-configuration Supported flows: Authorization Code (with PKCE), Implicit, Hybrid. +With `--oauth2-1-strict` (default `false`, opt-in and breaking), Authorizer additionally +enforces OAuth 2.1: any `response_type` that delivers a bearer token in the fragment +(`token`, `id_token token`, `code token`, `code id_token token`) is rejected, and PKCE +`plain` is no longer accepted — only `S256`. Discovery's `code_challenge_methods_supported` +reflects this (drops `plain` when strict mode is on). Off by default, so existing implicit/ +hybrid-with-token and PKCE-plain clients are unaffected until you opt in. + **Request parameters:** | Parameter | Required | Notes | @@ -439,7 +460,7 @@ Supported flows: Authorization Code (with PKCE), Implicit, Hybrid. | `client_id` | Yes | Your application's client ID | | `response_type` | Yes | Any supported single or hybrid combination (see discovery) | | `state` | Yes | Anti-CSRF token (opaque string). Mandatory in Authorizer | -| `redirect_uri` | No | Must match an allowed origin; defaults to `/app` | +| `redirect_uri` | No | Must match a registered redirect URI exactly (RFC 6749 §3.1.2.3) if the client has any registered; otherwise checked against `--allowed-origins` only. Defaults to `/app` | | `scope` | No | Space-separated. Default: `openid profile email` | | `response_mode` | No | `query`, `fragment`, `form_post`, `web_message`. Hybrid flows forbid `query` | | `code_challenge` | Yes, when `code` is in type | PKCE challenge: `BASE64URL(SHA256(code_verifier))` | @@ -451,6 +472,7 @@ Supported flows: Authorization Code (with PKCE), Implicit, Hybrid. | `ui_locales` | No | Forwarded to the login UI as a query parameter | | `id_token_hint` | No | Advisory ID token; invalid hints are ignored (never cause the request to fail) | | `screen_hint` | No | Authorizer extension: `signup` redirects to the signup page | +| `resource` | No | RFC 8707 resource indicator (authorization code flow only). Must be an absolute URI with no fragment (`invalid_target` otherwise); repeated values are rejected (`invalid_request`). Bound to the code and must be echoed unchanged at `/oauth/token`; sets the access token's `aud` | **Example authorization code request:** @@ -526,16 +548,20 @@ Serves four grant types: `authorization_code`, `refresh_token`, `client_credenti Authorization codes expire after **10 minutes** (RFC 6749 §4.1.2 recommendation) and are single-use. +If `resource` was present at `/authorize`, it must be echoed here (form field `resource`) and match exactly, or the request is rejected with `invalid_grant`. It is not a token endpoint parameter in its own right — it can only confirm the value already bound to the code. + **Refresh Token grant:** | Parameter | Required | Notes | | --------------- | -------- | ---------------------------------------------- | | `grant_type` | Yes | `refresh_token` | | `refresh_token` | Yes | A valid refresh token | -| `client_id` | Yes | Your client ID | +| `client_id` | Yes | Your client ID — must match the client the refresh token was originally issued to (enforced via the token's `aud` claim); a mismatch is `invalid_grant` | Refresh tokens are **rotated** on each use — the old one is invalidated and a new one returned. +**Reuse detection:** each refresh token carries a stable `family_id`, minted once and carried across every rotation in its lineage. Replaying an already-rotated (dead) token is treated as a breach and revokes only that token's family (the specific session lineage) — other sessions and login methods for the same user are untouched. A same-second double-submit of the immediately preceding token (multi-tab race, retry) is tolerated within a **10-second grace window** and is not treated as reuse; anything older, or a replay after the window, revokes the family. + **Success response:** ```json @@ -598,10 +624,10 @@ Standard codes: `invalid_request`, `invalid_client`, `invalid_grant`, `unsupport ### UserInfo Endpoint -**Endpoint:** `GET /userinfo` +**Endpoint:** `GET /userinfo` or `POST /userinfo` **Specs:** [OIDC Core §5.3](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo) | [OIDC Core §5.4 (scope-based claim filtering)](https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims) | [RFC 6750 (Bearer Token)](https://www.rfc-editor.org/rfc/rfc6750) -Returns claims about the authenticated end-user, **filtered by the scopes encoded in the access token**. +Returns claims about the authenticated end-user, **filtered by the scopes encoded in the access token**. Both `GET` and `POST` are accepted per OIDC Core §5.3.1 — either way the access token goes in the `Authorization: Bearer` header (a token in the POST body is not supported). ```bash curl -H "Authorization: Bearer ACCESS_TOKEN" https://your-authorizer.example/userinfo @@ -616,7 +642,7 @@ curl -H "Authorization: Bearer ACCESS_TOKEN" https://your-authorizer.example/use | `phone` | `phone_number`, `phone_number_verified` | | `address` | `address` | -The `sub` claim is **always** returned per OIDC Core §5.3.2. Keys belonging to a granted scope group are always present in the response; if the user has no value for a specific claim, the key is emitted with JSON `null` (explicitly permitted by §5.3.2) so callers can rely on a stable schema. +The `sub` claim is **always** returned per OIDC Core §5.3.2. Claims within a granted scope group are included only when the user actually has a value; unset claims are **omitted**, never emitted as JSON `null` — §5.3.2 permits omission but does not permit a present-but-null claim, and OIDF conformance testing (`oidcc-scope-profile`) enforces this. **Error response (RFC 6750 §3):** diff --git a/docs/core/rest-api.md b/docs/core/rest-api.md index 4ebeb58..794bfcc 100644 --- a/docs/core/rest-api.md +++ b/docs/core/rest-api.md @@ -37,6 +37,10 @@ Table of Contents - [`POST /v1/resend_verify_email`](#post-v1resend_verify_email) - [`POST /v1/verify_otp`](#post-v1verify_otp) - [`POST /v1/resend_otp`](#post-v1resend_otp) + - [`POST /v1/skip_mfa_setup`](#post-v1skip_mfa_setup) + - [`POST /v1/lock_mfa`](#post-v1lock_mfa) + - [`POST /v1/email_otp_mfa_setup`](#post-v1email_otp_mfa_setup) + - [`POST /v1/sms_otp_mfa_setup`](#post-v1sms_otp_mfa_setup) - [`POST /v1/forgot_password`](#post-v1forgot_password) - [`POST /v1/reset_password`](#post-v1reset_password) - [`POST /v1/logout`](#post-v1logout) @@ -90,6 +94,9 @@ Table of Contents - [`POST /v1/admin/fga/list_users`](#post-v1adminfgalist_users) - [`POST /v1/admin/fga/expand`](#post-v1adminfgaexpand) - [`POST /v1/admin/fga/reset`](#post-v1adminfgareset) + - [Client Registry](#client-registry) + - [Trusted Issuers](#trusted-issuers) + - [SAML IdP](#saml-idp) - [Errors](#errors) - [See also](#see-also) @@ -251,6 +258,22 @@ Complete an MFA challenge by submitting the email/phone OTP. Mirrors [`verify_ot Re-send the MFA OTP. Mirrors [`resend_otp`](./graphql-api#resend_otp). +### `POST /v1/skip_mfa_setup` + +Completes an in-progress, token-withheld MFA offer by recording an explicit decline, then issues the withheld access token. Rejected when MFA is org-enforced (`--enforce-mfa`). Mirrors [`skip_mfa_setup`](./graphql-api#skip_mfa_setup). + +### `POST /v1/lock_mfa` + +Records that the caller lost access to their only MFA factor(s); only allowed with no verified Email/SMS OTP fallback enrolled. Does not issue a token — the account requires admin recovery afterward. Mirrors [`lock_mfa`](./graphql-api#lock_mfa). + +### `POST /v1/email_otp_mfa_setup` + +Sends a one-time code to the caller's own email and creates an unverified email-OTP MFA enrollment — either for an already-authenticated caller adding a second factor, or a caller in the withheld first-time-offer state identified by the MFA session cookie. Mirrors [`email_otp_mfa_setup`](./graphql-api#email_otp_mfa_setup). + +### `POST /v1/sms_otp_mfa_setup` + +Same as `email_otp_mfa_setup`, for SMS. Mirrors [`sms_otp_mfa_setup`](./graphql-api#sms_otp_mfa_setup). + ### `POST /v1/forgot_password` Start password reset; emails a reset link. Mirrors [`forgot_password`](./graphql-api#forgot_password). @@ -656,12 +679,15 @@ Retrieve audit log entries with optional filtering and pagination. **Request body** -| Field | Type | Description | Required | -| ------------ | ----------------- | ----------------------- | -------- | -| `pagination` | `{ page, limit }` | Page & limit. | no | -| `actor_id` | `string` | Filter by actor id. | no | -| `action` | `string` | Filter by action type. | no | -| `resource` | `string` | Filter by resource type. | no | +| Field | Type | Description | Required | +| ---------------- | ----------------- | ---------------------------------- | -------- | +| `pagination` | `{ page, limit }` | Page & limit. | no | +| `actor_id` | `string` | Filter by actor id. | no | +| `action` | `string` | Filter by action type. | no | +| `resource_type` | `string` | Filter by resource type. | no | +| `resource_id` | `string` | Filter by resource id. | no | +| `from_timestamp` | `int64` | Filter entries at/after this Unix timestamp. | no | +| `to_timestamp` | `int64` | Filter entries at/before this Unix timestamp. | no | **Response** `{ audit_logs: AuditLog[], pagination: { ... } }` @@ -762,6 +788,50 @@ Delete the entire fine-grained authorization store (model, all versions, and all **Response** `{ message: string }` +### Client Registry + +Manage machine/workload identity clients. All admin-only. Field-level request/response +shapes and examples: [Client Registry guide](./client-registry). + +| Endpoint | Description | +| ---------------------------------------- | --------------------------------------------------------------------- | +| `POST /v1/admin/create_client` | Provision a new client; returns the secret exactly once. | +| `POST /v1/admin/update_client` | Update name, description, allowed scopes, or active state. | +| `POST /v1/admin/delete_client` | Delete a client by id, cascading to its trusted issuers. | +| `POST /v1/admin/rotate_client_secret` | Replace the client secret, returned exactly once. | +| `POST /v1/admin/client` | Get a single client by id. Secret never surfaced. | +| `POST /v1/admin/clients` | List clients with pagination. Secrets never surfaced. | + +### Trusted Issuers + +Manage external JWT issuers for RFC 7523 `private_key_jwt` client assertions. All +admin-only. Field-level request/response shapes: [Workload Identity](../enterprise/workload-identity). + +| Endpoint | Description | +| ------------------------------------------ | --------------------------------------------------------------------- | +| `POST /v1/admin/add_trusted_issuer` | Register an issuer for a client (`subject_claim` defaults to `sub`). | +| `POST /v1/admin/update_trusted_issuer` | Update name, JWKS URL, expected audience, active state, or SPIFFE hint. | +| `POST /v1/admin/delete_trusted_issuer` | Delete a trusted issuer by id. | +| `POST /v1/admin/trusted_issuer` | Get a single trusted issuer by id. | +| `POST /v1/admin/trusted_issuers` | List trusted issuers, optionally filtered by client id. | + +### SAML IdP + +Manage Authorizer acting as a SAML 2.0 IdP for downstream service providers, plus IdP +signing-key rotation. All admin-only. Field-level request/response shapes: [SAML IdP](../enterprise/saml-idp). + +| Endpoint | Description | +| ------------------------------------------------ | --------------------------------------------------------------------- | +| `POST /v1/admin/create_saml_service_provider` | Register a downstream SP. | +| `POST /v1/admin/update_saml_service_provider` | Update a downstream SP's name, endpoints, certificate, or mapping. | +| `POST /v1/admin/delete_saml_service_provider` | Delete a downstream SP by id. | +| `POST /v1/admin/saml_service_provider` | Get a single downstream SP by id. | +| `POST /v1/admin/saml_service_providers` | List downstream SPs for an org. | +| `POST /v1/admin/rotate_saml_idp_cert` | Generate a new current signing keypair, demoting the previous one. | +| `POST /v1/admin/retire_saml_idp_key` | Retire a published-but-not-signing SAML IdP key by id. | +| `POST /v1/admin/saml_idp_keys` | List all SAML IdP signing keys for an org. | +| `POST /v1/admin/import_saml_sp_metadata` | Parse pasted SP metadata XML; no record created, no remote fetch. | + ## Errors REST errors return a non-`200` HTTP status with a stable JSON envelope: diff --git a/docs/core/security.md b/docs/core/security.md index 1cff1eb..7cbfff4 100644 --- a/docs/core/security.md +++ b/docs/core/security.md @@ -76,6 +76,13 @@ Everything else in this document is opt-in or already on by default. for higher-security deployments where re-authentication is acceptable; lengthen for long-lived sessions where a 30-day window is too short. +**Revocation on password reset**: a successful [`reset_password`](./graphql-api#reset_password) +synchronously deletes all of the user's existing sessions and refresh +tokens from the session store before the mutation returns — any +pre-existing session or refresh token is rejected immediately after. This +closes the gap where an attacker holding a live session before the reset +kept access after it. + --- ## Trusted proxies @@ -114,6 +121,31 @@ proxy you **must** set this flag, otherwise: --- +## Trusted base URL + +```bash +./authorizer --url=https://auth.example.com +``` + +- **`--url`** (default empty): the operator-configured canonical base URL of + this Authorizer instance. When set, it is the **only** source used to + derive the server's own host — verification/reset/magic-link email links, + the JWT `iss` claim, and the OIDC discovery/JWKS document URLs — and every + request header that could otherwise influence it (`X-Authorizer-URL`, + `X-Forwarded-Host`, `Host`) is ignored. The value is normalized to + scheme+host (path, query, fragment, userinfo, and trailing slash stripped) + and pinned once at startup, before any listener accepts a connection. + +When **empty** (the default), Authorizer falls back to header-based +derivation (`X-Authorizer-URL`, then `X-Forwarded-Host`/`Host`), which +preserves flexible reverse-proxy / multi-tenant setups but leaves a +host-header-injection account-takeover surface (CWE-640): a request with a +forged host header can cause a password-reset or verification email to +contain a link pointing at an attacker-controlled domain. **Set `--url` in +production**, particularly behind a reverse proxy, to close this off. + +--- + ## CORS, CSRF, and origin enforcement ### CORS @@ -339,6 +371,167 @@ failed to decrypt stored TOTP secret; check that --jwt-secret has not changed si --- +## Multi-factor authentication (MFA) & Passkeys + +MFA methods — TOTP, email OTP, SMS OTP, and WebAuthn/passkey as a second +factor — are **enabled by default**. `--enforce-mfa` defaults to `false`: +MFA is optional and skippable unless you turn enforcement on. See +[Server Configuration](./server-config#multi-factor-authentication-mfa--webauthnpasskeys) +for the full flag reference (`--enforce-mfa`, `--disable-mfa`, +`--disable-totp-login`, `--disable-webauthn-mfa`, `--disable-email-otp`, +`--disable-sms-otp`). + +### Per-method availability + +The public `meta` GraphQL query exposes whether each method is actually +usable right now (config flag **and**, for email/SMS OTP, provider +configured): + +| Field | True when | +|---|---| +| `is_totp_mfa_enabled` | MFA enabled and `--disable-totp-login` is not set | +| `is_email_otp_mfa_enabled` | MFA enabled, `--disable-email-otp` is not set, and SMTP is configured | +| `is_sms_otp_mfa_enabled` | MFA enabled, `--disable-sms-otp` is not set, and Twilio is configured | +| `is_webauthn_enabled` | MFA enabled and `--disable-webauthn-mfa` is not set — this reflects WebAuthn's availability **as an MFA factor only**; primary passkey login/registration has no flag and is always available regardless of this field | +| `is_mfa_enforced` | mirrors `--enforce-mfa` | + +`_admin_meta.is_multi_factor_auth_service_enabled` reports whether MFA can be +used at all on this instance (at least one usable method) — the dashboard +uses it to gate the per-user "require MFA" toggle. + +### Withheld-token first-time setup + +Login/signup/OAuth-callback resolve a per-user MFA "gate" before issuing a +token: + +| Gate | Trigger | Token behavior | +|---|---|---| +| none | MFA doesn't apply to this user and isn't enforced | issued normally | +| block-verify | user already has a verified factor (TOTP, passkey, email-OTP, or SMS-OTP) | **withheld** until they verify it — never skippable | +| block-enroll | `--enforce-mfa` is set and the user hasn't enrolled anything yet | **withheld** until enrollment completes — never skippable | +| offer-all | MFA is available but not enforced, user hasn't enrolled, and has never skipped before | **withheld** until the user completes a method or calls `skip_mfa_setup` | +| skipped | same as offer-all, but the user already chose Skip once | issued normally, no nag | + +In every withheld case the response carries no `access_token` — only a +message and a set of `should_show_*` / `should_offer_*` flags on +`AuthResponse` (`should_show_totp_screen`, `should_offer_webauthn_mfa_setup`, +`should_offer_email_otp_mfa_setup`, `should_offer_sms_otp_mfa_setup`, +`should_offer_webauthn_mfa_verify`). The frontend authenticates the +follow-up call (`verify_otp`, `totp_mfa_setup`, `webauthn_registration_verify`, +`webauthn_login_verify`, or `skip_mfa_setup`) via a short-lived MFA session +cookie set alongside that response, not a bearer token — none has been +issued yet. `should_offer_mfa_setup` is deprecated and never set; ignore it. + +A registered passkey satisfies MFA on its own — no OTP/TOTP re-challenge — +because every WebAuthn assertion already requires user verification +(biometric/PIN) at the authenticator. + +### Lockout & admin recovery + +Two independent lockout mechanisms exist: + +1. **Transient per-user rate limit.** `verify_otp` (TOTP, TOTP recovery code, + email OTP, and SMS OTP all share this) allows **5 failed attempts per user + in a 15-minute sliding window**; the 6th failing attempt returns + `429 Too Many Requests` for the rest of that window. This is on top of the + global per-IP rate limiter and closes the gap where one account is + brute-forced from many IPs. A storage-layer fault to the counter fails + open (never locks a legitimate user out because of an infra blip). +2. **Permanent, self-declared lockout.** The `lock_mfa` mutation lets a user + who has lost access to their only MFA factor (with no working OTP + fallback) mark their own account locked (`mfa_locked_at`). It's refused + if the user has a verified email/SMS OTP fallback available — use that + instead. Once locked, **all** login attempts for that user (password, + passkey, everything) are rejected until an admin clears it. + +Admin recovery is the `_update_user` mutation with `reset_mfa: true`. It +clears `mfa_locked_at`, the user's `is_multi_factor_auth_enabled` override, +and `has_skipped_mfa_setup_at`, and **deletes every enrolled +authenticator (TOTP/email-OTP/SMS-OTP) and every registered WebAuthn +credential** for that user — their next login lands back on the same +first-time MFA setup screen a brand-new account sees. + +```graphql +mutation { + _update_user(params: { id: "user-id", reset_mfa: true }) { + id + mfa_locked_at + enrolled_mfa_methods + } +} +``` + +`User.mfa_locked_at` and `User.enrolled_mfa_methods` (any of `"totp"`, +`"webauthn"`, `"email_otp"`, `"sms_otp"`) let an admin dashboard show which +users are locked and what they have enrolled without guessing from +`is_multi_factor_auth_enabled` alone. + +### Recovery codes + +TOTP enrollment (`totp_mfa_setup`, or the enrollment payload returned inline +by a `block-enroll`/`offer-all` login response) issues **10 single-use +recovery codes**, shown to the user exactly once. At rest they are stored as +**SHA-256 hashes** (not the plaintext codes) and each is marked consumed the +first time it validates successfully — a stolen database dump never yields +usable codes, and a code can't be replayed. + +### WebAuthn / passkey ceremonies + +WebAuthn/passkey support is `web/app` (end-user login) only — +**`web/dashboard` admin login is untouched** (`_admin_login` has no passkey +path). The self-service GraphQL operations (no `_` admin prefix): + +| Operation | Purpose | +|---|---| +| `webauthn_registration_options(email, phone_number)` | begin registering a new passkey; returns JSON `PublicKeyCredentialCreationOptions` for `navigator.credentials.create()` | +| `webauthn_registration_verify(params: { name, credential, email, phone_number, state })` | verify the attestation and persist the credential | +| `webauthn_login_options(email: String)` | begin a login ceremony — omit `email` for **usernameless/discoverable** login (any resident passkey for the origin); pass it for the **MFA-alternative** flow, scoped to that user's own credentials | +| `webauthn_login_verify(params: { credential, state })` | verify the assertion and issue tokens through the same path as `login`/`verify_otp` | +| `webauthn_credentials` (query) | list the caller's own registered passkeys | +| `webauthn_delete_credential(id)` | delete one of the caller's own passkeys | + +`options`/`credential` are opaque JSON strings carrying the WebAuthn +`PublicKeyCredential*` structures; the SDK handles the base64url ⟷ +`ArrayBuffer` conversion between these and the browser's +`navigator.credentials` API. + +```graphql +mutation BeginPasskeyLogin { + webauthn_login_options { + options + } +} + +mutation FinishPasskeyLogin($credential: String!) { + webauthn_login_verify(params: { credential: $credential }) { + access_token + id_token + refresh_token + message + } +} +``` + +Both registration operations also accept an MFA-session-cookie caller (no +bearer token yet) during a token-withheld `offer-all`/`block-enroll` login — +completing registration there resolves the gate and issues the previously +withheld token, the same way `totp_mfa_setup` + `verify_otp(is_totp: true)` +does for TOTP. + +**Email-verification gate:** `webauthn_login_verify` is stricter than +password login — it refuses to issue tokens until the account's email is +verified, returning a distinct, actionable error +(`email is not verified. please verify your email before signing in with a +passkey`) rather than a generic invalid-credential error. This matters most +for passkey-only signup (no password at all): the first login attempt with +that passkey is refused until the user verifies their email through the +normal verification-email flow. + +A locked-out account (`mfa_locked_at` set) is refused at +`webauthn_login_verify` too, with the same message as password/OTP login. + +--- + ## GraphQL hardening ```bash diff --git a/docs/core/server-config.md b/docs/core/server-config.md index 6f90684..4fefe32 100644 --- a/docs/core/server-config.md +++ b/docs/core/server-config.md @@ -92,6 +92,13 @@ These flags replace v1 env such as `CLIENT_ID`, `CLIENT_SECRET`, and app behavio April 2026**: defaults to none — operators behind a proxy must set this explicitly or rate limiting and audit logs will key on the proxy IP. See [Trusted proxies](./security#trusted-proxies). +- **`--url`** (default empty): canonical/trusted base URL of this instance + (e.g. `https://auth.example.com`). When set, it is the **only** source used + to build verification/reset/magic-link email URLs, the JWT `iss` claim, and + OIDC discovery URLs — the `X-Authorizer-URL`, `X-Forwarded-Host`, and `Host` + request headers are ignored for that purpose. Leaving it empty keeps legacy + header-based derivation. **Recommended for production**, especially behind + a reverse proxy. See [Trusted base URL](./security#trusted-base-url). Organization / UI: @@ -123,15 +130,60 @@ Organization / UI: --enable-basic-authentication=true \ --enable-email-verification=true \ --enable-magic-link-login=true \ - --enable-signup=true \ - --enable-totp-login=true \ - --enable-email-otp=true \ - --enable-sms-otp=false + --enable-signup=true ``` These replace v1 flags such as `DISABLE_BASIC_AUTHENTICATION`, `DISABLE_EMAIL_VERIFICATION`, etc. See the [Auth behavior mapping](../migration/v1-to-v2#auth-behavior) for exact correspondences. +### Multi-factor authentication (MFA) & WebAuthn/passkeys + +**Breaking change**: `--enable-mfa`, `--enable-totp-login`, `--enable-email-otp`, +and `--enable-sms-otp` are **removed**. TOTP, email OTP, SMS OTP, and +WebAuthn/passkey-as-MFA are now all **on by default**; opt out per method with +the `--disable-*` flags below. `--enforce-mfa` also flipped its default from +`true` to `false` — MFA is now optional and skippable unless you explicitly +enforce it. + +```bash +./authorizer \ + --enforce-mfa=false \ + --disable-mfa=false \ + --disable-totp-login=false \ + --disable-webauthn-mfa=false \ + --disable-email-otp=false \ + --disable-sms-otp=false +``` + +- **`--enforce-mfa`** (default `false`): require every user to complete MFA + enrollment before a token is issued (`mfaGateBlockEnroll`, never skippable). + When `false`, users are offered MFA setup once and may skip it + (`skip_mfa_setup` mutation); a user's own already-enrolled factor is always + required to verify regardless of this flag. +- **`--disable-mfa`** (default `false`): one-way global kill switch — forces + MFA off entirely (TOTP/email-OTP/SMS-OTP unavailable, `--enforce-mfa` + neutralized) regardless of the per-method flags below. Does **not** affect + WebAuthn/passkey as a **primary login method** (only as an MFA factor). +- **`--disable-totp-login`** (default `false`): disable TOTP authenticator-app + MFA. +- **`--disable-webauthn-mfa`** (default `false`): disable WebAuthn/passkey as + an **MFA factor**. Does not affect WebAuthn/passkey as a primary, + passwordless login method — that is always available and has no flag. +- **`--disable-email-otp`** (default `false`): disable email-OTP MFA. Only + takes effect when SMTP is configured (`--smtp-*`); otherwise email OTP is + unavailable regardless of this flag. +- **`--disable-sms-otp`** (default `false`): disable SMS-OTP MFA. Only takes + effect when Twilio is configured (`--twilio-*`); otherwise SMS OTP is + unavailable regardless of this flag. + +Effective availability of each method is exposed on the public `meta` +GraphQL query (`is_totp_mfa_enabled`, `is_email_otp_mfa_enabled`, +`is_sms_otp_mfa_enabled`, `is_webauthn_enabled`, `is_mfa_enforced`) so +frontends never have to reverse-engineer flag combinations. See +[MFA & Passkeys](./security#multi-factor-authentication-mfa--passkeys) for +the full behavior — withheld-token first-time setup, lockout, admin recovery, +and the WebAuthn/passkey GraphQL operations. + ### Cookies ```bash @@ -321,6 +373,8 @@ See the dedicated [Security Hardening](./security) page for: - OTP and TOTP at-rest hardening, including the rolling-deploy note for multi-replica clusters - Login error normalization and user-enumeration defences +- Multi-factor authentication (MFA) behavior, lockout, admin recovery, and + WebAuthn/passkey ceremonies --- diff --git a/docs/core/sso-guide.md b/docs/core/sso-guide.md index aeef9a8..6bf2404 100644 --- a/docs/core/sso-guide.md +++ b/docs/core/sso-guide.md @@ -9,32 +9,20 @@ Authorizer can serve as the **central Single Sign-On (SSO) Identity Provider** f ## Architecture Overview -``` -┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ -│ Internal │ │ Customer │ │ Admin │ │ Third-Party│ -│ Dashboard │ │ Portal │ │ Tools │ │ (Grafana, │ -│ (React) │ │ (Next.js) │ │ (Go CLI) │ │ GitLab...) │ -└──────┬───────┘ └──────┬──────┘ └──────┬───────┘ └──────┬──────┘ - │ │ │ │ - │ OIDC Authorization Code + PKCE Flow │ - └──────────────────┼──────────────────┼─────────────────┘ - │ │ - ┌────────▼──────────────────▼─┐ - │ AUTHORIZER │ - │ (Central IdP) │ - │ │ - │ • Unified user store │ - │ • Session management │ - │ • MFA / TOTP │ - │ • Role-based access (RBAC) │ - │ • Social logins │ - │ • Custom token claims │ - └──────────────┬───────────────┘ - │ - Optional upstream federation - ┌─────────┬──────┼──────┬─────────┐ - │ │ │ │ │ - Google GitHub Azure Apple Facebook +```mermaid +flowchart TB + D["Internal Dashboard
(React)"] + P["Customer Portal
(Next.js)"] + T["Admin Tools
(Go CLI)"] + E["Third-Party
(Grafana, GitLab, ...)"] + + D & P & T & E -- "OIDC Authorization Code + PKCE" --> Auth + + subgraph Auth["AUTHORIZER (Central IdP)"] + F["Unified user store · Session management
MFA / TOTP · RBAC · Social logins · Custom token claims"] + end + + Auth -- "optional upstream federation" --> Google & GitHub & Azure & Apple & Facebook ``` Every app talks to Authorizer using standard **OIDC Discovery**. Point your app at `https://auth.yourcompany.com/.well-known/openid-configuration` and the client library auto-discovers all endpoints. @@ -313,10 +301,18 @@ Model tenants as organizations with per-org members and per-org roles, and attac Authorizer acts as a SAML 2.0 Service Provider per organization: register a corporate IdP's entity ID, SSO URL, and signing certificate, and that org's users log in through their IdP with JIT provisioning. See [SAML SSO](../enterprise/org-saml). +### SAML 2.0 Identity Provider (IdP) + +The architectural inverse of the SP role above: per organization, Authorizer can also act as a **SAML 2.0 Identity Provider**, issuing signed assertions to downstream Service Providers (Zendesk, Salesforce, Notion, …) so their SAML-based SSO logs users in with their existing Authorizer session. Each org gets its own SAML signing keypair with `current` / `active` / `retired` rotation states, and both SP-initiated and opt-in IdP-initiated flows are supported. See [SAML Identity Provider](../enterprise/saml-idp). + ### Per-Organization OIDC SSO (Broker) The OIDC sibling of the SAML SP: Authorizer brokers a per-org upstream OIDC IdP (Okta, Entra ID, Google Workspace) as a Relying Party. See [OIDC SSO](../enterprise/org-sso-oidc). +### Verified Domains & Home Realm Discovery + +Organizations can prove ownership of an email domain — via a DNS TXT challenge, or a trusted-assert shortcut for super-admins — so a login for `jane@acme.com` is automatically routed to Acme's SAML or OIDC connection instead of the generic login form. This routing lookup is called Home Realm Discovery (HRD) and is exposed at the public `/api/v1/org-discovery` endpoint (opt-in, off by default). A verified domain only ever affects routing — it never grants org membership and never blocks login. See [Verified Domains & Home Realm Discovery](../enterprise/org-domains). + ### SCIM 2.0 Provisioning (RFC 7644) Per-org inbound SCIM 2.0 endpoints let enterprise directories (Okta, Microsoft Entra ID, OneLogin) provision and deprovision users automatically. Deactivation synchronously revokes sessions and refresh tokens. See [SCIM Provisioning](../enterprise/scim). @@ -365,7 +361,8 @@ Authorizer gives you a **self-hosted, single-binary SSO server** that speaks sta | Custom access token scripts | | | [Client registry](./client-registry) + `client_credentials` grant | | | [Organizations](../enterprise/organizations) with per-org members & roles | | -| [Per-org SAML 2.0 SP](../enterprise/org-saml) & [OIDC broker SSO](../enterprise/org-sso-oidc) | | +| [Per-org SAML 2.0 SP](../enterprise/org-saml), [SAML 2.0 IdP](../enterprise/saml-idp) & [OIDC broker SSO](../enterprise/org-sso-oidc) | | +| [Verified domains & Home Realm Discovery](../enterprise/org-domains) | | | [SCIM 2.0 provisioning](../enterprise/scim) | | | [Workload identity](../enterprise/workload-identity) (K8s, SPIFFE) | | | [RFC 8693 token exchange](../enterprise/token-exchange) (delegation) | | diff --git a/docs/enterprise/org-domains.md b/docs/enterprise/org-domains.md new file mode 100644 index 0000000..d9deb37 --- /dev/null +++ b/docs/enterprise/org-domains.md @@ -0,0 +1,193 @@ +--- +sidebar_position: 2 +title: Verified Domains & Home Realm Discovery +--- + +# Verified Domains & Home Realm Discovery + +A **verified domain** proves that an [organization](./organizations) controls an email domain (`acme.com`), so a login for `jane@acme.com` can be automatically routed to Acme's SSO connection instead of showing the generic login form. This routing lookup is called **Home Realm Discovery (HRD)**, sometimes called "organization discovery." + +## The one rule that matters: domain ≠ membership + +A verified domain answers exactly one question — **"which org's SSO should a login for this email route to"** — and nothing else. It never determines: + +- **Who is a member.** Membership comes only from an explicit `OrgMember` row (via [`_add_org_member`](./organizations#manage-members), SSO JIT-provisioning, or SCIM). An external consultant `jane@consulting-firm.com` can be a full member of Acme even though `consulting-firm.com` is not a verified Acme domain. +- **Whether login is allowed.** A user whose email domain matches no verified domain is not blocked — they just don't get an SSO redirect, and fall back to whatever their membership already permits (password, social, magic link, or an SSO connection they access some other way). +- **What happens if a member's email domain changes.** Membership and roles are untouched — they were never keyed on the domain string. + +Deleting a verified domain only removes the routing hint. It never removes members, and it never revokes access. + +## How a domain gets verified + +Two methods ship today: + +| Method | Who can use it | Proof required | Operation | +|--------|-----------------|-----------------|-----------| +| **DNS TXT challenge** | Org admin (or super-admin) | Publish a TXT record at a specific name | `_request_org_domain` then `_verify_org_domain` | +| **Trusted assert** | Super-admin only | None — the platform operator is already trusted | `_add_verified_org_domain` | + +There is no auto-verify-by-owned-mailbox shortcut and no email-OTP-to-domain method in the current release — both were considered during design but did not ship. DNS TXT is the only self-serve proof path; trusted-assert is the operator escape hatch (used by the dashboard's "Add without DNS verification" quick-add for super-admins). + +A verified domain is a row **only once proven** — a pending DNS challenge is not a database row, it's a short-lived token in the memory store (~24h TTL) and is never used for routing until it's matched. + +## Admin GraphQL operations + +All are org-scoped-admin gated (super-admin, or that org's `authorizer:org_admin` member) except `_add_verified_org_domain`, which is super-admin only: + +| Operation | Type | Purpose | +|-----------|------|---------| +| `_request_org_domain` | mutation | Mint a DNS TXT challenge for `org_id` + `domain` | +| `_verify_org_domain` | mutation | Check the TXT record and, on match, insert the verified row | +| `_add_verified_org_domain` | mutation | **Super-admin only.** Insert a verified row with no proof (trusted-assert) | +| `_org_domains` | query | Paginated list of an org's verified domains (never another org's) | +| `_delete_org_domain` | mutation | Remove a verified mapping, freeing the domain for re-verification | + +### DNS TXT walkthrough + +**1. Request a challenge:** + +```graphql +mutation { + _request_org_domain(params: { org_id: "ORG_ID", domain: "acme.com" }) { + domain + record_type + record_name + record_value + } +} +``` + +Response: + +```json +{ + "data": { + "_request_org_domain": { + "domain": "acme.com", + "record_type": "TXT", + "record_name": "_authorizer-challenge.acme.com", + "record_value": "authorizer-domain-verification=<32-byte base32 token>" + } + } +} +``` + +**2. Publish the record**, then confirm it resolves: + +```bash +dig +short TXT _authorizer-challenge.acme.com +# "authorizer-domain-verification=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" +``` + +**3. Verify:** + +```graphql +mutation { + _verify_org_domain(params: { org_id: "ORG_ID", domain: "acme.com" }) { + domain + org_id + verified_at + } +} +``` + +The match is an **exact string comparison** against `authorizer-domain-verification=` — a substring/prefix match is deliberately not accepted. If the record isn't visible yet, `_verify_org_domain` returns a "dns verification failed" error and **leaves the challenge in place** so you can retry once DNS propagates; it does not require re-requesting a new token. The DNS lookup is TXT-only with a bounded ~5s timeout — no HTTP fetch is ever made against the domain. + +### Trusted-assert (super-admin) + +```graphql +mutation { + _add_verified_org_domain(params: { org_id: "ORG_ID", domain: "acme.com" }) { + domain + org_id + verified_at + } +} +``` + +Skips the DNS challenge entirely. Only a super-admin can call this — an org admin attempting it is rejected. Still subject to the same uniqueness and public-suffix guards as DNS verification (see below). + +### List and delete + +```graphql +query { + _org_domains(params: { org_id: "ORG_ID", pagination: { limit: 100 } }) { + pagination { total } + org_domains { domain verified_at } + } +} +``` + +```graphql +mutation { + _delete_org_domain(params: { domain: "acme.com" }) { + message + } +} +``` + +`_delete_org_domain` takes only the domain — the owning org is loaded from the row first, and the org-admin check is authorized against *that* org, not a caller-supplied `org_id`. + +## Home Realm Discovery endpoint + +``` +GET /api/v1/org-discovery?email= +``` + +Public, unauthenticated, and gated behind the `--enable-org-discovery` flag (**default `false`** — off until an operator opts in; a single-tenant deployment with no per-org SSO never needs it). When disabled, the endpoint returns `404`. + +The response is deliberately minimal to avoid letting a caller enumerate your tenant list by probing domains: + +```json +// match with SSO connection +{ "connection": { "type": "saml", "login_url": "/oauth/saml/acme/login" } } +``` + +```json +// verified domain but no SSO connection, OR no match at all — indistinguishable +{ "connection": null } +``` + +Nothing beyond `login_url` (which necessarily embeds the org's slug) is ever returned — no `organization_id`, `organization_name`, or `org_id`, and there's no operator "verbose" opt-in to add them. If the org has both an active SAML and an active OIDC connection, SAML takes precedence. + +A pending (unverified) domain is never resolved — `GetOrgDomainByDomain` is a lookup against verified rows only, so it's structurally impossible for an in-flight challenge to affect routing. + +The endpoint has its own per-IP rate limit bucket, independent of the global request limiter, to blunt domain-enumeration attempts; a malformed `email` returns a uniform `400` with no parsing detail. + +## `/app` login page integration + +The hosted login page (`/app`, gated by `--enable-login-page`) shows an email-first step when discovery is enabled: + +1. The user types their email and submits. +2. The SPA calls `GET /api/v1/org-discovery?email=…`. +3. **SSO match** — the browser is redirected to `connection.login_url`, with the original `redirect_uri` and `state` appended so the post-login flow returns to your app, not to `/app`. +4. **No match (or discovery disabled/erroring)** — the standard password/social/magic-link login renders inline, exactly as it does today. Discovery is a routing hint only; it never blocks or fails a login. + +This flag is opt-in per deployment (`--enable-org-discovery`, mirrored to the SPA as `Meta.is_org_discovery_enabled`); leaving it off keeps `/app` byte-for-byte the same as before this feature existed. + +### App-supplied escape hatch + +If your app already knows which org a user belongs to (a subdomain, an org picker, your own domain-to-tenant map), skip discovery entirely and send the user straight to that org's login: + +``` +GET /oauth/saml/{org_slug}/login?redirect_uri=...&state=... +``` + +or the [OIDC broker](./org-sso-oidc) equivalent. This is the recommended path whenever your app already owns the org mapping — HRD exists for the case where it doesn't. + +## Dashboard UX + +The org detail page's **Verified Domains** panel: a table of the org's verified domains with their verification date, an **Add Domain** dialog that walks through request → show the DNS record with copy-to-clipboard → verify (with a retryable "not propagated yet" hint on failure), and a super-admin-only "Add without DNS verification" quick path. Each row has a **Delete** action. + +## Security & behavior reference + +| Behavior | Detail | +|----------|--------| +| **Public-suffix / consumer guard** | Bare TLDs and multi-label public suffixes (`com`, `co.uk`) are rejected, plus a blocklist of major consumer providers (`gmail.com`, `outlook.com`, `icloud.com`, …) even where the public-suffix list would technically allow them. | +| **Uniqueness** | The normalized domain is the row's primary key — one verified domain routes to exactly one org, enforced atomically at the storage layer (not a check-then-insert race). A second org verifying an already-claimed domain gets a `domain_already_verified_by_another_org` error; re-verifying your own already-verified domain is a no-op success. | +| **Subdomains are exact-match** | Verifying `acme.com` does **not** cover `eng.acme.com` — each (sub)domain is its own row, matching the DNS challenge's exact-name granularity. | +| **Normalization** | One shared normalizer (IDNA/UTS-46, lowercased, punycode, no scheme/path/port/wildcard) is used for both writes and HRD lookups, so a login email resolves to the exact stored value. | +| **Org-delete cascade** | Deleting an organization cascades to delete its verified domains, freeing them for re-claim by anyone. Without this a deleted org's domain would be permanently unclaimable, since the domain is the primary key. | +| **Rate limiting** | `_request_org_domain` and `_verify_org_domain` are rate-limited per org; the public `/api/v1/org-discovery` endpoint is rate-limited per IP, separately from the global request limiter. | +| **Token secrecy** | The DNS challenge token is never logged; only the verification *method* (`dns_txt` / `trusted_assert`) is recorded in the audit event, never the token value. | +| **Multi-instance deployments** | The pending DNS challenge lives in the memory store, which is per-node unless Redis is configured — the same requirement as magic links and OTP. Configure Redis if `_request_org_domain` and `_verify_org_domain` can land on different nodes. | diff --git a/docs/enterprise/org-saml.md b/docs/enterprise/org-saml.md index ae1120e..527817e 100644 --- a/docs/enterprise/org-saml.md +++ b/docs/enterprise/org-saml.md @@ -1,5 +1,5 @@ --- -sidebar_position: 3 +sidebar_position: 4 title: Per-Org SAML 2.0 SSO --- @@ -17,6 +17,8 @@ For [organizations](./organizations) whose IdP speaks SAML 2.0 (Okta, Microsoft ## Setup +All the admin ops below accept a super-admin, or that org's `authorizer:org_admin` member — see [Org-scoped admin](./organizations#org-scoped-admin). + ### 1. Collect the IdP's details From the org's IdP you need: the **entity ID** (the assertion `Issuer`), the **SSO URL** (HTTP-Redirect binding), and the **X.509 signing certificate** (PEM). @@ -102,3 +104,7 @@ Disabled by default. Set `allow_idp_initiated: true` only if the org's IdP canno ### AuthnRequest signing AuthnRequests are currently emitted unsigned (HTTP-Redirect binding). All SP-side assertion security is enforced at the ACS; request signing protects the IdP against forged requests and is a future upgrade. + +## Acting as an IdP instead? + +This page covers Authorizer as the **SP** (consuming assertions from an org's upstream corporate IdP). Authorizer can also act as a **SAML IdP** — issuing signed assertions to downstream Service Providers like Zendesk or Salesforce, per org. See [SAML 2.0 Identity Provider](./saml-idp). diff --git a/docs/enterprise/org-sso-oidc.md b/docs/enterprise/org-sso-oidc.md index e5fed6e..71635c5 100644 --- a/docs/enterprise/org-sso-oidc.md +++ b/docs/enterprise/org-sso-oidc.md @@ -1,5 +1,5 @@ --- -sidebar_position: 2 +sidebar_position: 3 title: Per-Org OIDC SSO --- @@ -7,15 +7,24 @@ title: Per-Org OIDC SSO Each [organization](./organizations) can bring its own upstream OIDC identity provider (Okta, Microsoft Entra ID, Google Workspace, …). Authorizer acts as the **Relying Party (broker)**: the org's users authenticate at their corporate IdP, and Authorizer JIT-provisions them and issues a normal Authorizer session — your apps keep integrating with Authorizer only. -``` -User ──► /oauth/sso/{org_slug}/login ──► upstream IdP /authorize (PKCE + state + nonce) - ◄── /oauth/sso/{org_slug}/callback ◄── code -Authorizer verifies ID token (JWKS, iss, aud, nonce) ──► JIT provision ──► session cookie - ──► redirect back to your app (redirect_uri + state) +```mermaid +sequenceDiagram + participant U as User + participant A as Authorizer + participant I as Upstream IdP + + U->>A: GET /oauth/sso/{org_slug}/login + A->>I: /authorize (PKCE + state + nonce) + I-->>A: GET /oauth/sso/{org_slug}/callback (code) + A->>A: verify ID token (JWKS, iss, aud, nonce) + A->>A: JIT provision + session cookie + A-->>U: redirect to your app (redirect_uri + state) ``` ## Setup +All the admin ops below accept a super-admin, or that org's `authorizer:org_admin` member — see [Org-scoped admin](./organizations#org-scoped-admin). + ### 1. Register Authorizer at the upstream IdP Create an OIDC app (authorization code flow) at the org's IdP with the redirect URI: diff --git a/docs/enterprise/organizations.md b/docs/enterprise/organizations.md index 4b219de..0adc8c6 100644 --- a/docs/enterprise/organizations.md +++ b/docs/enterprise/organizations.md @@ -19,23 +19,24 @@ Organizations are the anchor for the other enterprise features: | [OIDC SSO broker](./org-sso-oidc) | One upstream OIDC IdP connection | | [SAML 2.0 SP](./org-saml) | One upstream SAML IdP connection | | [SCIM 2.0 provisioning](./scim) | One inbound SCIM endpoint + bearer token | +| [Verified domains + Home Realm Discovery](./org-domains) | Many verified email domains, used to route logins to this org's SSO | Users who sign in through an org's SSO connection are JIT-provisioned and automatically added as members of that organization. ## Admin API -All operations are super-admin GraphQL operations (`x-authorizer-admin-secret` header or the `authorizer.admin` cookie). +Organization create/update/delete/list stay super-admin-only (`x-authorizer-admin-secret` header or the `authorizer.admin` cookie) — only the platform operator provisions/deprovisions tenants. Fetching a single org and all member-management ops are **org-scoped-admin gated**: super-admin, or that org's `authorizer:org_admin` member (see [Org-scoped admin](#org-scoped-admin) below). -| Operation | Type | Purpose | -|-----------|------|---------| -| `_create_organization` | mutation | Create an org (`name` slug must be unique and URL-safe) | -| `_update_organization` | mutation | Update `name`, `display_name`, `enabled` | -| `_delete_organization` | mutation | Delete the org | -| `_organization` | query | Fetch one org by `id` | -| `_organizations` | query | Paginated list | -| `_add_org_member` | mutation | Add a user as member with optional per-org roles | -| `_remove_org_member` | mutation | Remove a member | -| `_org_members` | query | Paginated member list for an org | +| Operation | Type | Purpose | Who can call | +|-----------|------|---------|---------------| +| `_create_organization` | mutation | Create an org (`name` slug must be unique and URL-safe) | Super-admin only | +| `_update_organization` | mutation | Update `name`, `display_name`, `enabled` | Super-admin only | +| `_delete_organization` | mutation | Delete the org | Super-admin only | +| `_organization` | query | Fetch one org by `id` | Super-admin or that org's `authorizer:org_admin` | +| `_organizations` | query | Paginated list of all orgs | Super-admin only | +| `_add_org_member` | mutation | Add a user as member with optional per-org roles | Super-admin or that org's `authorizer:org_admin` | +| `_remove_org_member` | mutation | Remove a member | Super-admin or that org's `authorizer:org_admin` | +| `_org_members` | query | Paginated member list for an org | Super-admin or that org's `authorizer:org_admin` | ### Create an organization @@ -91,5 +92,37 @@ mutation { Notes: - Membership is unique per `(org_id, user_id)` — adding the same user twice is rejected. -- Per-org roles are independent across orgs: the same user can be `admin` in one org and `viewer` in another. +- Per-org roles are independent across orgs: the same user can be `admin` in one org and `viewer` in another. These are your app's own roles — not to be confused with the reserved `authorizer:org_admin` role below. - SSO JIT provisioning and SCIM provisioning create memberships automatically; `_add_org_member` is for manual/back-office management. + +## Org-scoped admin + +A member holding the reserved, namespaced role **`authorizer:org_admin`** can self-manage that one org's SSO/SCIM/domain settings — [SAML](./org-saml), [OIDC](./org-sso-oidc), [SCIM](./scim), and [verified domains](./org-domains) — without the platform super-admin secret. Every gated operation passes when the caller is either a platform super-admin, or an authenticated member whose `OrgMembership` for that org includes this exact role string. + +It is intentionally namespaced (not the bare `admin`) so it can never collide with your app's own per-org roles — granting a member `roles: ["admin"]` for your app's own purposes does **not** give them org-scoped admin rights. + +Grant it like any other per-org role, via `_add_org_member` (or by adding it to an existing member's `roles`): + +```graphql +mutation { + _add_org_member( + params: { + org_id: "ORG_ID" + user_id: "USER_ID" + roles: ["authorizer:org_admin"] + } + ) { + id + org_id + user_id + roles + } +} +``` + +Scope of what this grants: + +- **Org create, update, delete, and the all-orgs list (`_organizations`) remain super-admin-only** — an org-admin can never provision/deprovision tenants or see other orgs. +- An org-admin can manage members of **their own org only** (including granting `authorizer:org_admin` to another member of that org) — never another org's membership. +- A non-super-admin caller cannot remove an org's last `authorizer:org_admin` member, so an org can't accidentally lock itself out of self-service (a super-admin can always recover it). +- Bootstrapping an org's first `authorizer:org_admin` still requires a super-admin, via `_create_organization` followed by `_add_org_member`. diff --git a/docs/enterprise/saml-idp.md b/docs/enterprise/saml-idp.md new file mode 100644 index 0000000..840f4a7 --- /dev/null +++ b/docs/enterprise/saml-idp.md @@ -0,0 +1,132 @@ +--- +sidebar_position: 5 +title: SAML 2.0 Identity Provider +--- + +# SAML 2.0 Identity Provider (IdP) + +Per [organization](./organizations), Authorizer can also act as a **SAML 2.0 Identity Provider** — issuing signed assertions to downstream Service Providers (Zendesk, Salesforce, Notion, …) so their SAML-based SSO logs users in with their existing Authorizer session. This is the architectural inverse of [Authorizer as SAML SP](./org-saml): there Authorizer *consumes* an assertion from a corporate IdP; here it *produces* one for a downstream SP. Do not confuse the two — a `SAMLServiceProvider` row (this page) and an `OrgSAMLConnection` row (the SP-role doc) are unrelated tables. + +```mermaid +flowchart LR + subgraph SP["Per-org SP role (org-saml.md)"] + direction LR + Okta -- assertion --> Auth1["Authorizer (= SP)"] + end + subgraph IDP["Per-org IdP role (this page)"] + direction LR + Auth2["Authorizer (= IdP)"] -- assertion --> Zendesk + end +``` + +## Endpoints (per org) + +| Endpoint | Method | Purpose | +|----------|--------|---------| +| `/saml/idp/{org_slug}/metadata` | GET | IdP metadata XML — import this at the downstream SP | +| `/saml/idp/{org_slug}/sso` | GET, POST | SP-initiated SSO — receives the SP's `AuthnRequest` (HTTP-Redirect or HTTP-POST binding) | +| `/saml/idp/{org_slug}/sso/{sp_id}` | GET | IdP-initiated SSO — unsolicited assertion POSTed to the SP identified by `sp_id`, gated by that SP's `allow_idp_initiated` flag | + +All three resolve the org by `org_slug`; an SP registered for one org is never reachable through another org's endpoints. + +## Registering a downstream SP + +Admin operations (super-admin or org-admin of `org_id`), available via GraphQL, gRPC (`AuthorizerAdminService`), and the dashboard (**Organization → SAML Service Providers**): + +| GraphQL operation | Type | gRPC / REST-gateway | Purpose | +|--------------------|------|----------------------|---------| +| `_create_saml_service_provider` | mutation | `CreateSamlServiceProvider` · `POST /v1/admin/create_saml_service_provider` | Register a downstream SP | +| `_update_saml_service_provider` | mutation | `UpdateSamlServiceProvider` · `POST /v1/admin/update_saml_service_provider` | Update name, `entity_id`, `acs_url`, cert, attribute mapping, `allow_idp_initiated`, `is_active` | +| `_delete_saml_service_provider` | mutation | `DeleteSamlServiceProvider` · `POST /v1/admin/delete_saml_service_provider` | Remove a registered SP | +| `_saml_service_provider` | query | `GetSamlServiceProvider` · `POST /v1/admin/saml_service_provider` | Fetch one SP by `id` | +| `_list_saml_service_providers` | query | `ListSamlServiceProviders` · `POST /v1/admin/saml_service_providers` | Paginated list for an org | +| `_import_saml_sp_metadata` | mutation | `ImportSamlSpMetadata` · `POST /v1/admin/import_saml_sp_metadata` | Parse pasted SP metadata XML into `entity_id` / `acs_url` / certificate (does **not** create a record — prefill only; super-admin only, no remote fetch) | + +```graphql +mutation { + _create_saml_service_provider( + params: { + org_id: "ORG_ID" + name: "Zendesk prod" + entity_id: "https://acme.zendesk.com/sso/saml" + acs_url: "https://acme.zendesk.com/access/saml" + # sp_cert_pem: optional SP signing certificate (PEM) + # name_id_format: defaults to urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress + # mapped_attributes: optional JSON, e.g. + # "{\"email\":\"email\",\"given_name\":\"firstName\",\"groups\":\"memberOf\"}" + # allow_idp_initiated: defaults to false (SP-initiated only) + } + ) { + id + entity_id + acs_url + allow_idp_initiated + is_active + } +} +``` + +`entity_id` is unique within the org — `(org_id, entity_id)` is the lookup that resolves an incoming `AuthnRequest` to this record, and it also fixes the assertion's `Audience`. `acs_url` is the **only** place an assertion is ever POSTed; a request-supplied ACS URL that doesn't match this value is rejected. + +## Attribute mapping + +`mapped_attributes` is JSON mapping Authorizer profile fields to the SAML attribute **names** this SP expects — the inverse of the SP-role's `attribute_mapping`. Defaults when omitted: + +| Authorizer field | Default SAML attribute | +|------------------|------------------------| +| `email` | `email` (only ever emitted once the user's email is verified) | +| `given_name` | `given_name` | +| `family_name` | `family_name` | +| `nickname` | `nickname` | +| `picture` | `picture` | + +### Group assertion (SCIM groups → SAML `groups`) + +If the user belongs to [SCIM groups](./scim) in the issuing org, their group display names are emitted as one multi-valued attribute (default name `groups`, or a `groups` key in `mapped_attributes`). This is gated to the issuing org's namespace only — a group from a different org the user also belongs to can never appear in the assertion. See the [SCIM doc](./scim) for how groups and memberships are managed; this page only covers the SAML projection. + +## Signing key rotation + +Each org gets its own RSA signing keypair (`SAMLIDPKey`), generated lazily on first use — independent of the JWT signing key, since XML-DSIG needs the X.509 certificate wrapper. A key's `status` is one of: + +| Status | Meaning | +|--------|---------| +| `current` | The single key new assertions are signed with. At most one per org. | +| `active` | A formerly-current key, still published in IdP metadata so SPs with cached metadata keep validating during the rotation overlap. Never signs. | +| `retired` | Explicitly retired by an admin — no longer published, no longer usable. Retirement is always an explicit action, never time-based. | + +Admin operations: + +| GraphQL operation | Type | Purpose | +|--------------------|------|---------| +| `_rotate_saml_idp_cert` | mutation | Generate a new keypair as `current`; the previous `current` key is demoted to `active` | +| `_retire_saml_idp_key` | mutation | Retire an `active` key (an org's `current` key cannot be retired — rotate first) | +| `_list_saml_idp_keys` | query | List an org's keys with their `status` (private key is never projected) | + +```graphql +mutation { + _rotate_saml_idp_cert(params: { org_id: "ORG_ID" }) { + id + status + algorithm + } +} +``` + +Recommended rotation workflow: call `_rotate_saml_idp_cert`, wait for downstream SPs to refresh their cached copy of `/saml/idp/{org_slug}/metadata` (both certs are published during the overlap), then call `_retire_saml_idp_key` on the old key once you've confirmed nothing still needs it. + +## Security model + +| Invariant | Enforcement | +|-----------|-------------| +| ACS/EntityID strict binding | The SP is resolved from the registered `SAMLServiceProvider` row by the `AuthnRequest` issuer; the ACS URL is taken **only** from that row, never from the request — closes the open-redirect / assertion-exfiltration path. An unregistered SP is refused. | +| Audience isolation | The assertion `Audience` is set to the resolved SP's `entity_id`, so an assertion minted for SP-A can never validate at SP-B. | +| Org-membership gating | The authenticated user must be a member of the org the assertion is minted for. Without this, any logged-in Authorizer user could hit another org's `/saml/idp/{org}/sso` and get an assertion signed by that org's key. Absent-or-errored membership is treated as denied (fail-closed), never silently allowed. | +| Verified-email gating | When the Subject `NameID` (or the `email` attribute) would be the user's email address, that email must be verified — otherwise a member could register `victim@org.com` and be asserted as the victim to an SP that keys off email. | +| IdP-initiated gating | Unsolicited (IdP-initiated) SSO is refused unless the SP's `allow_idp_initiated` is `true`. Default is `false` (SP-initiated only). | +| RelayState bound | IdP-initiated `RelayState` is capped at 2048 bytes and dropped (not rejected) above that — it is only ever reflected into the delivered response form, never treated as a redirect target. | +| Signing scope | Signing uses only the org's `current` key; metadata additionally publishes `active` certs so a rotation overlap doesn't break SPs with cached metadata. | +| No unauthenticated issuance | SP-initiated flows only produce an assertion once the browser presents a valid Authorizer session; otherwise the request bounces through the normal login UI and resumes via an opaque, single-use continuation token — never a request-supplied redirect. | + +### IdP-initiated SSO + +`GET /saml/idp/{org_slug}/sso/{sp_id}` POSTs an unsolicited assertion straight to the SP's ACS — there's no `AuthnRequest` to bind an `InResponseTo` to, so replay defense for this path is the **consuming SP's** job (single-use `AssertionID`); Authorizer bounds exposure with a tight `NotBefore`/`NotOnOrAfter` window. Only enable `allow_idp_initiated` for SPs that support unsolicited assertions. diff --git a/docs/enterprise/scim.md b/docs/enterprise/scim.md index 139b925..e72a111 100644 --- a/docs/enterprise/scim.md +++ b/docs/enterprise/scim.md @@ -1,5 +1,5 @@ --- -sidebar_position: 4 +sidebar_position: 6 title: SCIM 2.0 Provisioning --- @@ -15,6 +15,8 @@ The organization is resolved **solely from the bearer token** — never from the ## Create the endpoint (admin API) +All the ops below accept a super-admin, or that org's `authorizer:org_admin` member — see [Org-scoped admin](./organizations#org-scoped-admin). + One SCIM endpoint per org, keyed by `org_id`: ```graphql @@ -46,19 +48,53 @@ All keyed by `org_id`. | Route | Method | Behavior | |-------|--------|----------| | `/scim/v2/Users` | POST | Create (JIT-provision) a user in the org | -| `/scim/v2/Users` | GET | Only the `userName eq "..."` filter (the IdP dedup probe). An unfiltered list returns an empty set — full org enumeration is out of scope | +| `/scim/v2/Users` | GET | Single-term filter over `userName`, `emails.value`, `name.givenName`, `name.familyName`, `active`, `externalId` (see [Filter operators](#filter-operators)), with `startIndex`/`count` pagination. An unfiltered list returns an empty set — full org enumeration is out of scope | | `/scim/v2/Users/{id}` | GET | Fetch one user | | `/scim/v2/Users/{id}` | PUT | Replace mutable profile fields + `active` flag | -| `/scim/v2/Users/{id}` | PATCH | Partial update, including `active` transitions | +| `/scim/v2/Users/{id}` | PATCH | Partial update — see [User PATCH](#user-patch) for the supported attributes | | `/scim/v2/Users/{id}` | DELETE | Treated as deactivation (`active: false`) | +| `/scim/v2/Groups` | POST | Create a group in the org (see [Group provisioning](#group-provisioning)) | +| `/scim/v2/Groups` | GET | Only the `displayName eq "..."` filter (case-insensitive). An unfiltered list, or any other operator, is unsupported | +| `/scim/v2/Groups/{id}` | GET | Fetch one group, with resolved `members` | +| `/scim/v2/Groups/{id}` | PUT | Replace `displayName` and set membership to exactly the posted `members` | +| `/scim/v2/Groups/{id}` | PATCH | Partial update to `displayName`/`externalId` plus member add/remove/replace | +| `/scim/v2/Groups/{id}` | DELETE | Remove the group and its FGA membership tuples | | `/scim/v2/ServiceProviderConfig`, `/ResourceTypes`, `/Schemas` | GET | SCIM discovery documents | -Groups are not yet implemented. - Notes: - `userName` is required on create; if the IdP omits it, the primary email is used as `userName`. - `externalId` is stored and round-tripped, so the IdP can correlate its own IDs. +- Group operations return `501` (`group provisioning is not enabled on this server`) if the org has no [FGA](../core/fga-guide) authorization engine configured — group membership is stored as FGA tuples, not a database column. + +## Filter operators + +`GET` list filters are single-term only — compound filters (`and`/`or`/`not`), value-path filters (`emails[type eq "work"]`), and ordering operators (`gt`/`lt`/…) are not supported and return `400 invalidFilter`, never a silent empty `200` (a connector reads an empty `200` as "does not exist" and duplicates the create). + +| Resource | Attributes | Operators | +|----------|-----------|-----------| +| `/scim/v2/Users` | `userName`, `emails.value`, `name.givenName`, `name.familyName`, `active`, `externalId` | `eq`, `ne`, `co`, `sw`, `pr` (`co`/`sw` rejected on `active` — meaningless on a boolean) | +| `/scim/v2/Groups` | `displayName` | `eq` only | + +Notes: + +- `eq` on `userName`/`emails.value`/`externalId` (Users) and `displayName` (Groups) is an indexed lookup — the dedup probe an IdP sends before every create. Every other operator/attribute combination falls back to a bounded in-app scan of the org's members (capped at 1,000 scanned memberships, paged 100 at a time) so a very large org can't turn a filter into unbounded work. +- String comparisons are case-insensitive, matching SCIM's default `caseExact:false` for these attributes. +- `startIndex`/`count` (RFC 7644 §3.4.2.4) paginate the Users list response; Groups list only ever returns 0 or 1 resource (the `displayName eq` match), so pagination params are not applicable there. + +## User PATCH {#user-patch} + +`PATCH /scim/v2/Users/{id}` supports `add`/`replace` operations (treated identically for these single-valued attributes) on: + +| Path | Effect | +|------|--------| +| `active` | Deactivate/reactivate — see [Deprovisioning semantics](#deprovisioning-semantics) | +| `name.givenName`, `name.familyName` | Update the profile name | +| `userName`, `emails` / `emails[type eq "..."].value` | Update the user's email (uniqueness-checked — `409` if another user already holds it) | +| `phoneNumber`, `phoneNumbers` / `phoneNumbers[...].value` | Update the phone number (uniqueness-checked) | +| `externalId` | Update the correlation id | + +Both the path-qualified shape (`{"op":"replace","path":"active","value":false}`) and the no-path attribute-map shape (`{"op":"replace","value":{"active":false,"name":{"givenName":"Jonathan"}}}`) are accepted, since Okta and Entra don't send the same one. `remove` ops and any path this server doesn't model (e.g. enterprise-extension attributes like `manager`/`department`) are silently skipped rather than rejected, so a connector that also syncs unsupported attributes doesn't fail the whole request. A patch that changes nothing returns the resource unchanged and fires no webhook. ## Deprovisioning semantics @@ -92,6 +128,89 @@ curl -s -G https://your-authorizer.example/scim/v2/Users \ --data-urlencode 'filter=userName eq "jane@acme.com"' ``` +## Group provisioning + +SCIM Groups (RFC 7643 §4.2) provision org-scoped groups whose membership drives both authorization and SAML assertions. A group carries only identity/metadata (`displayName`, `externalId`, timestamps) — membership is **not** a database column. It's modelled as [FGA](../core/fga-guide) relationship tuples (`group:/#member@user:`), the same graph that resolves roles and nested groups, so the group→role grant and the SAML group projection are both just reads of that one graph. + +- **Cross-org isolation (H6)**: a group is only visible/mutable through the SCIM connection whose org it belongs to; a cross-org group id resolves to `404`, indistinguishable from a nonexistent one. +- **`displayName` uniqueness** is enforced per-org at the service layer (not a DB constraint, for identical behavior across every storage backend) — a create that collides on `displayName` with no matching `externalId` gets `409 uniqueness`. +- **`displayName` matching is case-insensitive** (SCIM `caseExact:false`, RFC 7644 §3.4.2.2): filtering for `displayname eq "engineers"` finds a group stored as `"Engineers"`, and returns it with its stored casing. +- **`externalId` correlation**: a create carrying an `externalId` that already identifies a group in the org is idempotent — it adopts a `displayName` rename and syncs members (`200`) instead of creating a duplicate (`201`). +- **Pagination / safety valve**: the `displayName` lookup scans the org's groups (paged internally, not a single unbounded query) up to a **100,000-group safety valve** per org — a circuit-breaker against pathological orgs, not a realistic ceiling on group count. Reading a group's `members` similarly pages through the underlying FGA tuples 100 at a time. +- **Deleting a group** removes its row and every membership tuple pointing at it; role→group bindings (`role:.../r#assignee@group:.../g#member`) are left for an admin to clean up via `_fga_delete_tuples` if desired — group ids are UUIDs and never reused, so a dangling binding simply resolves to nobody. + +### PATCH operations on Groups + +`PATCH /scim/v2/Groups/{id}` supports: + +| Path | Op | Effect | +|------|----|--------| +| `displayName` | `add`/`replace` | Rename (uniqueness-checked) | +| `externalId` | `add`/`replace` | Update the correlation id | +| `members` (or `members[value eq "..."]`) | `add` | Add the listed users to the group | +| `members` (or `members[value eq "..."]`) | `remove` | Remove the listed users | +| `members` | `remove`, no member list | **Clear every member** (the unfiltered "empty this group" shape IdPs send when deprovisioning a group) | +| `members` | `replace` | Set membership to exactly the posted list (an empty list clears every member) | + +Both the RFC/Okta filtered-path shape (`{"op":"remove","path":"members[value eq \"\"]"}`) and Entra's non-RFC shape (`{"op":"remove","path":"members","value":[{"value":""}]}`) are accepted, along with the no-path attribute-map shape. Every added member must already be an org member — a user id from outside the org is silently skipped, never written as a tuple. A `PATCH` op targeting an unsupported path (e.g. `emails`, `name.givenName`) returns `400 invalidPath`, never a silent no-op `200`. + +### Role and SAML projection + +Group membership isn't read directly by clients — it's projected into two places at session-issuance time: + +- **OpenFGA roles**: a role granted to a group (`role:/#assignee@group:/#member`) is resolved transitively for every member and unioned onto the roles already minted into that org's session/JWT — see the [FGA guide](../core/fga-guide) for the tuple model (in particular [groups as subjects](../core/fga-guide#groups-as-subjects)). This only ever *adds* roles; a lookup failure derives none rather than blocking login. +- **SAML group assertions**: when Authorizer acts as an [IdP](./saml-idp), a user's groups *in the org the assertion is being issued for* are resolved and emitted as a multi-valued attribute (`groups` by default) — see [Group assertion](./saml-idp#group-assertion-scim-groups--saml-groups). + +Both projections are org-namespaced and fail closed: a user who belongs to groups/roles in other orgs never leaks them into a token or assertion issued for this org, and any FGA lookup error yields nothing rather than a partial or wrongly-scoped set. + +### Example: create and patch a group + +```bash +curl -s -X POST https://your-authorizer.example/scim/v2/Groups \ + -H "Authorization: Bearer $SCIM_TOKEN" \ + -H "Content-Type: application/scim+json" \ + -d '{ + "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"], + "displayName": "Engineers", + "externalId": "grp-eng", + "members": [{ "value": "" }] + }' +``` + +```bash +curl -s -X PATCH https://your-authorizer.example/scim/v2/Groups/ \ + -H "Authorization: Bearer $SCIM_TOKEN" \ + -H "Content-Type: application/scim+json" \ + -d '{ + "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], + "Operations": [ + { "op": "add", "path": "members", "value": [{ "value": "" }] } + ] + }' +``` + +## Provisioning webhooks + +Every SCIM lifecycle event fires an [event webhook](../core/graphql-api#webhooks) (configured via `_add_webhook`, keyed to one of the event names below), so a customer's own audit/automation pipeline can react to IdP-driven changes without polling: + +| Event | Fires when | +|-------|-----------| +| `user.provisioned` | An IdP creates a user via SCIM | +| `user.deprovisioned` | An IdP deactivates a user (`active:false` PATCH/PUT, or `DELETE`) | +| `user.scim_updated` | An IdP changes a user's attributes via SCIM (name/email/phone/externalId, or reactivation) | +| `group.created` | An IdP creates a group via SCIM | +| `group.updated` | An IdP changes a group's `displayName`/`externalId` or membership | +| `group.deleted` | An IdP deletes a group via SCIM | + +Delivery is **asynchronous** — fired off the request path after the SCIM operation commits, so a slow or unreachable webhook endpoint never delays or fails the IdP's HTTP response. Delivery is best-effort per webhook (one bad endpoint doesn't block the others) and logged. + +Payload shape: + +- User events: `{ "webhook_id", "event_name", "event_description", "user": { ...full user object... } }` +- Group events: `{ "webhook_id", "event_name", "event_description", "group": { "id", "org_id", "display_name", "external_id" } }` — note the key is `group`, not `user`, and it carries only identity fields (no members list); `external_id` is de-namespaced back to the raw IdP value. + +Every delivery carries an `X-Authorizer-Signature` header (HMAC-SHA256 over the raw JSON body, keyed on the client secret) so the receiver can verify authenticity. + ## IdP configuration pointers - **Okta** — add a SCIM 2.0 provisioning integration; set the SCIM connector base URL to `https://your-authorizer.example/scim/v2`, authentication mode *HTTP Header* with the bearer token, and enable Create/Update/Deactivate users. diff --git a/docs/enterprise/token-exchange.md b/docs/enterprise/token-exchange.md index 9d4a41c..39a6451 100644 --- a/docs/enterprise/token-exchange.md +++ b/docs/enterprise/token-exchange.md @@ -1,5 +1,5 @@ --- -sidebar_position: 5 +sidebar_position: 7 title: Token Exchange & Delegation --- @@ -93,6 +93,8 @@ effective = subject_token.scope ∩ agent.allowed_scopes ( ∩ requested scope, A delegated token can itself be the `subject_token` of a further exchange (app → agent → sub-agent). The prior `act` chain nests under the new actor. The chain depth is capped at **4**; deeper chains are rejected with `invalid_request`. +A runnable 4-hop example (`orchestrator → research-agent → crm-reader → export-agent`, plus live rejections for a 5th hop, scope re-widening, and actor-token substitution) is in [`with-agent-delegation`](https://github.com/authorizerdev/examples/tree/main/with-agent-delegation). + ## Resource binding & validation - `aud` on the delegated token is the single `resource` — it is only valid at that resource server. diff --git a/docs/enterprise/workload-identity.md b/docs/enterprise/workload-identity.md index 43d6dd5..6ab47ce 100644 --- a/docs/enterprise/workload-identity.md +++ b/docs/enterprise/workload-identity.md @@ -1,5 +1,5 @@ --- -sidebar_position: 6 +sidebar_position: 8 title: Workload Identity --- @@ -9,12 +9,18 @@ A [`service_account` client](../core/client-registry) normally authenticates wit The assertion works as client authentication on both machine grants: [`client_credentials`](../core/client-registry) and [token exchange](./token-exchange). -``` -Workload holds a platform-issued JWT (K8s SA token / SPIFFE JWT-SVID) - ──► POST /oauth/token grant_type=client_credentials - client_assertion_type=...jwt-bearer client_assertion= -Authorizer: iss → trusted issuer row → JWKS verify → aud/exp/subject/replay checks - ──► authenticates AS the bound service_account ──► Authorizer access token +```mermaid +sequenceDiagram + participant W as Workload + participant A as Authorizer + + Note over W: holds a platform-issued JWT (K8s SA token or SPIFFE JWT-SVID) + W->>A: POST /oauth/token, grant_type=client_credentials + W->>A: client_assertion_type=jwt-bearer, client_assertion=JWT + Note over A: iss maps to a trusted issuer row + Note over A: JWKS verify, then aud/exp/subject/replay checks + A->>A: authenticate AS the bound service_account + A-->>W: Authorizer access token ``` ## Request @@ -128,7 +134,23 @@ For testing: `kubectl create token worker -n payments --audience=https://your-au ### Online hardening: Kubernetes TokenReview -Offline JWKS validation proves the token was signed by the cluster — **not** that the bound Pod/ServiceAccount still exists. A deleted pod's not-yet-expired token would still verify. For high-security workloads a trust row can opt in to online validation (`enable_token_review` + `kubernetes_api_server_url` on the trusted-issuer record — honored at runtime but not yet exposed through the admin API): after the offline checks pass, Authorizer calls the cluster's `authentication.k8s.io/v1` TokenReview API and rejects the assertion fail-closed unless the apiserver reports it still authenticated. +Offline JWKS validation proves the token was signed by the cluster — **not** that the bound Pod/ServiceAccount still exists. A deleted pod's not-yet-expired token would still verify. For high-security workloads a trust row can opt in to online validation via `enable_token_review` + `kubernetes_api_server_url` — admin-settable fields on `_add_trusted_issuer` / `_update_trusted_issuer` (`kubernetes_api_server_url` must be a non-empty `https` URL whenever `enable_token_review` is `true`, checked at write time): after the offline checks pass, Authorizer calls the cluster's `authentication.k8s.io/v1` TokenReview API and rejects the assertion fail-closed unless the apiserver reports it still authenticated. + +```graphql +mutation { + _update_trusted_issuer( + params: { + id: "TRUSTED_ISSUER_UUID" + enable_token_review: true + kubernetes_api_server_url: "https://" + } + ) { + id + enable_token_review + kubernetes_api_server_url + } +} +``` - Authorizer authenticates the TokenReview call with its **own** in-cluster ServiceAccount token, which needs the `system:auth-delegator` ClusterRole. - The apiserver URL goes through the same SSRF-hardened client, so only a **publicly-routable apiserver endpoint** (e.g. a managed cluster's public API endpoint) works today — `https://kubernetes.default.svc` (a private ClusterIP) is rejected by design. diff --git a/docs/integrations/gatsbyjs.md b/docs/integrations/gatsbyjs.md index e25e506..4bcd6e8 100644 --- a/docs/integrations/gatsbyjs.md +++ b/docs/integrations/gatsbyjs.md @@ -46,6 +46,8 @@ Answer the few bootstrapping questions, - Selected `styled-components` for styling system - Select the additional features you want +> **Note:** If you use Gatsby 4 (the version used by the [`with-gatsbyjs` example](https://github.com/authorizerdev/examples/tree/main/with-gatsbyjs)), its native dependency `lmdb-store` does not compile on Node.js 20+. Use Node.js 16-18 to run it. + ## Step 4: Install `@authorizerdev/authorizer-react` ```sh @@ -58,6 +60,12 @@ OR yarn add @authorizerdev/authorizer-react ``` +Import the SDK stylesheet once, e.g. in `gatsby-browser.js` (see Step 6): + +```js +require('@authorizerdev/authorizer-react/styles.css') +``` + ## Step 5: Create Root Layout Create `src/components/layout.js` as the root layout for app with `AuthorizerProvider` @@ -105,6 +113,8 @@ export default function Layout({ children }) { Add root layout in gatsby browser config. Create `gatsby-browser.js` in the root of project with following content ```jsx +require('@authorizerdev/authorizer-react/styles.css') + const React = require('react') const Layout = require('./src/components/layout').default diff --git a/docs/integrations/react-native.md b/docs/integrations/react-native.md index 80439ef..483f6cf 100644 --- a/docs/integrations/react-native.md +++ b/docs/integrations/react-native.md @@ -39,36 +39,31 @@ Start your Authorizer instance with the required CLI flags: Note the `--client-id` value -- you will need it in the SDK configuration below. Check [Server Configuration](/core/server-config) for all available flags. -## Step 3: Install expo +## Step 3: Bootstrap react native project -``` -npm install --global expo-cli -``` - -## Step 4: Bootstrap react native project +The legacy global `expo-cli` (`expo init`) is deprecated. Use `create-expo-app` instead: ``` -expo init with-react-native-expo +npx create-expo-app with-react-native-expo --template blank-typescript ``` -Select blank default app - -## Step 5: Install dependencies +## Step 4: Install dependencies ``` -npm install @authorizerdev/authorizer-js expo-auth-session expo-random expo-secure-store expo-web-browser jwt-decode react-native-base64 +npm install @authorizerdev/authorizer-js expo-auth-session expo-crypto expo-secure-store expo-web-browser ``` -## Step 6: Create redirect url +> Note: `jwt-decode` and `react-native-base64` are **not** needed -- the flow below fetches the user profile via `authorizerRef.getProfile()` instead of decoding a JWT client-side. + +## Step 5: Create redirect url -Redirect URL is used to redirect back to your application once the authentication process is complete +Redirect URL is used to redirect back to your application once the authentication process is complete. The `useProxy` option (Expo's auth proxy service) has been removed from `expo-auth-session` -- call `makeRedirectUri()` with no arguments: ```js -const useProxy = false -const redirectUri = AuthSession.makeRedirectUri({ useProxy }) +const redirectUri = AuthSession.makeRedirectUri() ``` -## Step 7: Create AuthorizerJS Client +## Step 6: Create AuthorizerJS Client - Get your client ID from the `--client-id` flag used when starting the server @@ -76,6 +71,7 @@ const redirectUri = AuthSession.makeRedirectUri({ useProxy }) const authorizerClientID = 'YOUR_CLIENT_ID' const authorizerURL = 'YOUR_AUTHORIZER_INSTANCE_URL' const authorizationEndpoint = `${authorizerURL}/authorize` +const tokenEndpoint = `${authorizerURL}/oauth/token` const authorizerRef = new Authorizer({ clientID: authorizerClientID, authorizerURL: authorizerURL, @@ -83,9 +79,9 @@ const authorizerRef = new Authorizer({ }) ``` -## Step 8: Setup Expo AuthSession +## Step 7: Setup Expo AuthSession -Configure `useAuthRequest` hook with above configs +Configure `useAuthRequest` hook with above configs. The example uses the **authorization code + PKCE** flow (`responseType: 'code'`, `usePKCE: true`) rather than the implicit `token` flow, since Expo's `expo-auth-session` treats PKCE as the secure default for public clients. > Note: Use `offline_access` in scope if you want to get refresh token and want to perform silent refresh when user comes back to app. If your app is data sensitive we do not recommend using refresh token (example banking / finance app) @@ -94,37 +90,29 @@ const [request, result, promptAsync] = AuthSession.useAuthRequest( { redirectUri, clientId: authorizerClientID, - // id_token will return a JWT token - responseType: 'token', + responseType: 'code', // use offline access to get a refresh token and perform silent refresh in background scopes: ['openid', 'profile', 'email', 'offline_access'], extraParams: { // ideally, this will be a random value nonce: 'nonce', }, + usePKCE: true, }, { authorizationEndpoint } ) ``` -## Step 9: Listen to the authentication process change +## Step 8: Handle the authentication result -Get auth session result and set refresh token in secure store for silent refresh. -You also get the access token, id token for the further usage +On success, exchange the authorization code for tokens with `AuthSession.exchangeCodeAsync()`, store the refresh token, then fetch the profile with the access token to get the user's email. ```js const authorizerRefreshTokenKey = `authorizer_refresh_token` useEffect(() => { async function setResult() { - if (result) { - if (result.params.refresh_token) { - await SecureStore.setItemAsync( - authorizerRefreshTokenKey, - result.params.refresh_token - ) - } - + if (result && result.type === 'success') { if (result.error) { Alert.alert( 'Authentication error', @@ -133,24 +121,39 @@ useEffect(() => { return } - if (result.type === 'success') { - // Retrieve the JWT token and decode it - const jwtToken = result.params.id_token - const decoded = jwtDecode(jwtToken) + const codeRes = await AuthSession.exchangeCodeAsync( + { + code: result.params.code, + redirectUri, + clientId: authorizerClientID, + extraParams: { + code_verifier: request?.codeVerifier || '', + }, + }, + { tokenEndpoint } + ) - const { email } = decoded - setEmail(email) + if (codeRes?.refreshToken) { + await SecureStore.setItemAsync( + authorizerRefreshTokenKey, + codeRes.refreshToken + ) } + + // get profile using the access token + const { data: profileData } = await authorizerRef.getProfile({ + Authorization: `Bearer ${codeRes?.accessToken}`, + }) + setEmail(profileData?.email ?? undefined) } } setResult() }, [result]) ``` -## Step 10: Silent Refresh +## Step 9: Silent Refresh -Perform Silent Refresh. Note silent refresh will give you new access token, id token and refresh token. -You can use access token & id token for further API requests. +Perform Silent Refresh. Note silent refresh will give you a new access token and refresh token, which you can then use to re-fetch the profile. ```js // on init of app silently refresh token if it exists @@ -162,22 +165,27 @@ useEffect(() => { ) if (refreshToken) { try { - const res = await authorizerRef.getToken({ + const { data } = await authorizerRef.getToken({ grant_type: 'refresh_token', refresh_token: refreshToken, }) await SecureStore.setItemAsync( - 'authorizer_refresh_token', - res.refresh_token + authorizerRefreshTokenKey, + data?.refresh_token || '' ) - setEmail(jwtDecode(res.id_token).email) + + // get profile using the new access token + const { data: profileData } = await authorizerRef.getProfile({ + Authorization: `Bearer ${data?.access_token}`, + }) + setEmail(profileData?.email ?? undefined) } catch (err) { console.error(err) await SecureStore.deleteItemAsync(authorizerRefreshTokenKey) } } } catch (error) { - setEmail(null) + setEmail(undefined) await SecureStore.deleteItemAsync(authorizerRefreshTokenKey) } finally { setLoading(false) @@ -187,4 +195,33 @@ useEffect(() => { }, []) ``` -Also you can perform silent refresh when access token / id token expires. You also get `expires_in` in the response of token which you can use. So you can set time interval after which it should fetch new tokens. +Also you can perform silent refresh when the access token expires. You also get `expires_in` in the response of token which you can use. So you can set time interval after which it should fetch new tokens. + +## Step 10: Logout + +Revoke the refresh token, clear it from `SecureStore`, and open the Authorizer `/logout` endpoint in a browser session so the server-side cookie is cleared too: + +```js +const handleLogout = async () => { + setLoading(true) + setEmail(undefined) + + try { + const refreshToken = await SecureStore.getItemAsync( + authorizerRefreshTokenKey + ) + await authorizerRef.revokeToken({ + refresh_token: refreshToken || '', + }) + await SecureStore.deleteItemAsync(authorizerRefreshTokenKey) + await WebBrowser.openAuthSessionAsync( + `${authorizerURL}/logout?redirect_uri=${redirectUri}`, + 'redirectUrl' + ) + } catch (err) { + console.log(err) + } finally { + setLoading(false) + } +} +``` diff --git a/docs/migration/v1-to-v2.md b/docs/migration/v1-to-v2.md index f501c39..203ad78 100644 --- a/docs/migration/v1-to-v2.md +++ b/docs/migration/v1-to-v2.md @@ -272,9 +272,9 @@ Use these v2 **CLI flags** instead of v1 env or dashboard config. Flag names use | `DISABLE_BASIC_AUTHENTICATION` | `--enable-basic-authentication` (inverted) | | `DISABLE_EMAIL_VERIFICATION` | `--enable-email-verification` (inverted) | | `DISABLE_MAGIC_LINK_LOGIN` | `--enable-magic-link-login` (inverted) | -| `ENFORCE_MULTI_FACTOR_AUTHENTICATION` | `--enforce-mfa`, `--enable-mfa` | +| `ENFORCE_MULTI_FACTOR_AUTHENTICATION` | `--enforce-mfa` (default flipped: `true` → `false`; MFA is now optional unless you set this) | | `DISABLE_SIGN_UP` | `--enable-signup` (inverted) | -| TOTP / OTP | `--enable-totp-login`, `--enable-email-otp`, `--enable-sms-otp` | +| TOTP / OTP / WebAuthn | **Breaking**: `--enable-mfa`, `--enable-totp-login`, `--enable-email-otp`, and `--enable-sms-otp` are removed. TOTP, email OTP, SMS OTP, and WebAuthn-as-MFA are now **on by default**; opt out with `--disable-totp-login`, `--disable-email-otp`, `--disable-sms-otp`, `--disable-webauthn-mfa`, or turn everything off at once with `--disable-mfa`. See [MFA & WebAuthn/passkeys flags](../core/server-config#multi-factor-authentication-mfa--webauthnpasskeys). | | Mobile basic auth | `--enable-mobile-basic-authentication` | | Phone verification | `--enable-phone-verification` | diff --git a/docs/sdks/authorizer-go/admin.md b/docs/sdks/authorizer-go/admin.md index e31788a..573fdd0 100644 --- a/docs/sdks/authorizer-go/admin.md +++ b/docs/sdks/authorizer-go/admin.md @@ -18,8 +18,10 @@ default** and is 100% backward compatible — existing code keeps working unchan | `ProtocolREST` | Typed `POST/GET /v1/...` routes | Same flat responses as GraphQL. | | `ProtocolGRPC` | Generated gRPC stub | Uses a **separate endpoint** (default `:9091`). | -Pass `WithProtocol` to `NewAuthorizerClient`. As of 2.3.0-rc.9 all 20 public methods work -over every protocol, and all three return **identical flat response shapes**. +Pass `WithProtocol` to `NewAuthorizerClient`. Most public methods work over every protocol, +and all three return **identical flat response shapes**. Exceptions: the six `Webauthn*` +methods and `TotpMfaSetup` are **GraphQL only** (the server has no REST/gRPC RPC for them); +`GetToken` and `RevokeToken` always use REST (see the OAuth note below). ```go // REST (default endpoint, no extra config) @@ -45,7 +47,7 @@ if err != nil { panic(err) } -res, err := client.Login(&authorizer.LoginInput{Email: "user@example.com", Password: "Abc@123"}) +res, err := client.Login(&authorizer.LoginInput{Email: authorizer.NewStringRef("user@example.com"), Password: "Abc@123"}) ``` > OAuth endpoints (`/oauth/token`, `/oauth/revoke`) always use REST regardless of the @@ -75,8 +77,14 @@ for _, u := range res.Users { } ``` -Request/response types come from the generated proto package -(`authorizerv1 "github.com/authorizerdev/authorizer-go/gen/..."`); import it alongside the SDK. +Request/response types for the proto-backed methods come from the generated package +`authorizerv1 "github.com/authorizerdev/authorizer-go/internal/genpb/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 +instead. ### Admin client options @@ -97,13 +105,16 @@ admin, err := authorizer.NewAuthorizerAdminClient( ### Admin methods Each method declares which protocols support it. Calling a method on an unsupported -protocol raises a clear error early (e.g. _"AdminMeta is not available over graphql"_) -rather than emitting a 404. The protocol columns below also apply to the Python and JS -admin clients (JS supports graphql + rest only). +protocol raises a clear error early (e.g. _"AdminMeta not available over graphql; use grpc +or rest"_) rather than emitting a 404. The protocol columns below also apply to the Python +and JS admin clients (JS supports graphql + rest only). > **⚠ Destructive:** `DeleteUser`, `DeleteWebhook`, `DeleteEmailTemplate`, -> `FgaWriteModel` (overwrites the model), `FgaDeleteTuples`, and `FgaReset` (wipes all FGA -> data) permanently change or remove data. +> `FgaWriteModel` (overwrites the model), `FgaDeleteTuples`, `FgaReset` (wipes all FGA +> data), `DeleteClient`, `DeleteTrustedIssuer`, `DeleteSamlServiceProvider`, +> `RetireSamlIdpKey`, `DeleteOrganization`, `DeleteOrgOIDCConnection`, +> `DeleteOrgSAMLConnection`, `DeleteScimEndpoint`, and `DeleteOrgDomain` +> permanently change or remove data. #### Auth, session & meta @@ -167,6 +178,120 @@ admin clients (JS supports graphql + rest only). | `FgaExpand` | Expand a relation into its userset. | ✓ | ✓ | ✓ | | `FgaReset` | **Reset all FGA data.** | ✓ | ✓ | | +#### OAuth clients (machine-to-machine / workload identity) + +| Method | Description | grpc | rest | gql | +| -------------------- | --------------------------------------------------------------------------------------- | :--: | :--: | :-: | +| `CreateClient` | Provision a new machine/workload identity (service account). `client_secret` is returned **once**. | ✓ | ✓ | ✓ | +| `UpdateClient` | Update a client's metadata, scopes or active flag. | ✓ | ✓ | ✓ | +| `DeleteClient` | **Delete a client.** Tokens already issued to it stop being honoured. | ✓ | ✓ | ✓ | +| `RotateClientSecret` | Issue a new secret for a client. Returned once; the old secret keeps validating during the grace window. | ✓ | ✓ | ✓ | +| `GetClient` | Get a single client by id (`client_secret` never returned). | ✓ | ✓ | ✓ | +| `Clients` | List clients (paginated). | ✓ | ✓ | ✓ | + +#### Trusted issuers + +| Method | Description | grpc | rest | gql | +| ---------------------- | ------------------------------------------------------------------------------------------------------ | :--: | :--: | :-: | +| `AddTrustedIssuer` | Register an external token issuer (K8s service account, SPIFFE, OIDC) allowed to authenticate as a service account via JWT-bearer assertions. | ✓ | ✓ | ✓ | +| `UpdateTrustedIssuer` | Update a trusted issuer. | ✓ | ✓ | ✓ | +| `DeleteTrustedIssuer` | **Delete a trusted issuer.** Assertions from it stop authenticating immediately. | ✓ | ✓ | ✓ | +| `GetTrustedIssuer` | Get a single trusted issuer by id. | ✓ | ✓ | ✓ | +| `TrustedIssuers` | List trusted issuers (paginated), optionally filtered by `service_account_id`. | ✓ | ✓ | ✓ | + +#### SAML (service providers & IdP keys) + +| Method | Description | grpc | rest | gql | +| ---------------------------- | -------------------------------------------------------------------------------------------------| :--: | :--: | :-: | +| `CreateSamlServiceProvider` | Register a downstream SAML 2.0 SP that Authorizer (acting as IdP) issues signed assertions to. | ✓ | ✓ | ✓ | +| `UpdateSamlServiceProvider` | Update a downstream SP's name, endpoints, certificate, attribute mapping, or active state. | ✓ | ✓ | ✓ | +| `DeleteSamlServiceProvider` | **Delete a downstream SP.** SSO assertions to it stop being issued immediately. | ✓ | ✓ | ✓ | +| `GetSamlServiceProvider` | Get a single downstream SP by id. | ✓ | ✓ | ✓ | +| `ListSamlServiceProviders` | List downstream SPs for an org (paginated). | ✓ | ✓ | ✓ | +| `RotateSamlIdpCert` | Generate a new current signing keypair for an org's SAML IdP, demoting the previous current key. | ✓ | ✓ | ✓ | +| `RetireSamlIdpKey` | **Retire a published-but-not-signing SAML IdP key.** It stops being published in IdP metadata. | ✓ | ✓ | ✓ | +| `ListSamlIdpKeys` | List all SAML IdP signing keys for an org. | ✓ | ✓ | ✓ | +| `ImportSamlSpMetadata` | Parse pasted SP metadata XML into fields to prefill a create call. Does not create a record or fetch remotely. | ✓ | ✓ | ✓ | + +#### Organizations & members + +Organizations, members, SSO connections, SCIM endpoints and verified domains have no +proto/gRPC or REST RPCs — they are **GraphQL only**. Types are declared directly in the +Go SDK (not proto-generated): `Organization`, `OrgMember`, `CreateOrganizationRequest`, +`ListOrganizationsRequest`, etc. + +| Method | Description | grpc | rest | gql | +| --------------------- | ----------------------------------------------------| :--: | :--: | :-: | +| `CreateOrganization` | Create an organization. `Name` must be a unique, URL-safe slug. | | | ✓ | +| `UpdateOrganization` | Update an organization. | | | ✓ | +| `DeleteOrganization` | **Delete an organization** and its memberships/connections. | | | ✓ | +| `GetOrganization` | Get a single organization by id. | | | ✓ | +| `Organizations` | List organizations (paginated). | | | ✓ | +| `AddOrgMember` | Add a user to an organization. | | | ✓ | +| `RemoveOrgMember` | Remove a user from an organization. | | | ✓ | +| `OrgMembers` | List an organization's members (paginated). | | | ✓ | +| `UserOrganizations` | List a user's organizations, with per-org roles (paginated). | | | ✓ | + +`Organizations`, `OrgMembers` and `OrgDomains` (below) take pagination via this SDK's own +`*PaginationRequest` type directly on the request — a single level, matching the proto +shape everywhere else in the SDK: + +```go +res, err := admin.Organizations(&authorizer.ListOrganizationsRequest{ + Pagination: &authorizer.PaginationRequest{Limit: 20, Page: 1}, +}) + +members, err := admin.OrgMembers(&authorizer.ListOrgMembersRequest{ + OrgID: "org_123", + Pagination: &authorizer.PaginationRequest{Limit: 20, Page: 1}, +}) +``` + +#### Org SSO connections (OIDC & SAML) + +Upstream SSO an organization's members sign in through. **GraphQL only.** + +| Method | Description | grpc | rest | gql | +| ------------------------------ | ------------------------------------------------------------------------| :--: | :--: | :-: | +| `CreateOrgOIDCConnection` | Create an org's upstream OIDC SSO connection. | | | ✓ | +| `UpdateOrgOIDCConnection` | Update it. Supplying `ClientSecret` rotates it; omitting leaves it intact. | | | ✓ | +| `DeleteOrgOIDCConnection` | **Delete it.** SSO logins through it stop working immediately. | | | ✓ | +| `GetOrgOIDCConnection` | Get it by id or by org id (supply exactly one). | | | ✓ | +| `CreateOrgSAMLConnection` | Create an org's upstream SAML SSO connection. | | | ✓ | +| `UpdateOrgSAMLConnection` | Update it. Supplying `IdpCertificate` replaces it; omitting leaves it intact. | | | ✓ | +| `DeleteOrgSAMLConnection` | **Delete it.** SSO logins through it stop working immediately. | | | ✓ | +| `GetOrgSAMLConnection` | Get it by id or by org id (supply exactly one). | | | ✓ | + +#### SCIM provisioning + +**GraphQL only.** + +| Method | Description | grpc | rest | gql | +| -------------------- | --------------------------------------------------------------------------------| :--: | :--: | :-: | +| `CreateScimEndpoint` | Provision a SCIM endpoint for an organization. The bearer token is returned **once**. | | | ✓ | +| `RotateScimToken` | Rotate the SCIM endpoint's bearer token. New token returned once; old one stops validating. | | | ✓ | +| `DeleteScimEndpoint` | **Delete an organization's SCIM endpoint.** The IdP's provisioning token stops working immediately. | | | ✓ | +| `GetScimEndpoint` | Get an organization's SCIM endpoint (the bearer token is never returned). | | | ✓ | + +#### Org domains (home realm discovery) + +Verified DNS-domain-to-organization mappings used for home realm discovery. **GraphQL only.** + +| Method | Description | grpc | rest | gql | +| ------------------------ | --------------------------------------------------------------------------------------------------| :--: | :--: | :-: | +| `RequestOrgDomain` | Start domain verification, returning the DNS TXT record the tenant must publish to prove control. | | | ✓ | +| `VerifyOrgDomain` | Check the DNS TXT challenge and, if satisfied, verify the domain. | | | ✓ | +| `AddVerifiedOrgDomain` | Directly register a verified domain, bypassing the DNS TXT challenge. Super-admin only. | | | ✓ | +| `DeleteOrgDomain` | **Remove a verified domain.** Logins relying on it for home realm discovery stop resolving to the org. | | | ✓ | +| `OrgDomains` | List an organization's verified domains (paginated). | | | ✓ | + +```go +domains, err := admin.OrgDomains(&authorizer.ListOrgDomainsRequest{ + OrgID: "org_123", + Pagination: &authorizer.PaginationRequest{Limit: 20, Page: 1}, +}) +``` + #### GraphQL-only extras These have no proto / REST / gRPC equivalent and work **over GraphQL only**: diff --git a/docs/sdks/authorizer-go/functions.md b/docs/sdks/authorizer-go/functions.md new file mode 100644 index 0000000..46da233 --- /dev/null +++ b/docs/sdks/authorizer-go/functions.md @@ -0,0 +1,436 @@ +--- +sidebar_position: 2 +title: Functions +--- + +# Functions + +The `AuthorizerClient` provides methods to interact with the Authorizer API. Every method takes a request struct as a parameter and returns a response struct or an error. + +```go +import "github.com/authorizerdev/authorizer-go" + +client, err := authorizer.NewAuthorizerClient( + "YOUR_CLIENT_ID", + "https://your-instance.authorizer.dev", + "https://your-app.example.com", + nil, +) +if err != nil { + panic(err) +} + +// Example: Login +res, err := client.Login(&authorizer.LoginRequest{ + Email: stringPtr("user@example.com"), + Password: "Abc@123", +}) +if err != nil { + panic(err) +} +``` + +## Authentication & user management + +### Public methods (no authentication required) + +| Method | Signature | Returns | +| --------------------- | --------------------------------------------------------------- | --------------------------- | +| `Login` | `Login(req *LoginRequest) (*AuthTokenResponse, error)` | `*AuthTokenResponse` | +| `SignUp` | `SignUp(req *SignUpRequest) (*AuthTokenResponse, error)` | `*AuthTokenResponse` | +| `MagicLinkLogin` | `MagicLinkLogin(req *MagicLinkLoginRequest) (*Response, error)` | `*Response` | +| `VerifyOTP` | `VerifyOTP(req *VerifyOTPRequest) (*AuthTokenResponse, error)` | `*AuthTokenResponse` | +| `VerifyEmail` | `VerifyEmail(req *VerifyEmailRequest) (*AuthTokenResponse, error)` | `*AuthTokenResponse` | +| `ResendOTP` | `ResendOTP(req *ResendOTPRequest) (*Response, error)` | `*Response` | +| `ResendVerifyEmail` | `ResendVerifyEmail(req *ResendVerifyEmailRequest) (*Response, error)` | `*Response` | +| `ForgotPassword` | `ForgotPassword(req *ForgotPasswordRequest) (*ForgotPasswordResponse, error)` | `*ForgotPasswordResponse` | +| `ResetPassword` | `ResetPassword(req *ResetPasswordRequest) (*Response, error)` | `*Response` | +| `ValidateJWTToken` | `ValidateJWTToken(req *ValidateJWTTokenRequest) (*ValidateJWTTokenResponse, error)` | `*ValidateJWTTokenResponse` | +| `ValidateSession` | `ValidateSession(req *ValidateSessionRequest) (*ValidateSessionResponse, error)` | `*ValidateSessionResponse` | +| `GetMetaData` | `GetMetaData() (*MetaData, error)` | `*MetaData` | + +### Authenticated (pass `headers` map with bearer token) + +| Method | Signature | Returns | +| --------------------- | ------------------------------------------------------------------------------- | ----------------- | +| `GetSession` | `GetSession(headers map[string]string) (*AuthTokenResponse, error)` | `*AuthTokenResponse` | +| `GetProfile` | `GetProfile(headers map[string]string) (*User, error)` | `*User` | +| `UpdateProfile` | `UpdateProfile(req *UpdateProfileRequest, headers map[string]string) (*Response, error)` | `*Response` | +| `Logout` | `Logout(headers map[string]string) (*Response, error)` | `*Response` | +| `DeactivateAccount` | `DeactivateAccount(headers map[string]string) (*Response, error)` | `*Response` | + +### Fine-grained authorization + +| Method | Signature | Returns | +| -------------------- | --------------------------------------------------------------------------- | --------------------------- | +| `CheckPermissions` | `CheckPermissions(req *CheckPermissionsRequest, headers map[string]string) (*CheckPermissionsResponse, error)` | `*CheckPermissionsResponse` | +| `ListPermissions` | `ListPermissions(req *ListPermissionsRequest, headers map[string]string) (*ListPermissionsResponse, error)` | `*ListPermissionsResponse` | + +See the dedicated [Fine-Grained Authorization](/sdks/authorizer-go/admin#fga-admin) documentation for details. + +### MFA setup & recovery + +All take a `headers` map with a bearer token (or, if the caller doesn't have one yet, an `email`/`phone_number` pair that resolves the in-progress MFA session cookie instead). + +| Method | Signature | Returns | +| ----------------------- | ---------------------------------------------------------------------- | ------------------- | +| `SkipMfaSetup` | `SkipMfaSetup(req *SkipMfaSetupRequest, headers map[string]string) (*AuthTokenResponse, error)` | `*AuthTokenResponse` | +| `LockMfa` | `LockMfa(req *LockMfaRequest, headers map[string]string) (*Response, error)` | `*Response` | +| `EmailOtpMfaSetup` | `EmailOtpMfaSetup(req *EmailOtpMfaSetupRequest, headers map[string]string) (*Response, error)` | `*Response` | +| `SmsOtpMfaSetup` | `SmsOtpMfaSetup(req *SmsOtpMfaSetupRequest, headers map[string]string) (*Response, error)` | `*Response` | +| `TotpMfaSetup` | `TotpMfaSetup(req *TotpMfaSetupRequest, headers map[string]string) (*AuthTokenResponse, error)` | `*AuthTokenResponse` | + +> `LockMfa` has no OTP fallback — it locks the account and requires admin recovery afterward. + +### WebAuthn / passkeys + +| Method | Signature | Returns | +| ---------------------------------- | ----------- | ---------------------------------------- | +| `WebauthnRegistrationOptions` | `WebauthnRegistrationOptions(req *WebauthnRegistrationOptionsRequest, headers map[string]string) (*WebauthnRegistrationOptionsResponse, error)` | `*WebauthnRegistrationOptionsResponse` | +| `WebauthnRegistrationVerify` | `WebauthnRegistrationVerify(req *WebauthnRegistrationVerifyRequest, headers map[string]string) (*AuthTokenResponse, error)` | `*AuthTokenResponse` | +| `WebauthnLoginOptions` | `WebauthnLoginOptions(email *string) (*WebauthnLoginOptionsResponse, error)` | `*WebauthnLoginOptionsResponse` | +| `WebauthnLoginVerify` | `WebauthnLoginVerify(req *WebauthnLoginVerifyRequest) (*AuthTokenResponse, error)` | `*AuthTokenResponse` | +| `WebauthnDeleteCredential` | `WebauthnDeleteCredential(id string, headers map[string]string) (*Response, error)` | `*Response` | +| `WebauthnCredentials` | `WebauthnCredentials(headers map[string]string) ([]*WebauthnCredentialInfo, error)` | `[]*WebauthnCredentialInfo` | + +`WebauthnRegistrationOptions`/`WebauthnLoginOptions` return JSON-encoded `options` strings to feed straight to the browser's `navigator.credentials.create()` / `.get()`; `WebauthnRegistrationVerify`/`WebauthnLoginVerify` take the JSON-encoded credential response back. `WebauthnDeleteCredential` permanently deletes a registered passkey. `WebauthnCredentials` lists the authenticated caller's own registered passkeys. + +### OAuth (REST) + +| Method | Signature | Returns | +| -------------- | ------------------------------------------ | ------------------ | +| `GetToken` | `GetToken(req *GetTokenRequest) (*TokenResponse, error)` | `*TokenResponse` | +| `RevokeToken` | `RevokeToken(req *RevokeTokenRequest) (*Response, error)` | `*Response` | +| `Revoke` | `Revoke(req *RevokeRequest) (*Response, error)` | `*Response` | + +`GetToken` posts a form-encoded (`application/x-www-form-urlencoded`) request to `/oauth/token` and supports four grants: `authorization_code` (default, needs `code` + `code_verifier`), `refresh_token`, `client_credentials` (RFC 6749 §4.4), and RFC 8693 token exchange. The `client_credentials` and token-exchange grants are **machine / agent-to-agent flows — server-side only**: never send `client_secret`, `client_assertion`, or a `subject_token`/`actor_token` to untrusted or browser code. + +#### Machine-to-machine (client_credentials) + +Get a token for a service account / machine identity created via the [admin `CreateClient`](./admin) method: + +```go +import "github.com/authorizerdev/authorizer-go" + +machine, err := authorizer.NewAuthorizerClient( + "SERVICE_CLIENT_ID", + "https://your-instance.authorizer.dev", + "https://your-app.example.com", + nil, +) +if err != nil { + panic(err) +} + +token, err := machine.GetToken(&authorizer.GetTokenRequest{ + GrantType: stringPtr(authorizer.GrantTypeClientCredentials), + ClientSecret: stringPtr("SERVICE_CLIENT_SECRET"), +}) +if err != nil { + panic(err) +} +fmt.Println(token.AccessToken, token.Scope) +``` + +#### Agent delegation (RFC 8693 token exchange) + +An agent acting on behalf of a signed-in user exchanges the user's token plus its own machine token for a delegated token. The original user stays the JWT `sub`; each hop narrows `scope` and appends to the nested `act` claim (re-widening scope is rejected): + +```go +import "github.com/authorizerdev/authorizer-go" + +agent, err := authorizer.NewAuthorizerClient( + "AGENT_CLIENT_ID", + "https://your-instance.authorizer.dev", + "https://your-app.example.com", + nil, +) +if err != nil { + panic(err) +} + +// 1. the agent authenticates as itself +machineToken, err := agent.GetToken(&authorizer.GetTokenRequest{ + GrantType: stringPtr(authorizer.GrantTypeClientCredentials), + ClientSecret: stringPtr("AGENT_CLIENT_SECRET"), +}) +if err != nil { + panic(err) +} + +// 2. exchange the user's token for one delegated to this agent, scoped down +delegated, err := agent.GetToken(&authorizer.GetTokenRequest{ + GrantType: stringPtr(authorizer.GrantTypeTokenExchange), + ClientSecret: stringPtr("AGENT_CLIENT_SECRET"), + SubjectToken: &userAccessToken, + SubjectTokenType: stringPtr("urn:ietf:params:oauth:token-type:access_token"), + ActorToken: machineToken.AccessToken, + ActorTokenType: stringPtr("urn:ietf:params:oauth:token-type:access_token"), + Scope: stringPtr("crm:read"), + Resource: stringPtr("https://crm.internal.example"), +}) +if err != nil { + panic(err) +} +fmt.Println(delegated.AccessToken) // sub is still the user; act.sub is the agent +``` + +### Escape hatch — raw GraphQL + +For any operation not covered by a typed helper: + +```go +res, err := client.ExecuteGraphQL(&authorizer.GraphQLRequest{ + Query: `query { meta { version } }`, + Variables: nil, +}) +if err != nil { + panic(err) +} +``` + +`ExecuteGraphQL(req *GraphQLRequest) (map[string]interface{}, error)` returns the parsed response data. + +## Examples + +### Sign up + +```go +import "github.com/authorizerdev/authorizer-go" + +client, err := authorizer.NewAuthorizerClient( + "YOUR_CLIENT_ID", + "https://your-instance.authorizer.dev", + "https://your-app.example.com", + nil, +) +if err != nil { + panic(err) +} + +token, err := client.SignUp(&authorizer.SignUpRequest{ + Email: stringPtr("user@example.com"), + Password: "Abc@123", + ConfirmPassword: "Abc@123", + GivenName: stringPtr("Ada"), + FamilyName: stringPtr("Lovelace"), +}) +if err != nil { + panic(err) +} +fmt.Println(token.Message, *token.AccessToken) +``` + +### Log in and read the profile + +```go +import "github.com/authorizerdev/authorizer-go" + +client, err := authorizer.NewAuthorizerClient( + "YOUR_CLIENT_ID", + "https://your-instance.authorizer.dev", + "https://your-app.example.com", + nil, +) +if err != nil { + panic(err) +} + +token, err := client.Login(&authorizer.LoginRequest{ + Email: stringPtr("user@example.com"), + Password: "Abc@123", +}) +if err != nil { + panic(err) +} + +headers := map[string]string{"Authorization": "Bearer " + *token.AccessToken} + +user, err := client.GetProfile(headers) +if err != nil { + panic(err) +} +fmt.Println(user.ID, user.Email, user.Roles) +``` + +### Validate a JWT + +```go +import "github.com/authorizerdev/authorizer-go" + +client, err := authorizer.NewAuthorizerClient( + "YOUR_CLIENT_ID", + "https://your-instance.authorizer.dev", + "https://your-app.example.com", + nil, +) +if err != nil { + panic(err) +} + +res, err := client.ValidateJWTToken(&authorizer.ValidateJWTTokenRequest{ + Token: accessToken, +}) +if err != nil { + panic(err) +} +fmt.Println(res.IsValid, res.Claims) +``` + +### Magic-link login + +```go +import "github.com/authorizerdev/authorizer-go" + +client, err := authorizer.NewAuthorizerClient( + "YOUR_CLIENT_ID", + "https://your-instance.authorizer.dev", + "https://your-app.example.com", + nil, +) +if err != nil { + panic(err) +} + +res, err := client.MagicLinkLogin(&authorizer.MagicLinkLoginRequest{ + Email: stringPtr("user@example.com"), +}) +if err != nil { + panic(err) +} +fmt.Println(res.Message) // "Please check your inbox!..." +``` + +## Request types + +All request structs are serializable to JSON via `json.Marshal()`. Fields shown as `*Type` are optional (pointers). + +| Type | Key fields | +| -------------------------- | ------------------------------------------------------------------------------------------------------------------------- | +| `LoginRequest` | `Password`, `Email`, `PhoneNumber`, `Roles`, `Scope`, `State` | +| `SignUpRequest` | `Password`, `ConfirmPassword`, `Email`, `GivenName`, `FamilyName`, `PhoneNumber`, `Roles`, `Scope`, `RedirectURI`, `AppData`, … | +| `MagicLinkLoginRequest` | `Email`, `Roles`, `Scope`, `State`, `RedirectURI` | +| `VerifyOTPRequest` | `OTP`, `Email`, `PhoneNumber`, `IsTotp`, `State` | +| `VerifyEmailRequest` | `Token`, `State` | +| `ResendOTPRequest` | `Email`, `PhoneNumber`, `State` | +| `ResendVerifyEmailRequest` | `Email` | +| `ForgotPasswordRequest` | `Email`, `PhoneNumber`, `State`, `RedirectURI` | +| `ResetPasswordRequest` | `Password`, `ConfirmPassword`, `Token`, `OTP`, `PhoneNumber` | +| `ValidateJWTTokenRequest` | `Token`, (no explicit type field; server infers from token structure) | +| `ValidateSessionRequest` | `Cookie`, `Roles` | +| `UpdateProfileRequest` | `Email`, `OldPassword`, `NewPassword`, `ConfirmNewPassword`, `GivenName`, `FamilyName`, `Roles`, `AppData`, … | +| `GetTokenRequest` | `Code`, `GrantType`, `RefreshToken`, `CodeVerifier`, `ClientSecret`, `Scope`, `ClientAssertion`, `ClientAssertionType`, `SubjectToken`, `SubjectTokenType`, `ActorToken`, `ActorTokenType`, `Resource` | +| `RevokeTokenRequest` | `RefreshToken` | +| `RevokeRequest` | `RefreshToken` | +| `CheckPermissionsRequest` | `Checks`, `User` | +| `ListPermissionsRequest` | `Relation`, `ObjectType`, `User` | +| `PermissionCheckInput` | `Relation`, `Object`, `ContextualTuples` | +| `FgaTupleInput` | `User`, `Relation`, `Object` | +| `SkipMfaSetupRequest` | `Email`, `PhoneNumber`, `State` | +| `LockMfaRequest` | `Email`, `PhoneNumber` | +| `EmailOtpMfaSetupRequest` | `Email`, `PhoneNumber` | +| `SmsOtpMfaSetupRequest` | `Email`, `PhoneNumber` | +| `TotpMfaSetupRequest` | `Email`, `PhoneNumber` | +| `WebauthnRegistrationOptionsRequest` | `Email`, `PhoneNumber` | +| `WebauthnRegistrationVerifyRequest` | `Credential`, `Name`, `Email`, `PhoneNumber`, `State` | +| `WebauthnLoginVerifyRequest` | `Credential`, `State` | +| `WebauthnDeleteCredentialRequest` | `ID` | + +## Response types + +All response structs are deserializable from JSON via `json.Unmarshal()`. + +| Type | Key fields | +| -------------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| `AuthTokenResponse` | `Message`, `AccessToken`, `ExpiresIn`, `IdToken`, `RefreshToken`, `ShouldShowEmailOtpScreen`, `ShouldShowMobileOtpScreen`, `ShouldShowTotpScreen`, `ShouldOfferWebauthnMfaSetup`, `ShouldOfferWebauthnMfaVerify`, `ShouldOfferEmailOtpMfaSetup`, `ShouldOfferSmsOtpMfaSetup`, `AuthenticatorScannerImage`, `AuthenticatorSecret`, `AuthenticatorRecoveryCodes`, `User` | +| `User` | `ID`, `Email`, `EmailVerified`, `GivenName`, `FamilyName`, `PhoneNumber`, `Roles`, `CreatedAt`, `UpdatedAt`, `IsMultiFactorAuthEnabled`, `HasSkippedMfaSetupAt`, `MfaLockedAt`, `EnrolledMfaMethods`, `AppData`, … | +| `Response` | `Message` | +| `ForgotPasswordResponse` | `Message`, `ShouldShowMobileOtpScreen` | +| `ValidateJWTTokenResponse` | `IsValid`, `Claims` | +| `ValidateSessionResponse` | `IsValid`, `User` | +| `MetaData` | `Version`, `ClientID`, and `Is*Enabled` feature flags (login providers, MFA, sign-up, etc.) | +| `TokenResponse` | `AccessToken`, `ExpiresIn`, `IdToken`, `RefreshToken`, `TokenType`, `Scope`, `IssuedTokenType` | +| `CheckPermissionsResponse` | `Results` | +| `PermissionCheckResult` | `Relation`, `Object`, `Allowed` | +| `ListPermissionsResponse` | `Objects`, `Permissions`, `Truncated` | +| `Permission` | `Object`, `Relation` | +| `WebauthnRegistrationOptionsResponse` | `Options` (JSON-encoded `PublicKeyCredentialCreationOptions`) | +| `WebauthnLoginOptionsResponse` | `Options` (JSON-encoded `PublicKeyCredentialRequestOptions`) | +| `WebauthnCredentialInfo` | `ID`, `Name`, `Transports`, `CreatedAt`, `UpdatedAt`, `LastUsedAt` | + +## Constants + +| Constant | Value | +| ---------------------------------- | --------------------------------------------------------------- | +| `GrantTypeAuthorizationCode` | `"authorization_code"` | +| `GrantTypeRefreshToken` | `"refresh_token"` | +| `GrantTypeClientCredentials` | `"client_credentials"` | +| `GrantTypeTokenExchange` | `"urn:ietf:params:oauth:grant-type:token-exchange"` (RFC 8693) | + +## Error handling + +The SDK returns standard Go errors. Most API errors come back as error messages: + +```go +import "github.com/authorizerdev/authorizer-go" + +client, err := authorizer.NewAuthorizerClient( + "YOUR_CLIENT_ID", + "https://your-instance.authorizer.dev", + "https://your-app.example.com", + nil, +) +if err != nil { + panic(err) +} + +token, err := client.Login(&authorizer.LoginRequest{ + Email: stringPtr("user@example.com"), + Password: "wrong", +}) +if err != nil { + // err will be non-nil + fmt.Println(err) +} +``` + +## Protocol selection + +By default, the client uses GraphQL. You can override this with `WithProtocol`: + +```go +import "github.com/authorizerdev/authorizer-go" + +// Use REST endpoints +client, err := authorizer.NewAuthorizerClient( + "YOUR_CLIENT_ID", + "https://your-instance.authorizer.dev", + "https://your-app.example.com", + nil, + authorizer.WithProtocol(authorizer.ProtocolREST), +) + +// Use gRPC (requires a separate gRPC endpoint, default 9091) +client, err = authorizer.NewAuthorizerClient( + "YOUR_CLIENT_ID", + "https://your-instance.authorizer.dev", + "https://your-app.example.com", + nil, + authorizer.WithProtocol(authorizer.ProtocolGRPC), + authorizer.WithGRPCEndpoint("your-instance.authorizer.dev:9091"), +) +``` + +## Utility helpers + +The SDK exports pointer constructor helpers for optional fields: + +```go +import "github.com/authorizerdev/authorizer-go" + +// stringPtr(s) returns a *string pointing to s +// intPtr(i) returns an *int64 pointing to i +// boolPtr(b) returns a *bool pointing to b + +email := authorizer.stringPtr("user@example.com") +``` diff --git a/docs/sdks/authorizer-go/index.md b/docs/sdks/authorizer-go/index.md index 692cbe4..1bebf9a 100644 --- a/docs/sdks/authorizer-go/index.md +++ b/docs/sdks/authorizer-go/index.md @@ -54,7 +54,7 @@ if err != nil { ```go response, err := authorizerClient.Login(&authorizer.LoginInput{ - Email: "test@yopmail.com", + Email: authorizer.NewStringRef("test@yopmail.com"), Password: "Abc@123", }) if err != nil { @@ -62,6 +62,8 @@ if err != nil { } ``` +> `Email` (and `PhoneNumber`) are `*string` fields, so pass a pointer: use the SDK's `authorizer.NewStringRef(v string) *string` helper, or take the address of a local variable. + **Example: Validate JWT Token** ```go @@ -149,7 +151,7 @@ all, err := authorizerClient.ListPermissions(&authorizer.ListPermissionsRequest{ The SDK provides the following methods: - `Login` -- Authenticate with email and password -- `Signup` -- Register a new user +- `SignUp` -- Register a new user - `VerifyEmail` -- Verify user email - `ForgotPassword` -- Initiate forgot password flow - `ResetPassword` -- Reset password with token @@ -158,8 +160,27 @@ The SDK provides the following methods: - `MagicLinkLogin` -- Login with magic link - `ValidateJWTToken` -- Validate a JWT token - `GetSession` -- Get current session -- `RevokeToken` -- Revoke a token +- `RevokeToken` -- Revoke a token (`/oauth/revoke`) +- `Revoke` -- Revoke a token over the client's selected protocol (graphql/rest/grpc) - `Logout` -- Logout user - `ValidateSession` -- Validate a session - `CheckPermissions` -- Evaluate one or more permission checks (FGA) - `ListPermissions` -- List objects the subject holds a permission on (FGA) +- `GetMetaData` -- Get server metadata / enabled feature flags +- `GetToken` -- Exchange a grant (`authorization_code`, `refresh_token`, `client_credentials`, or RFC 8693 `token-exchange`) for a token at `/oauth/token` -- covers M2M and workload-identity flows +- `ExecuteGraphQL` -- Run a raw GraphQL request against the instance +- `ResendOTP` -- Resend a one-time code +- `ResendVerifyEmail` -- Resend the email verification link +- `VerifyOTP` -- Verify a one-time code and complete login +- `LockMfa` -- Lock the account into required MFA setup when no verified fallback is enrolled +- `SkipMfaSetup` -- Skip optional MFA setup and issue the withheld access token +- `EmailOtpMfaSetup` -- Start email-OTP MFA enrollment +- `SmsOtpMfaSetup` -- Start SMS-OTP MFA enrollment +- `TotpMfaSetup` -- Start TOTP MFA enrollment (GraphQL only) +- `DeactivateAccount` -- Deactivate the authenticated user's own account +- `WebauthnRegistrationOptions` -- Get a passkey creation challenge +- `WebauthnRegistrationVerify` -- Complete passkey enrollment +- `WebauthnLoginOptions` -- Get a passkey assertion challenge (email optional, for usernameless login) +- `WebauthnLoginVerify` -- Complete passkey login +- `WebauthnCredentials` -- List the caller's own registered passkeys +- `WebauthnDeleteCredential` -- Delete one of the caller's own registered passkeys diff --git a/docs/sdks/authorizer-js/admin.md b/docs/sdks/authorizer-js/admin.md index 5cf0001..6f995b6 100644 --- a/docs/sdks/authorizer-js/admin.md +++ b/docs/sdks/authorizer-js/admin.md @@ -78,8 +78,19 @@ Each method declares which protocols support it. Calling a method on an unsuppor protocol returns a clear error rather than emitting a 404. > **⚠ Destructive:** `deleteUser`, `deleteWebhook`, `deleteEmailTemplate`, -> `fgaWriteModel` (overwrites the model), `fgaDeleteTuples`, and `fgaReset` (wipes all FGA -> data) permanently change or remove data. +> `fgaWriteModel` (overwrites the model), `fgaDeleteTuples`, `fgaReset` (wipes all FGA +> data), `deleteClient`, `rotateClientSecret` (invalidates the old secret immediately), +> `deleteTrustedIssuer`, `deleteOrganization`, `removeOrgMember`, `deleteOrgOIDCConnection`, +> `deleteOrgSAMLConnection`, `deleteSAMLServiceProvider`, `retireSAMLIDPKey`, +> `deleteScimEndpoint`, `rotateScimToken` (invalidates the old token immediately), and +> `deleteOrgDomain` permanently change, remove, or invalidate data. + +> **Pagination shape:** the paginated list methods (`users`, `verificationRequests`, +> `webhooks`, `webhookLogs`, `emailTemplates`, `auditLogs`, `clients`, `trustedIssuers`, +> `organizations`, `orgMembers`, `userOrganizations`, `listSAMLServiceProviders`, +> `orgDomains`, …) take pagination fields (`limit`, `page`) directly on the params object — +> e.g. `admin.webhooks({ limit: 10 })`. There is no `pagination` wrapper. If you have older +> code written as `admin.webhooks({ pagination: { limit: 10 } })`, drop the wrapper. #### Auth, session & meta @@ -143,6 +154,111 @@ protocol returns a clear error rather than emitting a 404. | `fgaExpand` | Expand a relation into its userset. | ✓ | ✓ | | `fgaReset` | **Reset all FGA data.** | ✓ | | +#### OAuth clients (service accounts / M2M) + +Registered OAuth clients used by the `client_credentials` and RFC 8693 token-exchange grants +(see [`getToken`](/sdks/authorizer-js/functions#--gettoken)). `client_secret` is returned +exactly once, at creation and at rotation, and is never projected back afterwards. + +| Method | Description | rest | gql | +| ----------------------- | ---------------------------------------------------------- | :--: | :-: | +| `createClient` | Register a new OAuth client / service account. | ✓ | ✓ | +| `updateClient` | Update a client's name, description, scopes, or active state. | ✓ | ✓ | +| `deleteClient` | **Delete a client.** | ✓ | ✓ | +| `rotateClientSecret` | **Mint a fresh secret; the old one stops working immediately.** | ✓ | ✓ | +| `client` | Get a single client by id (never includes the secret). | ✓ | ✓ | +| `clients` | List clients (paginated). | ✓ | ✓ | + +#### Trusted issuers (secretless client authentication) + +External token issuers trusted to authenticate a service account via RFC 7523 +`client_assertion` (Kubernetes SA tokens, SPIFFE JWT-SVIDs, cloud OIDC tokens) instead of a +shared secret. + +| Method | Description | rest | gql | +| ------------------------- | ------------------------------------------------------------ | :--: | :-: | +| `addTrustedIssuer` | Register a trusted issuer for a service account. | ✓ | ✓ | +| `updateTrustedIssuer` | Update an existing trusted issuer. | ✓ | ✓ | +| `deleteTrustedIssuer` | **Delete a trusted issuer; its tokens stop authenticating immediately.** | ✓ | ✓ | +| `trustedIssuer` | Get a single trusted issuer by id. | ✓ | ✓ | +| `trustedIssuers` | List trusted issuers, optionally filtered by service account (paginated). | ✓ | ✓ | + +#### Organizations + +Tenant grouping of users. Organizations, their SSO connections, SCIM endpoints, and verified +domains have **no REST/proto routes yet** — GraphQL only. + +| Method | Description | rest | gql | +| --------------------- | ------------------------------------------------------------ | :--: | :-: | +| `createOrganization` | Create a new organization. | | ✓ | +| `updateOrganization` | Update an existing organization. | | ✓ | +| `deleteOrganization` | **Delete an organization.** | | ✓ | +| `organization` | Get a single organization by id. | | ✓ | +| `organizations` | List organizations (paginated). | | ✓ | +| `addOrgMember` | Add a user to an organization with optional per-org roles. | | ✓ | +| `removeOrgMember` | **Remove a user from an organization.** | | ✓ | +| `orgMembers` | List an organization's members (paginated). | | ✓ | +| `userOrganizations` | List the organizations a user belongs to, with roles per org (paginated). | | ✓ | + +#### Org SSO connections + +Per-organization upstream identity providers. `createOrgOIDCConnection` brokers Authorizer as +an OIDC Relying Party; `createOrgSAMLConnection` brokers Authorizer as a SAML 2.0 Service +Provider. Upstream secrets/certificates are accepted on write but never projected back. +GraphQL only. + +| Method | Description | rest | gql | +| ------------------------------ | ----------------------------------------------------------------------- | :--: | :-: | +| `createOrgOIDCConnection` | Create a per-org upstream OIDC connection. | | ✓ | +| `updateOrgOIDCConnection` | Update a per-org upstream OIDC connection. | | ✓ | +| `deleteOrgOIDCConnection` | **Delete a per-org upstream OIDC connection; SSO stops immediately.** | | ✓ | +| `orgOIDCConnection` | Get a connection by id OR by org_id (supply exactly one). | | ✓ | +| `createOrgSAMLConnection` | Create a per-org upstream SAML connection. | | ✓ | +| `updateOrgSAMLConnection` | Update a per-org upstream SAML connection. | | ✓ | +| `deleteOrgSAMLConnection` | **Delete a per-org upstream SAML connection; SSO stops immediately.** | | ✓ | +| `orgSAMLConnection` | Get a connection by id OR by org_id (supply exactly one). | | ✓ | + +#### SAML IdP (Authorizer as Identity Provider) + +The inverse of the org SAML connection above: downstream Service Providers that Authorizer +issues signed SAML assertions to, plus the per-org IdP signing keypairs used to sign them. + +| Method | Description | rest | gql | +| ---------------------------------- | -------------------------------------------------------------------------- | :--: | :-: | +| `createSAMLServiceProvider` | Register a downstream SP. | ✓ | ✓ | +| `updateSAMLServiceProvider` | Update a downstream SP's name, endpoints, certificate, mapping, or state. | ✓ | ✓ | +| `deleteSAMLServiceProvider` | **Delete a downstream SP; SSO for that SP stops immediately.** | ✓ | ✓ | +| `samlServiceProvider` | Get a single downstream SP by id. | ✓ | ✓ | +| `listSAMLServiceProviders` | List downstream SPs for an org (paginated). | ✓ | ✓ | +| `rotateSAMLIDPCert` | Generate a new current signing keypair for an org's SAML IdP. | ✓ | ✓ | +| `retireSAMLIDPKey` | **Retire a published-but-superseded key (cannot retire the current key).** | ✓ | ✓ | +| `listSAMLIDPKeys` | List all SAML IdP signing keys for an org (unpaginated). | ✓ | ✓ | +| `importSAMLSPMetadata` | Parse pasted SP metadata XML into entity_id/acs_url/certificate (does not create a record). | ✓ | ✓ | + +#### SCIM endpoints + +Per-org inbound SCIM 2.0 provisioning credential. The bearer token is returned exactly once, +at creation and at rotation, and is never projected back afterwards. GraphQL only. + +| Method | Description | rest | gql | +| ------------------------ | ---------------------------------------------------------------------- | :--: | :-: | +| `createScimEndpoint` | Provision the org's inbound SCIM endpoint. | | ✓ | +| `rotateScimToken` | **Mint a fresh bearer token; the old one stops working immediately.** | | ✓ | +| `deleteScimEndpoint` | **Remove the org's SCIM endpoint; inbound provisioning stops immediately.** | | ✓ | +| `scimEndpoint` | Get the org's SCIM endpoint (never includes the token). | | ✓ | + +#### Org verified domains + +Verified DNS-domain-to-organization mappings used for home-realm discovery. GraphQL only. + +| Method | Description | rest | gql | +| --------------------------| --------------------------------------------------------------------------| :--: | :-: | +| `requestOrgDomain` | Start domain verification, returning the DNS TXT challenge to publish. | | ✓ | +| `verifyOrgDomain` | Check the published DNS challenge and record the verified domain. | | ✓ | +| `addVerifiedOrgDomain` | Record a verified domain without a DNS challenge (super-admin only). | | ✓ | +| `deleteOrgDomain` | **Remove a verified domain; home-realm discovery for it stops immediately.** | | ✓ | +| `orgDomains` | List an organization's verified domains (paginated). | | ✓ | + #### GraphQL-only extras These have no REST equivalent and work **over GraphQL only**: diff --git a/docs/sdks/authorizer-js/functions.md b/docs/sdks/authorizer-js/functions.md index b64498f..07ae954 100644 --- a/docs/sdks/authorizer-js/functions.md +++ b/docs/sdks/authorizer-js/functions.md @@ -12,17 +12,19 @@ title: Functions **Table of Contents** - [authorize](#--authorize) +- [browserLogin](#--browserlogin) - [getToken](#--gettoken) - [login](#--login) - [signup](#--signup) - [verifyEmail](#--verifyemail) +- [resendVerifyEmail](#--resendverifyemail) - [getProfile](#--getprofile) - [updateProfile](#--updateprofile) - [forgotPassword](#--forgotpassword) - [resetPassword](#--resetpassword) - [oauthLogin](#--oauthlogin) - [magicLinkLogin](#--magiclinklogin) -- [getMetadata](#--getmetadata) +- [getMetaData](#--getmetadata) - [getSession](#--getsession) - [revokeToken](#--revoketoken) - [logout](#--logout) @@ -33,6 +35,23 @@ title: Functions - [verifyOtp](#--verifyotp) - [resendOtp](#--resendotp) - [deactivateAccount](#--deactivateaccount) +- [skipMfaSetup](#--skipmfasetup) +- [lockMfa](#--lockmfa) +- [emailOtpMfaSetup](#--emailotpmfasetup) +- [smsOtpMfaSetup](#--smsotpmfasetup) +- [totpMfaSetup](#--totpmfasetup) +- [webauthnRegistrationOptions](#--webauthnregistrationoptions) +- [webauthnRegistrationVerify](#--webauthnregistrationverify) +- [webauthnLoginOptions](#--webauthnloginoptions) +- [webauthnLoginVerify](#--webauthnloginverify) +- [webauthnCredentials](#--webauthncredentials) +- [webauthnDeleteCredential](#--webauthndeletecredential) +- [registerPasskey](#--registerpasskey) +- [loginWithPasskey](#--loginwithpasskey) +- [loginWithPasskeyAutofill](#--loginwithpasskeyautofill) +- [cancelPasskeyAutofill](#--cancelpasskeyautofill) +- [isWebauthnSupported](#--iswebauthnsupported) +- [parseMfaRedirectParams](#--parsemfaredirectparams) These functions can be invoked using the `Authorizer` instance: @@ -80,18 +99,44 @@ const { data, errors } = await authRef.authorize({ }) ``` +## - `browserLogin` + +Function to silently check for an existing browser session (via `getSession`) and return its tokens. If no session exists it falls back to redirecting the browser to the hosted login app (`{authorizerURL}/app`), the same fallback `authorize` uses when the iframe check fails. Browser-only; it takes no parameters. + +**Sample Usage** + +```js +const { data, errors } = await authRef.browserLogin() +if (data?.access_token) { + // an existing session was found +} +``` + ## - `getToken` -Function to get token information based on code / refresh_token +Function to exchange credentials for tokens at `/oauth/token`. This call always goes over REST regardless of the client's configured `protocol` (see [Protocols & Admin API](/sdks/authorizer-js/admin)). + +Supports 4 grant types: `authorization_code` (default), `refresh_token`, `client_credentials` ([RFC 6749 §4.4](https://datatracker.ietf.org/doc/html/rfc6749#section-4.4)), and token exchange ([RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693), `urn:ietf:params:oauth:grant-type:token-exchange`). + +> **Server-side only:** `client_credentials` and token exchange are machine/service flows for trusted server-side code. Never ship `client_secret`, `client_assertion`, or subject/actor tokens in a browser bundle. It accepts JSON object as a parameter with following keys -| Key | Description | Required | -| --------------- | ---------------------------------------------------------------------------------------------------------------------------- | -------- | -| `grant_type` | Supports `authorization_code` & `refresh_token` grant types. Default is `authorization_code` | false | -| `code_verifier` | Code verifier to verify against the code_challenge sent in authorize request. Required if `authorization_code` flow is used. | false | -| `code` | Code returned form authorize request is sent to make sure it is follow up of same request | false | -| `refresh_token` | Refresh token used to get the new access token. Required in case of `refresh_token` grant type | false | +| Key | Description | Required | +| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------- | -------- | +| `grant_type` | `authorization_code`, `refresh_token`, `client_credentials`, or `urn:ietf:params:oauth:grant-type:token-exchange`. Default is `authorization_code` | false | +| `code_verifier` | Code verifier to verify against the code_challenge sent in authorize request. Required if `authorization_code` flow is used (handled automatically by the SDK). | false | +| `code` | Code returned form authorize request is sent to make sure it is follow up of same request | false | +| `refresh_token` | Refresh token used to get the new access token. Required in case of `refresh_token` grant type | false | +| `client_secret` | Service-account secret. Used with `client_credentials`. Server-side only. | false | +| `scope` | Space-delimited OAuth2 scope. Omit for `client_credentials` to get the service account's full allowed scope set. | false | +| `client_assertion` | RFC 7523 JWT-bearer client credential (secretless workload identity: K8s SA tokens, SPIFFE JWT-SVIDs, cloud OIDC tokens). | false | +| `client_assertion_type` | Type of `client_assertion`. Use the exported `CLIENT_ASSERTION_TYPE_JWT_BEARER` constant. | false | +| `subject_token` | The authority being exercised (the user's token). Required for token exchange. | false | +| `subject_token_type` | Type of `subject_token`. Use the exported `TOKEN_TYPE_ACCESS_TOKEN` / `TOKEN_TYPE_JWT` constants. | false | +| `actor_token` | The acting agent's token; its presence selects the delegation profile. Used for token exchange. | false | +| `actor_token_type` | Type of `actor_token`. | false | +| `resource` | RFC 8707 resource indicator the issued token should be audience-bound to. | false | If session exists following keys are returned in the `data` object. @@ -101,8 +146,11 @@ If session exists following keys are returned in the `data` object. | ---------------------- | ------------------------------------------------------------------------------------------------------ | | `access_token` | accessToken that frontend application can use for further authorized requests | | `expires_in` | timestamp when the current token is going to expire, so that frontend can request for new access token | -| `id_token` | JWT token holding the user information | +| `id_token` | JWT token holding the user information. Only issued on user grants (`authorization_code` / `refresh_token`) — absent for `client_credentials` and token exchange | | `refresh_token` | When scope includes `offline_access`, Long living token is returned which can be used to get new access tokens. This is rotated with each request | +| `token_type` | Token type, e.g. `Bearer` | +| `scope` | Granted scope. Returned by `client_credentials` and token exchange grants | +| `issued_token_type` | The token type URN issued. Returned by the token exchange grant ([RFC 8693 §2.2](https://datatracker.ietf.org/doc/html/rfc8693#section-2.2)) | **Sample Usage** @@ -119,6 +167,12 @@ const { data, errors } = await authRef.getToken({ refresh_token: 'your refresh_token from login (should store in memmory such as store, variables)', }) + +// server-side machine-to-machine (client_credentials) — never in a browser bundle +const { data, errors } = await authRef.getToken({ + grant_type: 'client_credentials', + client_secret: 'YOUR_CLIENT_SECRET', +}) ``` ## - `signup` @@ -143,6 +197,8 @@ It accepts JSON object as a parameter with the following keys | `phone_number` | phone number of user | false | | `redirect_uri` | URL where user should be redirected after login | false | | `scope` | List of openID scopes. If not present default scopes ['openid', 'email', 'profile', 'offline_access'] is used | false | +| `state` | Opaque state string round-tripped through the flow. Auto-generated by the SDK if omitted | false | +| `app_data` | Arbitrary JSON object of application-specific data stored on the user | false | Following is the response for the `signup` in the `data` object @@ -159,6 +215,10 @@ Following is the response for the `signup` in the `data` object | `should_show_email_otp_screen` | Is set to true if email based multi factor authentication is enabled | | `should_show_mobile_otp_screen` | Is set to true if mobiled based multi factor authentication is enabled | | `should_show_totp_screen` | Is set to true if totp based multi factor authentication is enabled | +| `should_offer_webauthn_mfa_verify` | Is set to true if the user should be offered to verify with an existing passkey as their second factor | +| `should_offer_webauthn_mfa_setup` | Is set to true if the user should be offered to enroll a passkey as their second factor | +| `should_offer_email_otp_mfa_setup` | Is set to true if the user should be offered email-OTP MFA enrollment | +| `should_offer_sms_otp_mfa_setup` | Is set to true if the user should be offered SMS-OTP MFA enrollment | | `authenticator_scanner_image` | If totp registration is pending it sends base64 encoded image string that can be rendered by totp app scanners like Google Authentication | | `authenticator_secret` | If totp registration is pending, then this secret can be used for registration instead of image on authenticator apps | | `authenticator_recovery_codes` | If totp registration is pending, then recovery codes are sent using which totp can be accessed again | @@ -182,10 +242,14 @@ It accepts JSON object as a parameter with the following keys | Key | Description | Required | | ---------- | ---------------------------------------------------------------------------------------------------------------------- | -------- | -| `email` | Email address of user | true | +| `email` | Email address of user | false | +| `phone_number` | Phone number of user (alternative to `email`) | false | | `password` | Password of user | true | | `roles` | Roles of user that he/she wants to login with. It accepts array of string. Defaults to `[user]` role if not configured | false | | `scope` | List of openID scopes. If not present default scopes ['openid', 'email', 'profile'] is used | false | +| `state` | Opaque state string round-tripped through the flow | false | + +Either `email` or `phone_number` is required. Following is the response for `login` in the `data` object @@ -202,6 +266,10 @@ Following is the response for `login` in the `data` object | `should_show_email_otp_screen` | Is set to true if email based multi factor authentication is enabled | | `should_show_mobile_otp_screen` | Is set to true if mobiled based multi factor authentication is enabled | | `should_show_totp_screen` | Is set to true if totp based multi factor authentication is enabled | +| `should_offer_webauthn_mfa_verify` | Is set to true if the user should be offered to verify with an existing passkey as their second factor | +| `should_offer_webauthn_mfa_setup` | Is set to true if the user should be offered to enroll a passkey as their second factor | +| `should_offer_email_otp_mfa_setup` | Is set to true if the user should be offered email-OTP MFA enrollment | +| `should_offer_sms_otp_mfa_setup` | Is set to true if the user should be offered SMS-OTP MFA enrollment | | `authenticator_scanner_image` | If totp registration is pending it sends base64 encoded image string that can be rendered by totp app scanners like Google Authentication | | `authenticator_secret` | If totp registration is pending, then this secret can be used for registration instead of image on authenticator apps | | `authenticator_recovery_codes` | If totp registration is pending, then recovery codes are sent using which totp can be accessed again | @@ -224,6 +292,7 @@ It accepts JSON object as a parameter with following keys | Key | Description | Required | | ------- | ----------------------------- | -------- | | `token` | Token sent for verifying user | true | +| `state` | Opaque state string round-tripped through the flow | false | This mutation returns `AuthResponse` type with the following keys in the `data` object @@ -232,8 +301,6 @@ This mutation returns `AuthResponse` type with the following keys in the `data` | Key | Description | | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | `message` | Success / Error message from server | -| `should_show_email_otp_screen` | Boolean value for frontend application to show otp input for email based login screen | -| `should_show_mobile_otp_screen` | Boolean value for frontend application to show otp input for mobile based login screen | | `access_token` | accessToken that frontend application can use for further authorized requests | | `expires_in` | timestamp when the current token is going to expire, so that frontend can request for new access token | | `id_token` | JWT token holding the user information | @@ -242,6 +309,10 @@ This mutation returns `AuthResponse` type with the following keys in the `data` | `should_show_email_otp_screen` | Is set to true if email based multi factor authentication is enabled | | `should_show_mobile_otp_screen` | Is set to true if mobiled based multi factor authentication is enabled | | `should_show_totp_screen` | Is set to true if totp based multi factor authentication is enabled | +| `should_offer_webauthn_mfa_verify` | Is set to true if the user should be offered to verify with an existing passkey as their second factor | +| `should_offer_webauthn_mfa_setup` | Is set to true if the user should be offered to enroll a passkey as their second factor | +| `should_offer_email_otp_mfa_setup` | Is set to true if the user should be offered email-OTP MFA enrollment | +| `should_offer_sms_otp_mfa_setup` | Is set to true if the user should be offered SMS-OTP MFA enrollment | | `authenticator_scanner_image` | If totp registration is pending it sends base64 encoded image string that can be rendered by totp app scanners like Google Authentication | | `authenticator_secret` | If totp registration is pending, then this secret can be used for registration instead of image on authenticator apps | | `authenticator_recovery_codes` | If totp registration is pending, then recovery codes are sent using which totp can be accessed again | @@ -254,6 +325,35 @@ const { data, errors } = await authRef.verifyEmail({ }) ``` +## - `resendVerifyEmail` + +Function to resend the verification email to a user who signed up but hasn't verified their email yet. + +It accepts JSON object as a parameter with following keys + +| Key | Description | Required | +| ------------ | --------------------------------------------- | -------- | +| `email` | Email address to resend the verification to | true | +| `identifier` | Verification identifier (`basic_signup`) | true | +| `state` | Opaque state string round-tripped through the flow | false | + +It returns the following keys in response `data` object + +**Response** + +| Key | Description | +| --------- | ------------------------------------ | +| `message` | Success / Error message from server | + +**Sample Usage** + +```js +const { data, errors } = await authRef.resendVerifyEmail({ + email: 'foo@bar.com', + identifier: 'basic_signup', +}) +``` + ## - `getProfile` Function to get profile of user. This function makes an authorized request, hence if it is used from the browser the HTTP cookie is sent if user has logged in else you need to pass headers object. @@ -268,18 +368,31 @@ It returns the following keys in response `data` object **Response** -| Key | Description | -| ---------------- | ------------------------------------------------------------ | -| `id` | user unique identifier | -| `email` | email address of user | -| `given_name` | first name of user | -| `family_name` | last name of user | -| `signup_methods` | methods using which user have signed up, eg: `google,github` | -| `email_verified` | determine if email is verified or not | -| `picture` | profile picture URL | -| `roles` | user roles | -| `created_at` | timestamp at which the user entry was created | -| `updated_at` | timestamp at which the user entry was updated | +| Key | Description | +| -------------------------------- | ------------------------------------------------------------------ | +| `id` | user unique identifier | +| `email` | email address of user | +| `email_verified` | determine if email is verified or not | +| `given_name` | first name of user | +| `family_name` | last name of user | +| `middle_name` | middle name of user | +| `nickname` | nick name of user | +| `preferred_username` | preferred username of user | +| `gender` | gender of user | +| `birthdate` | birthdate of user | +| `phone_number` | phone number of user | +| `phone_number_verified` | determine if phone number is verified or not | +| `picture` | profile picture URL | +| `signup_methods` | methods using which user have signed up, eg: `google,github` | +| `roles` | user roles | +| `created_at` | timestamp at which the user entry was created | +| `updated_at` | timestamp at which the user entry was updated | +| `revoked_timestamp` | timestamp at which access was revoked, if any | +| `is_multi_factor_auth_enabled` | whether the user has multi-factor authentication enabled | +| `has_skipped_mfa_setup_at` | timestamp at which the user skipped MFA setup, if any | +| `mfa_locked_at` | timestamp at which MFA was locked for the user, if any | +| `enrolled_mfa_methods` | list of MFA methods the user has enrolled (e.g. `totp`, `webauthn`) | +| `app_data` | arbitrary JSON object of application-specific data on the user | **Sample Usage** @@ -304,14 +417,24 @@ It accepts 2 JSON object as its parameters. Here are the keys that `data` object accepts -| Key | Description | Required | -| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | -| `given_name` | New first name of the user | false | -| `family_name` | New last name of the user | false | -| `email` | New email of th user. This will logout the user and send the new verification mail to user if `DISABLE_EMAIL_NOTIFICATION` is set to false | false | -| `old_password` | In case if user wants to change password they need to specify the older password here. In this scenario `newPassword` and `confirmNewPassword` will be required. | false | -| `newPassword` | New password that user wants to set. In this scenario `old_password` and `confirmNewPassword` will be required | false | -| `confirmNewPassword` | Value same as the new password to make sure it matches the password entered by user. In this scenario `old_password` and `newPassword` will be required | false | +| Key | Description | Required | +| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | +| `given_name` | New first name of the user | false | +| `family_name` | New last name of the user | false | +| `middle_name` | New middle name of the user | false | +| `nickname` | New nickname of the user | false | +| `gender` | New gender of the user | false | +| `birthdate` | New birthdate of the user | false | +| `phone_number` | New phone number of the user | false | +| `picture` | New profile picture URL of the user | false | +| `email` | New email of th user. This will logout the user and send the new verification mail to user if `DISABLE_EMAIL_NOTIFICATION` is set to false | false | +| `old_password` | In case if user wants to change password they need to specify the older password here. In this scenario `new_password` and `confirm_new_password` will be required. | false | +| `new_password` | New password that user wants to set. In this scenario `old_password` and `confirm_new_password` will be required | false | +| `confirm_new_password` | Value same as the new password to make sure it matches the password entered by user. In this scenario `old_password` and `new_password` will be required | false | +| `is_multi_factor_auth_enabled` | Toggle whether MFA is enabled for the user | false | +| `app_data` | Arbitrary JSON object of application-specific data stored on the user | false | + +> Note: earlier versions of this doc referred to these password fields as `newPassword` / `confirmNewPassword` (camelCase) — the SDK and server both expect the snake_case `new_password` / `confirm_new_password` shown above. Here is sample of `headers` object @@ -353,9 +476,14 @@ It accepts JSON object as parameter with the following keys > Note: You will need a SMTP server with an email address and password configured as [authorizer environment](/core/server-config) using which system can send emails. -| Key | Description | Required | -| ------- | -------------------------------------------- | -------- | -| `email` | Email for which password needs to be changed | true | +| Key | Description | Required | +| -------------- | ------------------------------------------------------------ | -------- | +| `email` | Email for which password needs to be changed | false | +| `phone_number` | Phone number for which password needs to be changed (alternative to `email`) | false | +| `redirect_uri` | URL where user should be redirected after resetting password. Defaults to the client's configured `redirectURL` | false | +| `state` | Opaque state string round-tripped through the flow. Auto-generated by the SDK if omitted | false | + +Either `email` or `phone_number` is required. It returns the following keys in response `data` object @@ -380,9 +508,15 @@ Function to reset password. This is the step 2 of forgot password process. It accepts JSON object as a parameter with following keys -| Key | Description | Required | -| ------- | --------------------------------- | -------- | -| `token` | Token sent to the user in step 1. | true | +| Key | Description | Required | +| -------------------- | --------------------------------------------------------------------- | -------- | +| `token` | Token sent to the user by email in step 1 (`forgotPassword`) | false | +| `otp` | OTP sent to the user by SMS in step 1, if mobile-based reset is used | false | +| `phone_number` | Phone number the OTP was sent to. Required if `otp` is used | false | +| `password` | New password to set | true | +| `confirm_password` | Value same as `password` to make sure it matches | true | + +Either `token` (email flow) or `otp` + `phone_number` (mobile flow) is required, along with `password` and `confirm_password`. It returns the following keys in response `data` object @@ -397,6 +531,8 @@ It returns the following keys in response `data` object ```js const { data, errors } = await authRef.resetPassword({ token: `some_token`, + password: 'newPass123', + confirm_password: 'newPass123', }) ``` @@ -404,17 +540,27 @@ const { data, errors } = await authRef.resetPassword({ Function to login using OAuth Providers. This is mainly used in browser as user is redirect to respective oauth platform. -> Note only enabled oauth providers can be used here. To get the information about enabled oauth provider you can use [`getMetadata`](#--getmetadata) function +> Note only enabled oauth providers can be used here. To get the information about enabled oauth provider you can use [`getMetaData`](#--getmetadata) function -It supports optional argument for `role` based login +It accepts the following positional arguments: + +| Argument | Description | Required | +| ------------------ | ----------------------------------------------------------------------------------------------------- | -------- | +| `oauthProvider` | One of `apple`, `github`, `google`, `facebook`, `linkedin`, `twitter`, `microsoft`, `twitch`, `roblox`, `discord` | true | +| `roles` | Array of role strings to log in with | false | +| `redirect_uri` | URL to redirect to after the OAuth flow completes. Defaults to the client's configured `redirectURL` | false | +| `state` | Opaque state string round-tripped through the flow. Auto-generated by the SDK if omitted | false | **Sample Usage** ```js await authRef.oauthLogin('google') -// login with specific role -await authRef.oauthLogin('google', 'admin') +// login with specific role(s) +await authRef.oauthLogin('google', ['admin']) + +// override the redirect_uri +await authRef.oauthLogin('github', undefined, 'https://your-app.example.com/callback') ``` ## - magicLinkLogin @@ -429,6 +575,7 @@ Function to perform password less login. | `roles` | List of valid valid roles using which user needs to login | false | | `scope` | List of openID scopes. If not present default scopes ['openid', 'email', 'profile'] is used | false | | `redirect_uri` | URL where user should be redirected after login | false | +| `state` | Opaque state string round-tripped through the flow. Auto-generated by the SDK if omitted | false | It returns the following keys in response `data` object @@ -446,7 +593,7 @@ const { data, errors } = await authRef.magicLinkLogin({ }) ``` -## - `getMetadata` +## - `getMetaData` Function to get meta information about your authorizer instance. eg, version, configurations, etc @@ -454,28 +601,46 @@ It returns the following keys in response `data` object **Response** -| Key | Description | -| --------------------------------- | ------------------------------------------------------------- | -| `version` | Authorizer version that is currently deployed | -| `client_id` | Identifier of your instance | -| `is_google_login_enabled` | It gives information if google login is configured or not | -| `is_github_login_enabled` | It gives information if github login is configured or not | -| `is_facebook_login_enabled` | It gives information if facebook login is configured or not | -| `is_email_verification_enabled` | It gives information if email verification is enabled or not | -| `is_basic_authentication_enabled` | It gives information, if basic auth is enabled or not | -| `is_magic_link_login_enabled` | It gives information if password less login is enabled or not | +| Key | Description | +| -------------------------------------------- | ----------------------------------------------------------------------- | +| `version` | Authorizer version that is currently deployed | +| `client_id` | Identifier of your instance | +| `is_google_login_enabled` | It gives information if google login is configured or not | +| `is_github_login_enabled` | It gives information if github login is configured or not | +| `is_facebook_login_enabled` | It gives information if facebook login is configured or not | +| `is_linkedin_login_enabled` | It gives information if linkedin login is configured or not | +| `is_apple_login_enabled` | It gives information if apple login is configured or not | +| `is_discord_login_enabled` | It gives information if discord login is configured or not | +| `is_twitter_login_enabled` | It gives information if twitter login is configured or not | +| `is_microsoft_login_enabled` | It gives information if microsoft login is configured or not | +| `is_twitch_login_enabled` | It gives information if twitch login is configured or not | +| `is_roblox_login_enabled` | It gives information if roblox login is configured or not | +| `is_email_verification_enabled` | It gives information if email verification is enabled or not | +| `is_basic_authentication_enabled` | It gives information, if basic auth is enabled or not | +| `is_magic_link_login_enabled` | It gives information if password less login is enabled or not | +| `is_sign_up_enabled` | It gives information if new sign ups are allowed | +| `is_strong_password_enabled` | It gives information if strong password policy is enforced | +| `is_multi_factor_auth_enabled` | It gives information if multi-factor authentication is enabled | +| `is_mobile_basic_authentication_enabled` | It gives information if mobile (phone number) basic auth is enabled | +| `is_phone_verification_enabled` | It gives information if phone verification is enabled | +| `is_totp_mfa_enabled` | It gives information if TOTP is available as an MFA method | +| `is_email_otp_mfa_enabled` | It gives information if email OTP is available as an MFA method | +| `is_sms_otp_mfa_enabled` | It gives information if SMS OTP is available as an MFA method | +| `is_webauthn_enabled` | It gives information if WebAuthn/passkeys are available as an MFA method | +| `is_mfa_enforced` | It gives information if MFA is enforced for all users | +| `is_org_discovery_enabled` | It gives information if home-realm/org discovery is enabled | **Sample Usage** ```js -const { data, errors } await authRef.getMetadata() +const { data, errors } = await authRef.getMetaData() ``` ## - `getSession` Function to get session information. This function makes an authorized request, hence if it is used from the browser the HTTP cookie is sent if user has logged in else you need to pass headers object. -It accepts the optional JSON object as parameter, you can pass the HTTP Headers there. Optionally you can also pass a `SessionQueryRequest` object as the second argument to validate `roles` against the session. +It accepts the optional JSON object as parameter, you can pass the HTTP Headers there. Optionally you can also pass a `SessionQueryRequest` object (`{ roles?: string[], scope?: string[] }`) as the second argument to validate `roles` / `scope` against the session. | Key | Description | Required | | --------------- | ------------------------------------------------------------------------------------ | -------- | @@ -490,10 +655,16 @@ It returns the following keys in response `data` object | `message` | Error / Success message from server | | `access_token` | accessToken that frontend application can use for further authorized requests | | `expires_in` | timestamp when the current token is going to expire, so that frontend can request for new access token | +| `id_token` | JWT token holding the user information | +| `refresh_token` | When scope includes `offline_access`, Long living token is returned which can be used to get new access tokens. This is rotated with each request | | `user` | User object with all the basic profile information | | `should_show_email_otp_screen` | Is set to true if email based multi factor authentication is enabled | | `should_show_mobile_otp_screen` | Is set to true if mobiled based multi factor authentication is enabled | | `should_show_totp_screen` | Is set to true if totp based multi factor authentication is enabled | +| `should_offer_webauthn_mfa_verify` | Is set to true if the user should be offered to verify with an existing passkey as their second factor | +| `should_offer_webauthn_mfa_setup` | Is set to true if the user should be offered to enroll a passkey as their second factor | +| `should_offer_email_otp_mfa_setup` | Is set to true if the user should be offered email-OTP MFA enrollment | +| `should_offer_sms_otp_mfa_setup` | Is set to true if the user should be offered SMS-OTP MFA enrollment | | `authenticator_scanner_image` | If totp registration is pending it sends base64 encoded image string that can be rendered by totp app scanners like Google Authentication | | `authenticator_secret` | If totp registration is pending, then this secret can be used for registration instead of image on authenticator apps | | `authenticator_recovery_codes` | If totp registration is pending, then recovery codes are sent using which totp can be accessed again | @@ -504,15 +675,15 @@ It returns the following keys in response `data` object // from browser with HTTP Cookie const { data, errors } = await authRef.getSession() -// role validation with http cookie -const { data, errors } = await authRef.getSession(null, 'admin') +// role validation with http cookie — the second argument is a SessionQueryRequest object, not a bare string +const { data, errors } = await authRef.getSession(null, { roles: ['admin'] }) // from NodeJS / if HTTP cookie is not used const { data, errors } = await authRef.getSession( { Authorization: `Bearer some_token`, }, - 'admin', + { roles: ['admin'] }, ) ``` @@ -591,6 +762,7 @@ It returns the following keys in response `data` object | Key | Description | | ---------- | -------------------------------------------------- | | `is_valid` | Boolean indicating if given token was valid or not | +| `claims` | Decoded JWT claims of the validated token | **Sample Usage** @@ -619,6 +791,7 @@ It returns the following keys in response `data` object | Key | Description | | ---------- | -------------------------------------------------- | | `is_valid` | Boolean indicating if given token was valid or not | +| `user` | User object with all the basic profile information | **Sample Usage** @@ -694,19 +867,19 @@ const { data: whatIf } = await authRef.checkPermissions( ## - `listPermissions` -Function to list what the subject can access — ideal for filtering a list page down to what the user may see. With both `relation` and `object_type` set it answers "which `object_type`s can I `relation`?"; either or both filters may be omitted, so an empty `data` object returns every permission the subject holds. Subject resolution follows the same rules as `checkPermissions`. +Function to list which objects of a given type the subject holds a relation on — ideal for filtering a list page down to what the user may see ("which `object_type`s can I `relation`?"). Subject resolution follows the same rules as `checkPermissions`. It accepts 2 JSON objects as its parameters. -1. data - Optional relation / object type filters +1. data - Relation / object type to enumerate 2. headers - To pass Authorization header (optional in the browser) Here are the keys that the `data` object accepts | Key | Description | Required | | ------------- | ------------------------------------------------------------------------------------------------------ | -------- | -| `relation` | Relation to list for (e.g. `can_view`). Omit to enumerate every relation of the active model | false | -| `object_type` | Object type to enumerate (e.g. `document`). Omit to enumerate every type of the active model | false | +| `relation` | Relation to list for (e.g. `can_view`) | true | +| `object_type` | Object type to enumerate (e.g. `document`) | true | | `user` | Subject override ("type:id", or a bare id treated as `user:`). Honored only for super-admins or self | false | It returns the following keys in response `data` object @@ -716,8 +889,6 @@ It returns the following keys in response `data` object | Key | Description | | ------------- | ------------------------------------------------------------------------------------------ | | `objects` | Distinct fully-qualified ids of the objects the subject holds the relation on, e.g. `["document:1"]` | -| `permissions` | The `(object, relation)` detail behind `objects` — relevant when no `relation` filter was supplied | -| `truncated` | `true` when the result was capped (1000 entries) and more permissions exist | **Sample Usage** @@ -727,11 +898,6 @@ const { data, errors } = await authRef.listPermissions( { Authorization: `Bearer ${token}` }, ); // data?.objects => ['document:1', 'document:7', ...] - -// No filters: everything the caller holds, with per-relation detail -const all = await authRef.listPermissions({}, { Authorization: `Bearer ${token}` }); -// all.data?.permissions => [{ object: 'document:1', relation: 'can_view' }, ...] -// all.data?.truncated => false ``` ## - `verifyOtp` @@ -745,6 +911,8 @@ It accepts JSON object as a parameter with following keys | `email` | Email address of user | false | | `phone_number` | Phone number of user | false | | `otp` | OTP (One Time Password) sent to user email address | true | +| `is_totp` | Set to `true` when verifying/enrolling a TOTP code instead of an email/SMS OTP | false | +| `state` | Opaque state string round-tripped through the flow | false | Either `email` or `phone_number` is required @@ -763,6 +931,10 @@ It returns the following keys in response `data` object | `should_show_email_otp_screen` | Is set to true if email based multi factor authentication is enabled | | `should_show_mobile_otp_screen` | Is set to true if mobiled based multi factor authentication is enabled | | `should_show_totp_screen` | Is set to true if totp based multi factor authentication is enabled | +| `should_offer_webauthn_mfa_verify` | Is set to true if the user should be offered to verify with an existing passkey as their second factor | +| `should_offer_webauthn_mfa_setup` | Is set to true if the user should be offered to enroll a passkey as their second factor | +| `should_offer_email_otp_mfa_setup` | Is set to true if the user should be offered email-OTP MFA enrollment | +| `should_offer_sms_otp_mfa_setup` | Is set to true if the user should be offered SMS-OTP MFA enrollment | | `authenticator_scanner_image` | If totp registration is pending it sends base64 encoded image string that can be rendered by totp app scanners like Google Authentication | | `authenticator_secret` | If totp registration is pending, then this secret can be used for registration instead of image on authenticator apps | | `authenticator_recovery_codes` | If totp registration is pending, then recovery codes are sent using which totp can be accessed again | @@ -786,6 +958,7 @@ It accepts JSON object as a parameter with following keys | -------------- | --------------------- | -------- | | `email` | Email address of user | false | | `phone_number` | Phone number of user | false | +| `state` | Opaque state string round-tripped through the flow | false | Either `email` or `phone_number` is required @@ -834,3 +1007,357 @@ const { data, errors } = await authRef.deactivateAccount({ Authorization: `Bearer some_token`, }) ``` + +## - `skipMfaSetup` + +Function to skip a first-time MFA enrollment offer mid login (the `should_offer_*` gate) when the user has a completed-login state to fall back to. Returns the full `AuthResponse`/token shape (same as `login`/`signup`), since skipping completes the gate. + +It accepts JSON object as a parameter with following keys + +| Key | Description | Required | +| -------------- | ------------------------------------------------------ | -------- | +| `email` | Email address of user | false | +| `phone_number` | Phone number of user | false | +| `state` | Opaque state string round-tripped through the flow | false | + +**Sample Usage** + +```js +const { data, errors } = await authRef.skipMfaSetup({ + email: 'foo@bar.com', +}) +``` + +## - `lockMfa` + +Function to lock multi-factor authentication for a user by email or phone number. + +It accepts JSON object as a parameter with following keys + +| Key | Description | Required | +| -------------- | ------------------------ | -------- | +| `email` | Email address of user | false | +| `phone_number` | Phone number of user | false | + +It returns the following keys in response `data` object + +**Response** + +| Key | Description | +| --------- | ------------------------------------ | +| `message` | Success / Error message from server | + +**Sample Usage** + +```js +const { data, errors } = await authRef.lockMfa({ + email: 'foo@bar.com', +}) +``` + +## - `emailOtpMfaSetup` + +Function to enroll email-OTP as a multi-factor authentication method — sends an OTP to the user's email to be verified with [`verifyOtp`](#--verifyotp). + +It accepts an optional JSON object as a parameter with following keys + +| Key | Description | Required | +| -------------- | ------------------------ | -------- | +| `email` | Email address of user | false | +| `phone_number` | Phone number of user | false | + +It returns the following keys in response `data` object + +**Response** + +| Key | Description | +| --------- | ------------------------------------ | +| `message` | Success / Error message from server | + +**Sample Usage** + +```js +const { data, errors } = await authRef.emailOtpMfaSetup() +``` + +## - `smsOtpMfaSetup` + +Function to enroll SMS-OTP as a multi-factor authentication method — sends an OTP to the user's phone number to be verified with [`verifyOtp`](#--verifyotp). + +It accepts an optional JSON object as a parameter with following keys + +| Key | Description | Required | +| -------------- | ------------------------ | -------- | +| `email` | Email address of user | false | +| `phone_number` | Phone number of user | false | + +It returns the following keys in response `data` object + +**Response** + +| Key | Description | +| --------- | ------------------------------------ | +| `message` | Success / Error message from server | + +**Sample Usage** + +```js +const { data, errors } = await authRef.smsOtpMfaSetup() +``` + +## - `totpMfaSetup` + +Function to generate a fresh TOTP secret/QR code/recovery-codes for the caller to enroll as an MFA method — the TOTP twin of `emailOtpMfaSetup`/`smsOtpMfaSetup`. Unlike those, nothing is sent anywhere: the enrollment payload comes back directly in the response, so the caller scans/enters it, then completes enrollment via `verifyOtp({ is_totp: true, otp: '...' })`. + +It accepts an optional JSON object as a parameter with following keys + +| Key | Description | Required | +| -------------- | ------------------------ | -------- | +| `email` | Email address of user | false | +| `phone_number` | Phone number of user | false | + +It returns the following keys in response `data` object + +**Response** + +| Key | Description | +| ---------------------------------| ------------------------------------------------------------------------------------------------------------------------------------------- | +| `message` | Success / Error message from server | +| `should_show_totp_screen` | Boolean indicating the TOTP enrollment screen should be shown | +| `authenticator_scanner_image` | Base64 encoded QR image that can be scanned by authenticator apps like Google Authenticator | +| `authenticator_secret` | Secret that can be entered manually instead of scanning the QR image | +| `authenticator_recovery_codes` | Recovery codes that can be used to regain TOTP access if the device is lost | + +**Sample Usage** + +```js +const { data, errors } = await authRef.totpMfaSetup() +// render data.authenticator_scanner_image, then: +await authRef.verifyOtp({ is_totp: true, otp: '123456' }) +``` + +## WebAuthn / Passkeys + +The following methods drive [WebAuthn](https://www.w3.org/TR/webauthn-3/) passkey registration and login. The low-level `webauthn*` methods talk to the server only (GraphQL-only, no REST route) and expect/return the opaque JSON strings the WebAuthn spec defines; the higher-level `registerPasskey`/`loginWithPasskey*` helpers additionally drive the browser's `navigator.credentials` ceremony for you and are what most apps should use directly. + +## - `webauthnRegistrationOptions` + +Function to fetch passkey registration ceremony options from the server. + +| Argument | Description | Required | +| --------------- | ----------------------------------------------------------------------------- | -------- | +| `email` | Email of the user to register a passkey for (MFA-session-cookie path only) | false | +| `phoneNumber` | Phone number of the user (MFA-session-cookie path only) | false | + +**Response** + +| Key | Description | +| --------- | ------------------------------------------------------------------ | +| `options` | Opaque JSON string (`PublicKeyCredentialCreationOptionsJSON`) to pass to the browser's WebAuthn API | + +**Sample Usage** + +```js +const { data, errors } = await authRef.webauthnRegistrationOptions() +``` + +## - `webauthnRegistrationVerify` + +Function to verify a completed passkey registration ceremony. + +| Key | Description | Required | +| ---------------- | --------------------------------------------------------------------------------------------------------- | -------- | +| `credential` | Opaque JSON string (`RegistrationResponseJSON`) returned by the browser's WebAuthn ceremony | true | +| `name` | Friendly name to store for this credential | false | +| `email` | Only used on the MFA-session-cookie path (registering a passkey mid login-time MFA offer) | false | +| `phone_number` | Only used on the MFA-session-cookie path | false | +| `state` | Opaque state string round-tripped through the flow | false | + +Returns the full `AuthResponse`/token shape: on the MFA-session-cookie path this also completes the gate, so `access_token` and friends are populated exactly like `verifyOtp`/`skipMfaSetup`. On the ordinary authenticated-settings-page path `access_token` is always `null` — the caller already has one. + +**Sample Usage** + +```js +const { data, errors } = await authRef.webauthnRegistrationVerify({ + credential: credentialJSON, + name: 'My laptop', +}) +``` + +## - `webauthnLoginOptions` + +Function to fetch passkey login (assertion) ceremony options from the server. + +| Argument | Description | Required | +| ---------- | ------------------------------------------------------------------------------------------ | -------- | +| `email` | Scopes the ceremony to one account's own passkeys. Omit for usernameless (discoverable-credential) login | false | + +**Response** + +| Key | Description | +| --------- | ------------------------------------------------------------------ | +| `options` | Opaque JSON string (`PublicKeyCredentialRequestOptionsJSON`) to pass to the browser's WebAuthn API | + +**Sample Usage** + +```js +const { data, errors } = await authRef.webauthnLoginOptions() +``` + +## - `webauthnLoginVerify` + +Function to verify a completed passkey login (assertion) ceremony. + +| Key | Description | Required | +| -------------- | --------------------------------------------------------------------------------------------- | -------- | +| `credential` | Opaque JSON string (`AuthenticationResponseJSON`) returned by the browser's WebAuthn ceremony | true | +| `state` | Opaque state string round-tripped through the flow | false | + +Returns the full `AuthResponse`/token shape, same as `login`. + +**Sample Usage** + +```js +const { data, errors } = await authRef.webauthnLoginVerify({ + credential: credentialJSON, +}) +``` + +## - `webauthnCredentials` + +Function to list the caller's enrolled passkeys. Takes no parameters. + +**Response** (array of) + +| Key | Description | +| ---------------- | ------------------------------------------------------ | +| `id` | Credential id | +| `name` | Friendly name for the credential | +| `transports` | Transports the authenticator advertised (e.g. `internal`, `usb`) | +| `created_at` | Timestamp the credential was registered | +| `updated_at` | Timestamp the credential was last updated | +| `last_used_at` | Timestamp the credential was last used to log in | + +**Sample Usage** + +```js +const { data, errors } = await authRef.webauthnCredentials() +``` + +## - `webauthnDeleteCredential` + +Function to delete one of the caller's enrolled passkeys by id. + +| Argument | Description | Required | +| ---------- | ----------------------------- | -------- | +| `id` | Id of the credential to delete | true | + +**Response** + +| Key | Description | +| --------- | ------------------------------------ | +| `message` | Success / Error message from server | + +**Sample Usage** + +```js +const { data, errors } = await authRef.webauthnDeleteCredential('credential-id') +``` + +## - `registerPasskey` + +High-level helper that drives a full passkey registration ceremony end to end: fetch options from the server (`webauthnRegistrationOptions`), prompt the platform authenticator via the browser's WebAuthn API, then verify (`webauthnRegistrationVerify`). Normally requires an authenticated session (a passkey is added to the caller's own account) — pass `mfaSetup` to instead authenticate via the MFA session cookie mid a login-time MFA offer. + +| Argument | Description | Required | +| ------------ | ----------------------------------------------------------------------------------------------------- | -------- | +| `name` | Friendly name to store for this credential | false | +| `mfaSetup` | `{ email?, phoneNumber?, state? }` — only used to authenticate via the MFA-session-cookie path | false | + +**Sample Usage** + +```js +const { data, errors } = await authRef.registerPasskey('My laptop') +``` + +## - `loginWithPasskey` + +High-level helper that drives a full passkey login ceremony end to end. Omit `email` for a usernameless (discoverable-credential) login; pass it to scope the ceremony to one account's own passkeys (the MFA-alternative flow). + +| Argument | Description | Required | +| ---------- | ------------------------------------------------------------------------------------------------------------------- | -------- | +| `email` | Scopes the ceremony to one account's own passkeys | false | +| `opts` | `{ mediation?: CredentialMediationRequirement, signal?: AbortSignal }` — pass `mediation: 'conditional'` for passkey autofill (prefer `loginWithPasskeyAutofill` instead) | false | + +**Sample Usage** + +```js +const { data, errors } = await authRef.loginWithPasskey() +``` + +## - `loginWithPasskeyAutofill` + +Starts a "passkey autofill" (conditional mediation) login: the browser offers discoverable passkeys inline in a username field's autofill dropdown rather than a modal. The returned promise resolves only when the user actually picks a passkey (or rejects when aborted/cancelled) — fire it on mount and ignore abort errors. Requires an `` on the page. Takes no parameters; only one autofill ceremony runs at a time (a new call, or an explicit modal `loginWithPasskey()`, aborts the previous one). + +**Sample Usage** + +```js +useEffect(() => { + authRef.loginWithPasskeyAutofill().then(({ data }) => { + if (data?.access_token) { + // logged in via autofilled passkey + } + }) + return () => authRef.cancelPasskeyAutofill() +}, []) +``` + +## - `cancelPasskeyAutofill` + +Aborts a pending `loginWithPasskeyAutofill` ceremony, e.g. on component unmount. Safe to call when none is in flight. Synchronous, returns nothing. + +**Sample Usage** + +```js +authRef.cancelPasskeyAutofill() +``` + +## - `isWebauthnSupported` + +Standalone function (not a method on the `Authorizer` instance) that reports whether the current browser supports the WebAuthn `PublicKeyCredential` JSON APIs this SDK's passkey methods rely on. + +**Sample Usage** + +```js +import { isWebauthnSupported } from '@authorizerdev/authorizer-js' + +if (isWebauthnSupported()) { + // show a "Sign in with a passkey" button +} +``` + +## - `parseMfaRedirectParams` + +Standalone function (not a method on the `Authorizer` instance) that parses the `mfa_required` / `mfa_methods` / `mfa_gate` query params the server's OAuth callback appends to the redirect URL instead of the normal `state`/`code` params when a first-time MFA offer or verification is needed before a token can be issued. Useful on the page your OAuth `redirectURL` points to. + +| Argument | Description | Required | +| ---------- | ------------------------------------------------------------------------------------------------------------- | -------- | +| `url` | The full redirect URL (e.g. `window.location.href`), or a `URL` instance. Must be absolute — a bare path/search string throws | true | + +Returns `null` if the URL has no `mfa_required=1` param, otherwise an object: + +| Key | Description | +| -------------- | --------------------------------------------------------------------------------------------------- | +| `mfaRequired` | Always `true` when non-null | +| `mfaMethods` | Raw method-name strings from the server (e.g. `totp`, `webauthn`, `email_otp`, `sms_otp`) | +| `mfaGate` | `'verify'` — the user has a completed second factor to challenge; or `'offer'` — first-time enrollment with a Skip option. Defaults to `'offer'` when absent | + +**Sample Usage** + +```js +import { parseMfaRedirectParams } from '@authorizerdev/authorizer-js' + +const params = parseMfaRedirectParams(window.location.href) +if (params?.mfaRequired) { + // route to the MFA setup/verify screen for params.mfaMethods +} +``` diff --git a/docs/sdks/authorizer-js/index.md b/docs/sdks/authorizer-js/index.md index 9233bfa..0b8faeb 100644 --- a/docs/sdks/authorizer-js/index.md +++ b/docs/sdks/authorizer-js/index.md @@ -127,7 +127,7 @@ const { Authorizer } = require("@authorizerdev/authorizer-js"); const authRef = new Authorizer({ authorizerURL: "https://app.heroku.com", redirectURL: "http://app.heroku.com/app", - clientID: + clientID: "YOUR_CLIENT_ID", }); async function main() { diff --git a/docs/sdks/authorizer-python/admin.md b/docs/sdks/authorizer-python/admin.md index 17a9fd0..854049d 100644 --- a/docs/sdks/authorizer-python/admin.md +++ b/docs/sdks/authorizer-python/admin.md @@ -109,8 +109,12 @@ Each method declares which protocols support it. Calling a method on an unsuppor protocol raises a clear error early rather than emitting a 404. > **⚠ Destructive:** `delete_user`, `delete_webhook`, `delete_email_template`, -> `fga_write_model` (overwrites the model), `fga_delete_tuples`, and `fga_reset` (wipes all -> FGA data) permanently change or remove data. +> `fga_write_model` (overwrites the model), `fga_delete_tuples`, `fga_reset` (wipes all +> FGA data), `delete_client`, `rotate_client_secret`/`rotate_scim_token` (invalidate the +> old secret/token), `delete_trusted_issuer`, `delete_organization`, +> `delete_org_oidc_connection`/`delete_org_saml_connection`, `delete_saml_service_provider`, +> `retire_saml_idp_key`, `delete_org_domain`, and `delete_scim_endpoint` permanently change +> or remove data — see each method's note below. #### Auth, session & meta @@ -174,12 +178,81 @@ protocol raises a clear error early rather than emitting a 404. | `fga_expand` | Expand a relation into its userset. | ✓ | ✓ | ✓ | | `fga_reset` | **Reset all FGA data.** | ✓ | ✓ | | +#### Clients (service accounts / machine identities) + +Clients created here authenticate over `/oauth/token` with the `client_credentials` and +token-exchange grants — see [Machine-to-machine & agent delegation](./functions#oauth-rest). + +| Method | Description | grpc | rest | gql | +| ---------------------- | ------------------------------------------------ | :--: | :--: | :-: | +| `create_client` | Create a client. `client_secret` is shown **once**. | ✓ | ✓ | ✓ | +| `update_client` | Update a client. | ✓ | ✓ | ✓ | +| `delete_client` | **Delete a client** — its tokens stop resolving. | ✓ | ✓ | ✓ | +| `rotate_client_secret` | **Rotate a client's secret** (old one invalidated, new one shown once). | ✓ | ✓ | ✓ | +| `get_client` | Get a single client. | ✓ | ✓ | ✓ | +| `clients` | List clients (paginated). | ✓ | ✓ | ✓ | + +#### Trusted issuers + +External OIDC/JWT issuers Authorizer accepts tokens from (e.g. for federated +machine/agent identities). + +| Method | Description | grpc | rest | gql | +| ------------------------ | ----------------------------------------- | :--: | :--: | :-: | +| `add_trusted_issuer` | Add a trusted issuer. | ✓ | ✓ | ✓ | +| `update_trusted_issuer` | Update a trusted issuer. | ✓ | ✓ | ✓ | +| `delete_trusted_issuer` | **Delete a trusted issuer** — its tokens stop authenticating. | ✓ | ✓ | ✓ | +| `get_trusted_issuer` | Get a single trusted issuer. | ✓ | ✓ | ✓ | +| `trusted_issuers` | List trusted issuers (paginated). | ✓ | ✓ | ✓ | + +#### SAML Identity Provider + +Authorizer acting as a SAML IdP for downstream service providers. + +| Method | Description | grpc | rest | gql | +| -------------------------------- | -------------------------------------------------------------------- | :--: | :--: | :-: | +| `create_saml_service_provider` | Register a downstream SP. | ✓ | ✓ | ✓ | +| `update_saml_service_provider` | Update a registered SP. | ✓ | ✓ | ✓ | +| `delete_saml_service_provider` | **Delete a registered SP** — it can no longer be issued assertions. | ✓ | ✓ | ✓ | +| `get_saml_service_provider` | Get a single registered SP. | ✓ | ✓ | ✓ | +| `list_saml_service_providers` | List registered SPs (paginated). | ✓ | ✓ | ✓ | +| `rotate_saml_idp_cert` | Generate a new signing keypair; the previous key stays active. | ✓ | ✓ | ✓ | +| `retire_saml_idp_key` | **Retire a signing key** — drops out of IdP metadata; cannot retire the current key. | ✓ | ✓ | ✓ | +| `list_saml_idp_keys` | List signing keys (`-> list[SAMLIDPKey]`). | ✓ | ✓ | ✓ | +| `import_saml_sp_metadata` | Parse pasted SP metadata XML (no record is created, no URL fetched). | ✓ | ✓ | ✓ | + #### GraphQL-only extras These have no REST / gRPC equivalent and work **over GraphQL only**: -| Method | Description | -| -------------------- | ------------------------------------ | -| `admin_signup` | Bootstrap the first admin. | -| `update_env` | Update server environment/config. | -| `generate_jwt_keys` | Generate a new JWT signing key pair. | +| Method | Description | +| --------------------------------- | ------------------------------------------------------- | +| `admin_signup` | Bootstrap the first admin. | +| `update_env` | Update server environment/config. | +| `generate_jwt_keys` | Generate a new JWT signing key pair. | +| `create_organization` | Create an organization. | +| `update_organization` | Update an organization. | +| `delete_organization` | **Delete an organization.** | +| `get_organization` | Get a single organization. | +| `organizations` | List organizations (paginated). | +| `add_org_member` | Add a member to an organization. | +| `remove_org_member` | Remove a member from an organization. | +| `org_members` | List an organization's members. | +| `create_org_oidc_connection` | Create an org-scoped OIDC SSO connection. | +| `update_org_oidc_connection` | Update an org-scoped OIDC SSO connection. | +| `delete_org_oidc_connection` | **Delete an org-scoped OIDC SSO connection** — members lose this SSO path. | +| `get_org_oidc_connection` | Get an org-scoped OIDC SSO connection. | +| `create_org_saml_connection` | Create an org-scoped SAML SSO connection. | +| `update_org_saml_connection` | Update an org-scoped SAML SSO connection. | +| `delete_org_saml_connection` | **Delete an org-scoped SAML SSO connection** — members lose this SSO path. | +| `get_org_saml_connection` | Get an org-scoped SAML SSO connection. | +| `user_organizations` | List the organizations a user belongs to. | +| `request_org_domain` | Start home-realm-discovery domain verification (DNS challenge). | +| `verify_org_domain` | Verify a requested domain's DNS challenge. | +| `add_verified_org_domain` | Super-admin only: trust-assert a domain as verified, skipping the DNS challenge. | +| `delete_org_domain` | **Delete a verified org domain** — it stops routing logins to the org. | +| `org_domains` | List an organization's verified domains. | +| `create_scim_endpoint` | Create a SCIM provisioning endpoint. Bearer token shown **once**. | +| `rotate_scim_token` | **Rotate a SCIM endpoint's bearer token** (old one invalidated, new one shown once). | +| `delete_scim_endpoint` | **Delete a SCIM endpoint** — provisioning stops working. | +| `get_scim_endpoint` | Get a single SCIM endpoint. | diff --git a/docs/sdks/authorizer-python/functions.md b/docs/sdks/authorizer-python/functions.md index b49af38..438ede5 100644 --- a/docs/sdks/authorizer-python/functions.md +++ b/docs/sdks/authorizer-python/functions.md @@ -19,6 +19,9 @@ from authorizer import ( ValidateJWTTokenRequest, ValidateSessionRequest, SessionQueryRequest, UpdateProfileRequest, GetTokenRequest, RevokeTokenRequest, CheckPermissionsRequest, ListPermissionsRequest, PermissionCheckInput, FgaTupleInput, + SkipMfaSetupRequest, LockMfaRequest, OtpMfaSetupRequest, + WebauthnRegistrationOptionsRequest, WebauthnRegistrationVerifyRequest, + WebauthnLoginOptionsRequest, WebauthnLoginVerifyRequest, WebauthnDeleteCredentialRequest, TokenType, ) ``` @@ -59,6 +62,40 @@ from authorizer import ( See the dedicated [Fine-Grained Authorization](./fga) page for usage. +### MFA setup & recovery + +All take a bearer token via `headers` (or, if the caller doesn't have one yet, an +`email`/`phone_number` pair that resolves the in-progress MFA session cookie instead). + +| Method | Signature | Returns | +| ----------------------- | ------------------------------------------------------------------------ | ------------------- | +| `skip_mfa_setup` | `skip_mfa_setup(req: SkipMfaSetupRequest, headers=None)` | `AuthToken` | +| `lock_mfa` | `lock_mfa(req: LockMfaRequest, headers=None)` | `GenericResponse` | +| `email_otp_mfa_setup` | `email_otp_mfa_setup(req: OtpMfaSetupRequest=None, headers=None)` | `GenericResponse` | +| `sms_otp_mfa_setup` | `sms_otp_mfa_setup(req: OtpMfaSetupRequest=None, headers=None)` | `GenericResponse` | +| `totp_mfa_setup` | `totp_mfa_setup(req: OtpMfaSetupRequest=None, headers=None)` | `AuthToken` | + +> `lock_mfa` has no OTP fallback — it locks the account and requires admin recovery +> (see [admin `update_user`](./admin)) afterward. + +### WebAuthn / passkeys + +| Method | Signature | Returns | +| ---------------------------------- | ------------------------------------------------------------------------------------ | ---------------------------------------- | +| `webauthn_registration_options` | `webauthn_registration_options(req: WebauthnRegistrationOptionsRequest=None, headers=None)` | `WebauthnRegistrationOptionsResponse` | +| `webauthn_registration_verify` | `webauthn_registration_verify(req: WebauthnRegistrationVerifyRequest, headers=None)` | `AuthToken` | +| `webauthn_login_options` | `webauthn_login_options(req: WebauthnLoginOptionsRequest=None, headers=None)` | `WebauthnLoginOptionsResponse` | +| `webauthn_login_verify` | `webauthn_login_verify(req: WebauthnLoginVerifyRequest, headers=None)` | `AuthToken` | +| `webauthn_delete_credential` | `webauthn_delete_credential(req: WebauthnDeleteCredentialRequest, headers=None)` | `GenericResponse` | +| `webauthn_credentials` | `webauthn_credentials(headers=None)` | `list[WebauthnCredentialInfo]` | + +`webauthn_registration_options`/`webauthn_login_options` return a JSON-encoded +`options` string to feed straight to the browser's `navigator.credentials.create()` / +`.get()`; `webauthn_registration_verify`/`webauthn_login_verify` take the JSON-encoded +credential response back. `webauthn_delete_credential` permanently deletes the +registered passkey. `webauthn_credentials` lists the authenticated caller's own +registered passkeys. + ### OAuth (REST) | Method | Signature | Returns | @@ -66,6 +103,65 @@ See the dedicated [Fine-Grained Authorization](./fga) page for usage. | `get_token` | `get_token(req: GetTokenRequest)` | `GetTokenResponse` | | `revoke_token` | `revoke_token(req: RevokeTokenRequest)` | `GenericResponse` | +`get_token` posts a form-encoded (`application/x-www-form-urlencoded`) request to +`/oauth/token` and supports four grants: `authorization_code` (default, needs `code` + +`code_verifier`), `refresh_token`, `client_credentials` (RFC 6749 §4.4), and RFC 8693 +token exchange. The `client_credentials` and token-exchange grants are **machine / +agent-to-agent flows — server-side only**: never send `client_secret`, +`client_assertion`, or a `subject_token`/`actor_token` to untrusted or browser code. + +#### Machine-to-machine (client_credentials) + +Get a token for a service account / machine identity created via the [admin +`create_client`](./admin) method: + +```python +from authorizer import ( + AuthorizerClient, GetTokenRequest, GRANT_TYPE_CLIENT_CREDENTIALS, +) + +machine = AuthorizerClient(client_id="SERVICE_CLIENT_ID", authorizer_url="https://your-instance.authorizer.dev") +token = machine.get_token( + GetTokenRequest(grant_type=GRANT_TYPE_CLIENT_CREDENTIALS, client_secret="SERVICE_CLIENT_SECRET") +) +print(token.access_token, token.scope) +``` + +#### Agent delegation (RFC 8693 token exchange) + +An agent acting on behalf of a signed-in user exchanges the user's token plus its own +machine token for a delegated token. The original user stays the JWT `sub`; each hop +narrows `scope` and appends to the nested `act` claim (re-widening scope is rejected): + +```python +from authorizer import ( + AuthorizerClient, GetTokenRequest, + GRANT_TYPE_CLIENT_CREDENTIALS, GRANT_TYPE_TOKEN_EXCHANGE, TOKEN_TYPE_ACCESS_TOKEN, +) + +agent = AuthorizerClient(client_id="AGENT_CLIENT_ID", authorizer_url="https://your-instance.authorizer.dev") + +# 1. the agent authenticates as itself +machine_token = agent.get_token( + GetTokenRequest(grant_type=GRANT_TYPE_CLIENT_CREDENTIALS, client_secret="AGENT_CLIENT_SECRET") +) + +# 2. exchange the user's token for one delegated to this agent, scoped down +delegated = agent.get_token( + GetTokenRequest( + grant_type=GRANT_TYPE_TOKEN_EXCHANGE, + client_secret="AGENT_CLIENT_SECRET", + subject_token=user_access_token, + subject_token_type=TOKEN_TYPE_ACCESS_TOKEN, + actor_token=machine_token.access_token, + actor_token_type=TOKEN_TYPE_ACCESS_TOKEN, + scope="crm:read", + resource="https://crm.internal.example", + ) +) +print(delegated.access_token) # sub is still the user; act.sub is the agent +``` + ### Escape hatch — raw GraphQL For any operation not covered by a typed helper: @@ -145,7 +241,7 @@ All request dataclasses serialize via `to_dict()`. Fields shown `| None` are opt | Type | Key fields | | -------------------------- | ------------------------------------------------------------------------------------------------------------------------- | | `LoginRequest` | `password*`, `email`, `phone_number`, `roles`, `scope`, `state` | -| `SignUpRequest` | `password*`, `confirm_password*`, `email`, `given_name`, `family_name`, `phone_number`, `roles`, `scope`, `redirect_uri`, `is_multi_factor_auth_enabled`, `app_data`, … | +| `SignUpRequest` | `password*`, `confirm_password*`, `email`, `given_name`, `family_name`, `phone_number`, `roles`, `scope`, `redirect_uri`, `app_data`, … | | `MagicLinkLoginRequest` | `email*`, `roles`, `scope`, `state`, `redirect_uri` | | `VerifyOTPRequest` | `otp*`, `email`, `phone_number`, `is_totp`, `state` | | `VerifyEmailRequest` | `token*`, `state` | @@ -157,12 +253,20 @@ All request dataclasses serialize via `to_dict()`. Fields shown `| None` are opt | `ValidateSessionRequest` | `cookie`, `roles` | | `SessionQueryRequest` | `roles`, `scope` | | `UpdateProfileRequest` | `email`, `old_password`, `new_password`, `confirm_new_password`, `given_name`, `family_name`, `roles`, `app_data`, … | -| `GetTokenRequest` | `code`, `grant_type`, `refresh_token`, `code_verifier` | +| `GetTokenRequest` | `code`, `grant_type`, `refresh_token`, `code_verifier`, `client_secret`, `scope`, `client_assertion`, `client_assertion_type`, `subject_token`, `subject_token_type`, `actor_token`, `actor_token_type`, `resource` | | `RevokeTokenRequest` | `refresh_token*` | | `CheckPermissionsRequest` | `checks*` (`list[PermissionCheckInput]`), `user` | | `ListPermissionsRequest` | `relation`, `object_type`, `user` | | `PermissionCheckInput` | `relation*`, `object*`, `contextual_tuples` (`list[FgaTupleInput]`) | | `FgaTupleInput` | `user*`, `relation*`, `object*` | +| `SkipMfaSetupRequest` | `email`, `phone_number`, `state` | +| `LockMfaRequest` | `email`, `phone_number` | +| `OtpMfaSetupRequest` | `email`, `phone_number` (shared by `email_otp_mfa_setup` / `sms_otp_mfa_setup` / `totp_mfa_setup`) | +| `WebauthnRegistrationOptionsRequest` | `email`, `phone_number` | +| `WebauthnRegistrationVerifyRequest` | `credential*`, `name`, `email`, `phone_number`, `state` | +| `WebauthnLoginOptionsRequest` | `email` | +| `WebauthnLoginVerifyRequest` | `credential*`, `state` | +| `WebauthnDeleteCredentialRequest` | `id*` | `* = required` @@ -179,11 +283,14 @@ All response dataclasses are built via `from_dict()`. | `ValidateJWTTokenResponse` | `is_valid`, `claims` | | `ValidateSessionResponse` | `is_valid`, `user` | | `MetaData` | `version`, `client_id`, and `is_*_enabled` feature flags (login providers, MFA, sign-up, etc.) | -| `GetTokenResponse` | `access_token`, `expires_in`, `id_token`, `refresh_token` | +| `GetTokenResponse` | `access_token`, `expires_in`, `id_token`, `refresh_token`, `token_type`, `scope`, `issued_token_type` | | `CheckPermissionsResponse` | `results` (`list[PermissionCheckResult]`) | | `PermissionCheckResult` | `relation`, `object`, `allowed` | | `ListPermissionsResponse` | `objects`, `permissions` (`list[Permission]`), `truncated` | | `Permission` | `object`, `relation` | +| `WebauthnRegistrationOptionsResponse` | `options` (JSON-encoded `PublicKeyCredentialCreationOptions`) | +| `WebauthnLoginOptionsResponse` | `options` (JSON-encoded `PublicKeyCredentialRequestOptions`) | +| `WebauthnCredentialInfo` | `id`, `name`, `transports`, `created_at`, `updated_at`, `last_used_at` | ## Enums @@ -193,6 +300,21 @@ All response dataclasses are built via `from_dict()`. | `ResponseTypes` | `CODE`, `TOKEN` | | `OAuthProviders` | `APPLE`, `GITHUB`, `GOOGLE`, `FACEBOOK`, `LINKEDIN`, `TWITTER`, `MICROSOFT`, `TWITCH`, `ROBLOX`, `DISCORD` | +## OAuth grant / token-type constants + +Plain string constants (not enums) for building `GetTokenRequest` — pass their values, or +the constants themselves, to `grant_type`, `*_token_type`, and `client_assertion_type`: + +| Constant | Value | +| ---------------------------------- | --------------------------------------------------------------- | +| `GRANT_TYPE_AUTHORIZATION_CODE` | `"authorization_code"` | +| `GRANT_TYPE_REFRESH_TOKEN` | `"refresh_token"` | +| `GRANT_TYPE_CLIENT_CREDENTIALS` | `"client_credentials"` | +| `GRANT_TYPE_TOKEN_EXCHANGE` | `"urn:ietf:params:oauth:grant-type:token-exchange"` (RFC 8693) | +| `TOKEN_TYPE_ACCESS_TOKEN` | `"urn:ietf:params:oauth:token-type:access_token"` | +| `TOKEN_TYPE_JWT` | `"urn:ietf:params:oauth:token-type:jwt"` | +| `CLIENT_ASSERTION_TYPE_JWT_BEARER` | `"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"` (RFC 7523) | + ## Error handling The SDK raises two exception types: diff --git a/docs/sdks/authorizer-python/index.md b/docs/sdks/authorizer-python/index.md index f58236c..9e2f8df 100644 --- a/docs/sdks/authorizer-python/index.md +++ b/docs/sdks/authorizer-python/index.md @@ -117,6 +117,8 @@ AuthorizerClient( authorizer_url: str, redirect_url: str = "", extra_headers: dict[str, str] | None = None, + protocol: str = "graphql", + grpc_endpoint: str = "", ) ``` @@ -126,6 +128,8 @@ AuthorizerClient( | `authorizer_url` | Base URL of your Authorizer instance, **no trailing slash**. | yes | | `redirect_url` | Default redirect URL used by magic-link and forgot-password flows. | no | | `extra_headers` | Extra headers sent on every request (e.g. a custom `Origin` for CSRF). | no | +| `protocol` | Wire protocol: `"graphql"` (default), `"rest"`, or `"grpc"`. | no | +| `grpc_endpoint` | gRPC target `host:port`, only used when `protocol="grpc"`. See [Protocols & Admin API](./admin). | no | > **CSRF (v2.3.0+):** the SDK automatically sets an `Origin` header so state-changing > requests aren't rejected with `403`. Override it via `extra_headers` if you need a @@ -147,3 +151,4 @@ print(user.email) - [Functions](./functions) — the complete method, request, and response reference. - [Fine-Grained Authorization](./fga) — `check_permissions` and `list_permissions`. +- [Protocols & Admin API](./admin) — `rest`/`grpc` transports and the `AuthorizerAdminClient`. diff --git a/docs/sdks/authorizer-react/components.md b/docs/sdks/authorizer-react/components.md index 37e2e13..370683f 100644 --- a/docs/sdks/authorizer-react/components.md +++ b/docs/sdks/authorizer-react/components.md @@ -15,9 +15,13 @@ title: Components - [`AuthorizerBasicAuthLogin`](#authorizerbasicauthlogin) - [`AuthorizerMagicLinkLogin`](#authorizermagiclinklogin) - [`AuthorizerSocialLogin`](#authorizersociallogin) +- [`AuthorizerPasskeyLogin`](#authorizerpasskeylogin) - [`AuthorizerForgotPassword`](#authorizerforgotpassword) - [`AuthorizerResetPassword`](#authorizerresetpassword) - [`AuthorizerVerifyOtp`](#authorizerverifyotp) +- [`AuthorizerTOTPScanner`](#authorizertotpscanner) +- [`AuthorizerMFASetup`](#authorizermfasetup) +- [`AuthorizerPasskeyRegister`](#authorizerpasskeyregister) ## `AuthorizerProvider` @@ -27,17 +31,19 @@ title: Components - `config`: Object to configure the `authorizer` backend URL and redirect URL. It accepts JSON object with following keys -| Key | Type | Description | Required | -| ----------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -------- | -| `authorizerURL` | `string` | Authorizer backend URL | `true` | -| `redirectURL` | `string` | Frontend application URL or the page where you want to redirect user post login. Default value is `window.location.origin` | `true` | -| `clientID` | `string` | Your client identifier (the value of `--client-id` flag used when starting the server) | `false` | -| `protocol` | `'graphql' \| 'rest'` | Wire transport for API calls. Default is `'graphql'`. Use `'rest'` to route calls through the typed REST endpoints instead. | `false` | -| `extraHeaders` | `Record` | Optional headers added to every SDK request (e.g. `{ 'Origin': 'https://your-app.com' }`) | `false` | -| `onStateChangeCallback` | `function` | Async callback that is called whenever context state information changes. | `false` | +| Key | Type | Description | Required | +| --------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------ | -------- | +| `authorizerURL` | `string` | Authorizer backend URL | `true` | +| `redirectURL` | `string` | Frontend application URL or the page where you want to redirect user post login. Default value is `window.location.origin` | `true` | +| `clientID` | `string` | Your client identifier (the value of `--client-id` flag used when starting the server) | `false` | +| `protocol` | `'graphql' \| 'rest'` | Wire transport for API calls. Default is `'graphql'`. Use `'rest'` to route calls through the typed REST endpoints instead. | `false` | > **Note:** `'grpc'` is not supported in `authorizer-react` because browsers cannot speak raw gRPC. Use `'graphql'` (default) or `'rest'`. +`AuthorizerProvider` also accepts one prop alongside `config` (not nested inside it): + +- `onStateChangeCallback={async (newState) => {}}`: Async callback fired whenever the context state (`user`, `token`, `loading`, `config`) changes. + ### Sample Usage ```jsx @@ -64,21 +70,25 @@ const App = () => { A core component that includes: -- social logins +- social logins (incl. Discord, when enabled on the backend) +- passkey ("Sign in with a passkey") login, shown automatically when the browser supports WebAuthn - signup view - login view - forgot password view +- multi-factor authentication setup, verification, and locked-account screens, driven by the backend's response to login/signup/passkey requests Pre configured component that shows various login/signup options based on the backend configurations. Make sure it is used as Child of `AuthorizerProvider`. ### Props -It has following optional props as callback events that are triggered via various user events. - - `onLogin={(loginResponse)=>{}}`: event called when login form is submitted successfully. -- `onMagicLinkLogin={(magicLinkResponse)=>{}}`: event called when magic link login form is submitted successfully. - `onSignup={(signupResponse)=>{}}`: event called when signup form is submitted successfully. -- `onForgotPassword={(forgotPasswordResponse)={}}`: called when forgot password form is submitted successfully. +- `onMagicLinkLogin={(magicLinkResponse)=>{}}`: event called when magic link login form is submitted successfully. +- `onForgotPassword={(forgotPasswordResponse)=>{}}`: called when forgot password form is submitted successfully. +- `onPasswordReset={()=>{}}`: called after a password reset completes from the forgot-password OTP flow. +- `onCancelMfa={()=>{}}`: optional. When provided, a "Back" link is shown on the MFA setup/verify screens (rendered when the URL carries MFA-redirect params, e.g. after an OAuth or magic-link continuation) so the host can send the user elsewhere, such as back to the login URL with the MFA params cleared. +- `roles={string[]}`: optional. Restricts which roles a login/signup grants, e.g. `roles={['user']}`. +- `signupFieldsOverrides={FormFieldsOverrides}`: optional. Lets you relabel, reword the placeholder of, hide, or make optional the `given_name`, `family_name`, `email_or_phone_number`, `password`, and `confirmPassword` fields on the signup form. Each field accepts `{ label?, placeholder?, hide?, notRequired? }`. ### Sample Usage @@ -94,7 +104,7 @@ const LoginPage = () => { onLogin={(loginResponse) => {}} onMagicLinkLogin={(magicLinkResponse) => {}} onSignup={(signupResponse) => {}} - onForgotPassword={(forgotPasswordResponse = {})} + onForgotPassword={(forgotPasswordResponse) => {}} /> ) @@ -108,6 +118,8 @@ A component to render basic authentication singup form. Make sure it is used as ### Props - `onSignup={(response)=>{}}`: event called when signup form is submitted successfully. +- `roles={string[]}`: optional. Restricts which roles the created account is granted. +- `fieldOverrides={FormFieldsOverrides}`: optional. Per-field `{ label?, placeholder?, hide?, notRequired? }` overrides for `given_name`, `family_name`, `email_or_phone_number`, `password`, and `confirmPassword`. ### Sample Usage @@ -132,6 +144,7 @@ A component to render basic authentication login form. Make sure this is used as ### Props - `onLogin={(response)=>{}}`: event called when login form is submitted successfully. +- `roles={string[]}`: optional. Restricts which roles the login grants. ### Sample Usage @@ -156,6 +169,7 @@ A component to render magic link login form. Make sure this is used as Child of ### Props - `onMagicLinkLogin={(response)=>{}}`: event called when magic link login form is submitted successfully. +- `roles={string[]}`: optional. Restricts which roles the login grants. ### Sample Usage @@ -175,11 +189,14 @@ const LoginPage = () => { ## `AuthorizerSocialLogin` -A component to render list of social media login buttons based on backend configurations. Make sure this is used as Child of `AuthorizerProvider`. +A component to render list of social media login buttons based on backend configurations (Google, GitHub, Facebook, LinkedIn, Apple, Twitter, Microsoft, Twitch, Roblox, and Discord — each shown only when its corresponding `is_*_login_enabled` flag is on). Clicking a button navigates the browser directly to the provider's OAuth flow (`{authorizerURL}/oauth_login/{provider}`); there are no submit callbacks. Make sure this is used as Child of `AuthorizerProvider`. ### Props -- `onForgotPassword={(response)=>{}}`: event called when forgot password form is submitted successfully. +It has no required props. Both are normally supplied for you when composed inside `Authorizer`. + +- `urlProps={Record}`: optional. `state`/`scope`/`redirect_uri` (or `redirectURL`) forwarded onto the OAuth redirect URL. +- `roles={string[]}`: optional. Restricts which roles the resulting login grants. ### Sample Usage @@ -197,6 +214,33 @@ const LoginPage = () => { } ``` +## `AuthorizerPasskeyLogin` + +Renders a "Sign in with a passkey" button that performs a passwordless, usernameless (discoverable-credential) WebAuthn login. Unlike social login, there is no backend config flag for this — it renders purely based on browser support (`isWebauthnSupported()`), and renders nothing when the browser can't do WebAuthn or when the org enforces MFA (`config.is_mfa_enforced`), since passkey-as-sole-factor would let a user skip a required second factor. Make sure this is used as Child of `AuthorizerProvider`. + +If the account resolved by the passkey ceremony needs a second factor, this component internally takes over and renders the MFA setup, verification, or locked-account screen instead of the button — see [`AuthorizerMFASetup`](#authorizermfasetup) and [`AuthorizerVerifyOtp`](#authorizerverifyotp). + +### Props + +- `onLogin={(data)=>{}}`: event called when the passkey login (or the MFA step that follows it) completes successfully. +- `onStepChange={(step)=>{}}`: optional. Fired whenever the component switches between `'button' | 'mfa-setup' | 'mfa-verify' | 'locked'`. Useful when rendering other login options alongside this component, so they can be hidden while an MFA screen is showing. + +### Sample Usage + +```jsx +import { AuthorizerPasskeyLogin } from '@authorizerdev/authorizer-react' + +const LoginPage = () => { + return ( + <> +

Login

+
+ {}} /> + + ) +} +``` + ## `AuthorizerForgotPassword` A component to render forgot password form. Make sure this is used as Child of `AuthorizerProvider`. @@ -227,9 +271,9 @@ A component that can be used to reset the password. This component can be used i ### Props -It has following optional prop as callback event that is triggered on form submit. - -- `onReset={(response) => {}}`: Called when reset form is submitted +- `onReset={(response) => {}}`: optional. Called when reset form is submitted. If omitted, the component redirects the browser to `redirect_uri` (from the URL) or the configured `redirectURL` on success. +- `showOTPInput={boolean}`: optional, default `false`. Renders an OTP field above the password fields — used for the mobile/phone forgot-password flow, where `AuthorizerForgotPassword` renders this component itself with `showOTPInput` set. +- `phone_number={string}`: optional. The phone number the OTP was sent to; required alongside `showOTPInput` for the phone reset flow. ### Sample Usage @@ -249,15 +293,18 @@ const ResetPassword = () => { ## `AuthorizerVerifyOtp` -A component to render the OTP verification form. Make sure it is used as Child of `AuthorizerProvider`. +A component to render the OTP/MFA verification form. It handles email/SMS OTP and TOTP codes, and — when offered alongside a code factor — a "Verify with a passkey" WebAuthn option. On SMS OTP it also attempts WebOTP auto-fill (`navigator.credentials.get({ otp: ... })`) so supporting browsers can fill the code automatically. Make sure it is used as Child of `AuthorizerProvider`. ### Props -- `email="foo@bar.com"`: user email address on which the OTP was sent. - -It also has following optional prop as callback event that is triggered on form submit. - -- `onLogin={(response)=>{}}`: event called when verify OTP form is submitted successfully. +- `email={string}`: optional. User email address the OTP was sent to. Not required for MFA continuations resolved purely from the session cookie (e.g. an OAuth-redirect or passkey-primary-login continuation, where the frontend never learns the identifier). +- `phone_number={string}`: optional. User phone number the OTP was sent to, as an alternative to `email`. +- `is_totp={boolean}`: optional, default `false`. Set when the pending factor is an authenticator-app (TOTP) code rather than an emailed/texted OTP — changes the help text and disables WebOTP/resend. +- `offerWebauthnVerify={boolean}`: optional, default `false`. Shows the "Verify with a passkey" button above the code form when the account also has webauthn as an available MFA factor. +- `hasCodeFactor={boolean}`: optional, default `false`. Whether a code-based factor (TOTP, or a verified email/SMS OTP) actually exists to fall back on. When `false` and `offerWebauthnVerify` is `true`, passkey is the only option and the code form is hidden entirely. +- `hasSmsOtp={boolean}`: optional, default `false`. True specifically when the pending code factor is SMS-delivered — gates the WebOTP auto-fill call, which only makes sense for an incoming SMS. +- `onBack={()=>{}}`: optional. When provided, a "Back" link lets the user leave this challenge (e.g. return to the login screen) instead of being stuck mid-verification. +- `onLogin={(response)=>{}}`: event called when the verify-OTP form (or passkey verification) is submitted successfully. ### Sample Usage @@ -269,8 +316,107 @@ const VerifyOtp = () => { <>

Verify OTP


- {}} /> + {}} + /> ) } ``` + +## `AuthorizerTOTPScanner` + +Shown right after a user opts into authenticator-app (TOTP) MFA: renders the QR code to scan, the setup key as a manual-entry fallback (with a copy button), the one-time recovery codes (copy/download/print), and then hands off to [`AuthorizerVerifyOtp`](#authorizerverifyotp) to confirm the first code. Make sure it is used as Child of `AuthorizerProvider`. + +You normally won't render this directly — [`AuthorizerMFASetup`](#authorizermfasetup) renders it for you once TOTP enrolment data is available. + +### Props + +- `authenticator_scanner_image={string}`: required. Base64-encoded QR code image (rendered as `data:image/jpeg;base64,...`). +- `authenticator_secret={string}`: required. The manual-entry setup key shown alongside the QR code. +- `authenticator_recovery_codes={string[]}`: required. One-time recovery codes shown once, with copy/download/print actions. +- `email={string}`: optional. Forwarded to the OTP-confirmation step. +- `phone_number={string}`: optional. Forwarded to the OTP-confirmation step. +- `onLogin={(response)=>{}}`: optional. Forwarded to the OTP-confirmation step; called once the first TOTP code is verified. +- `setView={(v)=>{}}`: optional. Forwarded to the OTP-confirmation step for view-switching hosts (e.g. `Authorizer`'s internal login/signup view state). + +### Sample Usage + +```jsx +import { AuthorizerTOTPScanner } from '@authorizerdev/authorizer-react' + +const TotpSetup = ({ enrollment }) => { + return ( + {}} + /> + ) +} +``` + +## `AuthorizerMFASetup` + +The hub where a signed-in (or mid-login) user opts into the MFA methods the server offers: authenticator app (TOTP), passkey, email one-time code, and SMS one-time code. Only methods you flag as `available` are shown, so users only ever see real options. TOTP and passkey have complete in-component enrolment flows ([`AuthorizerTOTPScanner`](#authorizertotpscanner) and [`AuthorizerPasskeyRegister`](#authorizerpasskeyregister)); email/SMS OTP are sent and confirmed via [`AuthorizerVerifyOtp`](#authorizerverifyotp) directly through the SDK, or delegated to your own UI via `onSetupMethod`. Make sure it is used as Child of `AuthorizerProvider`. + +`Authorizer` (and `AuthorizerSignup` / `AuthorizerBasicAuthLogin` / `AuthorizerPasskeyLogin`) already render this automatically when the backend offers MFA setup during login/signup — you mainly need this directly to build a custom "Security settings" page where a user can add a second factor later. + +### Props + +- `availableMfaMethods={AvailableMfaMethods}`: required. `{ totp?: boolean; passkey?: boolean; emailOtp?: boolean; smsOtp?: boolean }` — which methods to offer. An omitted/`false` value hides that method entirely. +- `totpEnrollment={{ authenticator_scanner_image, authenticator_secret, authenticator_recovery_codes }}`: optional. When present, choosing "Set up" for TOTP renders `AuthorizerTOTPScanner` inline with this data immediately. When absent, the component fetches it for you via the SDK's `totpMfaSetup`, unless `onSetupMethod` is supplied. +- `onSetupMethod={(method: MfaMethod)=>{}}`: optional escape hatch. Called instead of the default SDK-driven flow when the user picks a method your host wants to handle itself (`'totp' | 'passkey' | 'email_otp' | 'sms_otp'`). +- `heading={string}`: optional, default `"Add a second step to sign in"`. Heading text above the method list. +- `loginContext={{ email?, phone_number?, state?, onComplete }}`: optional. Present only for the login-time (withheld-token) MFA offer, as opposed to a settings-page "add a second factor" hub. Adds a "Skip for now" action, and completing a method calls `onComplete` with the token the server issues (instead of just closing/refreshing, as a settings-page usage would). +- `onBack={()=>{}}`: optional. Shows a "Back" link on the top-level method list so the host can let the user leave MFA setup entirely. +- `passkeyRegistered={boolean}`: optional. Whether the signed-in user already has a passkey registered, so the passkey row can show an "Enabled" badge and "Manage" instead of "Set up". + +It also exports two supporting types: + +- `AvailableMfaMethods` — the shape of the `availableMfaMethods` prop. +- `MfaMethod` — `'totp' | 'passkey' | 'email_otp' | 'sms_otp'`, the union passed to `onSetupMethod`. + +### Sample Usage + +```jsx +import { AuthorizerMFASetup } from '@authorizerdev/authorizer-react' + +const SecuritySettings = () => { + return ( + + ) +} +``` + +## `AuthorizerPasskeyRegister` + +Lets a signed-in user enrol a new passkey (WebAuthn credential) as an MFA / passwordless method, and optionally lists their already-registered passkeys. Complements [`AuthorizerPasskeyLogin`](#authorizerpasskeylogin), which only handles the login ceremony. Since passkey support is a browser capability rather than a server config flag, this renders an "unsupported" notice — instead of nothing — when the browser can't run the WebAuthn ceremony, since in a settings context the user needs to know why the option is missing. Make sure it is used as Child of `AuthorizerProvider`. + +### Props + +- `onSuccess={(data)=>{}}`: optional. Called after a passkey is successfully registered. During the login-time MFA-offer path (see `mfaSetup` below) this carries the `access_token` issued by completing a withheld MFA offer; a plain settings-page add never sets it. +- `name={string}`: optional. A friendly name for the credential (e.g. `"MacBook Touch ID"`). When omitted, an inline text field is shown so the user can name it themselves. +- `showCredentials={boolean}`: optional, default `false`. Shows the list of already-registered passkeys above the add button. Requires an authenticated session (calls `webauthnCredentials`), so it's off by default. +- `mfaSetup={{ email?, phoneNumber?, state? }}`: optional. Present only during a token-withheld login-time MFA offer — authenticates the registration ceremony via the MFA session cookie instead of a bearer token (there isn't one yet), and completing it issues the previously-withheld token. + +### Sample Usage + +```jsx +import { AuthorizerPasskeyRegister } from '@authorizerdev/authorizer-react' + +const SecuritySettings = () => { + return ( + {}} + /> + ) +} +``` diff --git a/docs/sdks/authorizer-react/hooks.md b/docs/sdks/authorizer-react/hooks.md index 6a2585d..9334de5 100644 --- a/docs/sdks/authorizer-react/hooks.md +++ b/docs/sdks/authorizer-react/hooks.md @@ -15,7 +15,8 @@ Here is the complete list of state variables and functions that are returned by | Variable name | Description | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `config` | Root configuration object passed to `AuthorizerProvider` + backend configuration obtained via [`meta`](/sdks/authorizer-js/functions#--getmetadata) api | +| `config` | Root configuration object passed to `AuthorizerProvider` + backend configuration obtained via [`meta`](/sdks/authorizer-js/functions#--getmetadata) api. Includes feature flags such as `is_discord_login_enabled`, `is_webauthn_enabled`, `is_totp_mfa_enabled`, `is_email_otp_mfa_enabled`, `is_sms_otp_mfa_enabled`, and `is_mfa_enforced`. | +| `configLoadError` | `string \| null`. Set when the initial `meta` fetch from `authorizerURL` fails (e.g. the backend is unreachable). Login methods that depend on that config — basic auth, signup, social login, magic link — won't render until it succeeds. | | `user` | If not logged in this is `null`, else it includes all the user related information like `email`, `given_name`, `family_name`, `picture`, and [more](/sdks/authorizer-js/functions#--getprofile) | | `token` | If not logged in it is `null` else it is access token string which can be used to make authorized requests | | `loading` | Loading state for the application to know if its fetching token or user profile data | diff --git a/docs/sdks/authorizer-react/index.md b/docs/sdks/authorizer-react/index.md index 0f4fec8..bc2f1e4 100644 --- a/docs/sdks/authorizer-react/index.md +++ b/docs/sdks/authorizer-react/index.md @@ -66,7 +66,6 @@ const App = () => { authorizerURL: 'http://localhost:8080', redirectURL: window.location.origin, clientID: 'YOUR_CLIENT_ID', // value of --client-id flag used to start the server - extraHeaders: {}, // Optional JSON object to pass extra headers in each authorizer requests. }} > @@ -100,6 +99,7 @@ Components in `@authorizerdev/authorizer-react` are designed using css variables --authorizer-primary-color: #3b82f6; --authorizer-primary-disabled-color: #60a5fa; --authorizer-gray-color: #d1d5db; +--authorizer-slate-color: #e2e8f0; --authorizer-white-color: #ffffff; --authorizer-danger-color: #dc2626; --authorizer-success-color: #10b981; diff --git a/docs/sdks/authorizer-react/integration.md b/docs/sdks/authorizer-react/integration.md index 3e3d766..7ac57d1 100644 --- a/docs/sdks/authorizer-react/integration.md +++ b/docs/sdks/authorizer-react/integration.md @@ -167,6 +167,50 @@ function LogoutButton() { } ``` +## Multi-factor authentication and passkeys + +`Authorizer` (and the individual `AuthorizerSignup` / `AuthorizerBasicAuthLogin` / +`AuthorizerPasskeyLogin` components) already handle MFA end-to-end during login and +signup: when the backend offers or requires a second factor, they automatically render +the setup, verification, or locked-account screens for you — no extra wiring needed. + +To let a signed-in user manage their own second factors from a "Security settings" page, +use [`AuthorizerMFASetup`](./components#authorizermfasetup) and +[`AuthorizerPasskeyRegister`](./components#authorizerpasskeyregister) directly: + +```jsx +import { + useAuthorizer, + AuthorizerMFASetup, + AuthorizerPasskeyRegister, +} from '@authorizerdev/authorizer-react'; + +function SecuritySettings() { + const { config } = useAuthorizer(); + + return ( + <> +

Two-factor authentication

+ + +

Passkeys

+ + + ); +} +``` + +`AuthorizerPasskeyLogin` and passkey verification only render for browsers that support +WebAuthn — there is no backend flag to gate them on, so gate any custom UI you build +around them on `isWebauthnSupported()` from `@authorizerdev/authorizer-js` the same way. + ## Next.js integration `authorizer-react` is a client-side library — render the provider in a client component diff --git a/docs/sdks/authorizer-svelte/components.md b/docs/sdks/authorizer-svelte/components.md index 6bb8e7d..928b889 100644 --- a/docs/sdks/authorizer-svelte/components.md +++ b/docs/sdks/authorizer-svelte/components.md @@ -25,13 +25,15 @@ title: Components ### Props -- `config`: Object to configure the `authorizer` backend URL and redirect URL. It accepts JSON object with following keys +- `config`: Object to configure the `authorizer` backend URL, redirect URL and client id. It accepts JSON object with following keys -| Key | Type | Description | Required | -| ----------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------- | -------- | -| `authorizerURL` | `string` | Authorizer backend URL | `true` | -| `redirectURL` | `string` | Frontend application URL or the page where you want to redirect user post login. Default value is `window.location.origin` | `true` | -| `onStateChangeCallback` | `function` | [optional] Async callback that is called whenever context state information changes. | `false` | +| Key | Type | Description | Required | +| --------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------| -------- | +| `authorizerURL` | `string` | Authorizer backend URL | `true` | +| `redirectURL` | `string` | Frontend application URL or the page where you want to redirect user post login. Default value is `window.location.origin` | `true` | +| `client_id` | `string` | Client ID of the application, generated from the Authorizer dashboard | `true` | + +- `onStateChangeCallback={(newState)=>{}}`: [optional] Async callback that is called whenever context state information changes. ### Sample Usage @@ -44,6 +46,7 @@ const App = () => { config={{ authorizerURL: 'http://localhost:8080', redirectURL: window.location.origin, + client_id: 'YOUR_CLIENT_ID', }} onStateChangeCallback={async (newState) => {}} > @@ -60,6 +63,7 @@ A core component that includes: - social logins - signup view - login view +- magic link login view - forgot password view Pre configured component that shows various login/signup options based on the backend configurations. Make sure it is used as Child of `AuthorizerProvider`. @@ -72,6 +76,7 @@ It has following optional props as callback events that are triggered via variou - `onMagicLinkLogin={(magicLinkResponse)=>{}}`: event called when magic link login form is submitted successfully. - `onSignup={(signupResponse)=>{}}`: event called when signup form is submitted successfully. - `onForgotPassword={(forgotPasswordResponse)={}}`: called when forgot password form is submitted successfully. +- `roles={['user']}`: [optional] list of roles to request for the logged in / signed up user. ### Sample Usage @@ -101,6 +106,9 @@ A component to render basic authentication singup form. Make sure it is used as ### Props - `onSignup={(response)=>{}}`: event called when signup form is submitted successfully. +- `setView={(view)=>{}}`: [optional] callback used internally by `Authorizer` to switch between `Login`/`Signup`/`ForgotPassword` views. Only needed if you are composing your own view switcher. +- `urlProps={{ scope: [], redirect_uri: undefined, state: undefined }}`: [optional] object to override the OAuth `scope`, `redirect_uri` and `state` query params used for the signup request. +- `roles={['user']}`: [optional] list of roles to request for the signed up user. ### Sample Usage @@ -125,6 +133,9 @@ A component to render basic authentication login form. Make sure this is used as ### Props - `onLogin={(response)=>{}}`: event called when login form is submitted successfully. +- `setView={(view)=>{}}`: [optional] callback used internally by `Authorizer` to switch between `Login`/`Signup`/`ForgotPassword` views. Only needed if you are composing your own view switcher. +- `urlProps={{ scope: [], redirect_uri: undefined, state: undefined }}`: [optional] object to override the OAuth `scope`, `redirect_uri` and `state` query params used for the login request. +- `roles={['user']}`: [optional] list of roles to request for the logged in user. ### Sample Usage @@ -149,6 +160,8 @@ A component to render magic link login form. Make sure this is used as Child of ### Props - `onMagicLinkLogin={(response)=>{}}`: event called when magic link login form is submitted successfully. +- `urlProps={{ state: 'random-string', redirect_uri: undefined }}`: [optional] object to override the `state` and `redirect_uri` query params used for the magic link request. +- `roles={['user']}`: [optional] list of roles to request for the logged in user. ### Sample Usage @@ -172,7 +185,8 @@ A component to render list of social media login buttons based on backend config ### Props -- `onForgotPassword={(response)=>{}}`: event called when forgot password form is submitted successfully. +- `urlProps={{ scope: [] }}`: [optional] object to override the OAuth `scope` query param sent with the social login request. +- `roles={['user']}`: [optional] list of roles to request for the logged in user. ### Sample Usage @@ -196,7 +210,9 @@ A component to render forgot password form. Make sure this is used as Child of ` ### Props -No props exposed for this components +- `onForgotPassword={(response)=>{}}`: event called when forgot password form is submitted successfully. +- `setView={(view)=>{}}`: [optional] callback used internally by `Authorizer` to switch between `Login`/`Signup`/`ForgotPassword` views. Only needed if you are composing your own view switcher. +- `urlProps={{ state: 'random-string', redirect_uri: undefined }}`: [optional] object to override the `state` and `redirect_uri` query params used for the request. ### Sample Usage @@ -248,9 +264,11 @@ A component to render the OTP verification form. Make sure it is used as Child o - `email="foo@bar.com"`: user email address on which the OTP was sent. -It also has following optional prop as callback event that is triggered on form submit. +It also has following optional props. -- `onLogin={(response)=>{}}`: event called when verify OTP form is submitted successfully. +- `onLogin={(response)=>{}}`: callback event that is triggered when the verify OTP form is submitted successfully. +- `setView={(view)=>{}}`: callback used internally by `Authorizer` to switch between `Login`/`Signup`/`ForgotPassword` views. Only needed if you are composing your own view switcher. +- `urlProps={{ state: undefined }}`: object to override the `state` query param used for the verify request. ### Sample Usage diff --git a/docs/sdks/authorizer-vue/index.md b/docs/sdks/authorizer-vue/index.md index 2a84fb6..8f14cc6 100644 --- a/docs/sdks/authorizer-vue/index.md +++ b/docs/sdks/authorizer-vue/index.md @@ -5,8 +5,352 @@ title: Getting Started # Getting Started -The Authorizer Vue SDK documentation is coming soon. +Vue 3 SDK for [authorizer.dev](https://authorizer.dev) integration in your [Vue](https://vuejs.org/) application. This will allow you to have authentication and authorization ready in minutes. -In the meantime, you can find the source code and usage instructions on the GitHub repository: +Here is a quick guide on getting started with `@authorizerdev/authorizer-vue` package. -[https://github.com/authorizerdev/authorizer-vue](https://github.com/authorizerdev/authorizer-vue) +## Step 1: Get Authorizer Instance + +Deploy production ready Authorizer instance using one click deployment options available below + +| **Infra provider** | **One-click link** | **Additional information** | +| :----------------: | :-----------------: | :----------------------------------------------------: | +| Railway.app | [Deploy on Railway](https://railway.com/deploy/authorizer-1?referralCode=FEF4uT&utm_medium=integration&utm_source=template&utm_campaign=generic) | [docs](https://docs.authorizer.dev/deployment/railway) | +| Heroku | [Deploy to Heroku](https://heroku.com/deploy?template=https://github.com/authorizerdev/authorizer-heroku) | [docs](https://docs.authorizer.dev/deployment/heroku) | +| Render | [Deploy to Render](https://render.com/deploy?repo=https://github.com/authorizerdev/authorizer-render) | [docs](https://docs.authorizer.dev/deployment/render) | + +For more information check [docs](https://docs.authorizer.dev/getting-started/) + +## Step 2: Setup Instance + +Start your Authorizer instance with the required CLI flags: + +```bash +./authorizer \ + --database-type=sqlite \ + --database-url=test.db \ + --jwt-type=HS256 \ + --jwt-secret=test \ + --admin-secret=admin \ + --client-id=123456 \ + --client-secret=secret +``` + +Note the `--client-id` value -- you will need it in the SDK configuration below. Check [Server Configuration](/core/server-config) for all available flags. + +## Step 3 - Install package + +Assuming you have a Vue 3 application up and running, install the following package in your application + +```sh +npm i --save @authorizerdev/authorizer-vue +OR +yarn add @authorizerdev/authorizer-vue +``` + +## Step 4 - Configure Provider and use Authorizer Components + +`AuthorizerProvider` uses Vue's [provide/inject](https://vuejs.org/api/composition-api-dependency-injection.html#provide) to make a reactive auth context available to all descendants via the `useAuthorizer` injection key. + +Configure `AuthorizerProvider` at the root of your application and import `default.css`. + +> Note: You can override default style with `css` variables. See [Updating styles](#updating-styles) below for more details. + +`eg: App.vue` + +```vue + + + +``` + +**Use `AuthorizerRoot` Component** + +`eg: views/Login.vue` + +```vue + + + +``` + +## Components + +`@authorizerdev/authorizer-vue` exports the following components that you can use in your Vue application to build authentication and authorization faster: + +- [`AuthorizerProvider`](#authorizerprovider) +- [`AuthorizerRoot`](#authorizerroot) +- [`AuthorizerSignup`](#authorizersignup) +- [`AuthorizerBasicAuthLogin`](#authorizerbasicauthlogin) +- [`AuthorizerMagicLinkLogin`](#authorizermagiclinklogin) +- [`AuthorizerSocialLogin`](#authorizersociallogin) +- [`AuthorizerForgotPassword`](#authorizerforgotpassword) +- [`AuthorizerResetPassword`](#authorizerresetpassword) +- [`AuthorizerVerifyOtp`](#authorizerverifyotp) + +### `AuthorizerProvider` + +`AuthorizerProvider` is the container component that wraps all the Authorizer components. It binds the backend configuration in the app and renders various views accordingly. It exposes a reactive context via the `useAuthorizer` provide/inject key, which you can `inject` in any descendant component. + +#### Props + +| Prop | Type | Required | Description | +| ----------------------- | ------------------------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------| +| `config` | `AuthorizerConfigInput` | Yes | Authorizer connection config (see below) | +| `onStateChangeCallback` | `(state: AuthorizerState) => void` | No | [optional] Callback that is called whenever context state information changes. | + +`config` accepts the following keys: + +| Key | Type | Description | Required | +| -------------- | ------------------------| ----------------------------------------------------------------------------------------------------------------------------| -------- | +| `authorizerURL`| `string` | Authorizer backend URL | `true` | +| `redirectURL` | `string` | Frontend application URL or the page where you want to redirect user post login. Default value is `window.location.origin` | `true` | +| `clientID` | `string` | Client ID of the application, generated from the Authorizer dashboard | `true` | +| `protocol` | `'graphql' \| 'rest'` | [optional] Transport protocol used by the underlying `authorizer-js` SDK. Defaults to `'graphql'` | `false` | + +#### Sample Usage + +```vue + + + +``` + +### `AuthorizerRoot` + +A core component that includes: + +- social logins +- signup view +- login view +- magic link login view +- forgot password view + +Pre configured component that shows various login/signup options based on the backend configurations. Make sure it is used as a descendant of `AuthorizerProvider`. + +#### Props + +It has the following optional props as callback events that are triggered via various user events, plus a `roles` prop. + +- `onLogin`: event called when login form is submitted successfully. +- `onSignup`: event called when signup form is submitted successfully. +- `onMagicLinkLogin`: event called when magic link login form is submitted successfully. +- `onForgotPassword`: event called when forgot password form is submitted successfully. +- `roles`: [optional] `string[]` list of roles to request for the logged in / signed up user. + +#### Sample Usage + +```vue + + + +``` + +### `AuthorizerSignup` + +A component to render the basic authentication signup form. Make sure it is used as a descendant of `AuthorizerProvider`. + +#### Props + +- `onSignup`: event called when signup form is submitted successfully. +- `setView`: [optional] callback used internally by `AuthorizerRoot` to switch between `Login`/`Signup`/`ForgotPassword` views. Only needed if you are composing your own view switcher. +- `urlProps`: [optional] object to override the `scope`, `redirect_uri` and `state` query params used for the signup request. +- `roles`: [optional] `string[]` list of roles to request for the signed up user. + +#### Sample Usage + +```vue + +``` + +### `AuthorizerBasicAuthLogin` + +A component to render the basic authentication login form. Make sure it is used as a descendant of `AuthorizerProvider`. + +#### Props + +- `onLogin`: event called when login form is submitted successfully. +- `setView`: [optional] callback used internally by `AuthorizerRoot` to switch between `Login`/`Signup`/`ForgotPassword` views. Only needed if you are composing your own view switcher. +- `urlProps`: [optional] object to override the `scope`, `redirect_uri` and `state` query params used for the login request. +- `roles`: [optional] `string[]` list of roles to request for the logged in user. + +#### Sample Usage + +```vue + +``` + +### `AuthorizerMagicLinkLogin` + +A component to render the magic link login form. Make sure it is used as a descendant of `AuthorizerProvider`. + +#### Props + +- `onMagicLinkLogin`: event called when magic link login form is submitted successfully. +- `urlProps`: [optional] object to override the `redirect_uri` and `state` query params used for the request. +- `roles`: [optional] `string[]` list of roles to request for the logged in user. + +#### Sample Usage + +```vue + +``` + +### `AuthorizerSocialLogin` + +A component to render the list of social media login buttons based on backend configurations. Supports Google, GitHub, Facebook, LinkedIn, Apple, Twitter, Microsoft, Discord and Roblox. Make sure it is used as a descendant of `AuthorizerProvider`. + +#### Props + +- `urlProps`: [optional] object to override the `scope` query param sent with the social login request. +- `roles`: [optional] `string[]` list of roles to request for the logged in user. + +#### Sample Usage + +```vue + +``` + +### `AuthorizerForgotPassword` + +A component to render the forgot password form. Make sure it is used as a descendant of `AuthorizerProvider`. + +#### Props + +- `onForgotPassword`: event called when forgot password form is submitted successfully. +- `setView`: [optional] callback used internally by `AuthorizerRoot` to switch between `Login`/`Signup`/`ForgotPassword` views. Only needed if you are composing your own view switcher. +- `urlProps`: [optional] object to override the `redirect_uri` and `state` query params used for the request. + +#### Sample Usage + +```vue + +``` + +### `AuthorizerResetPassword` + +A component that can be used to reset the password. This component can be used on the page which is configured with the backend as `RESET_PASSWORD_URL`, check [environment variables](/core/server-config) for more details. This component validates the token in the URL sent via email to the user and helps reset the password. + +#### Props + +- `onReset`: [optional] event called when the reset form is submitted successfully. + +#### Sample Usage + +```vue + +``` + +### `AuthorizerVerifyOtp` + +A component to render the OTP verification form. Make sure it is used as a descendant of `AuthorizerProvider`. + +#### Props + +- `email`: (required) user email address on which the OTP was sent. +- `onLogin`: [optional] event called when the verify OTP form is submitted successfully. +- `setView`: [optional] callback used internally by `AuthorizerRoot` to switch between `Login`/`Signup`/`ForgotPassword` views. Only needed if you are composing your own view switcher. +- `urlProps`: [optional] object to override the `state` query param used for the verify request. + +#### Sample Usage + +```vue + +``` + +## Updating styles + +Components in `@authorizerdev/authorizer-vue` are designed using css variables and come with a default stylesheet which declares these variables. You can override these css variables to update styling as per your theme: + +> Note: Given are the default values for the variables. + +```css +--authorizer-primary-color: #3b82f6; +--authorizer-primary-disabled-color: #60a5fa; +--authorizer-gray-color: #d1d5db; +--authorizer-white-color: #ffffff; +--authorizer-danger-color: #dc2626; +--authorizer-success-color: #10b981; +--authorizer-text-color: #374151; +--authorizer-fonts-font-stack: -apple-system, system-ui, sans-serif; +--authorizer-fonts-large-text: 18px; +--authorizer-fonts-medium-text: 14px; +--authorizer-fonts-small-text: 12px; +--authorizer-fonts-tiny-text: 10px; +--authorizer-radius-card: 5px; +--authorizer-radius-button: 5px; +--authorizer-radius-input: 5px; +``` + +## Examples + +Please check the [example repo](https://github.com/authorizerdev/examples) to see how to use this component library. diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 150bc2c..af16f7e 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -26,6 +26,11 @@ const config: Config = { onBrokenLinks: 'warn', onBrokenAnchors: 'warn', + markdown: { + mermaid: true, + }, + themes: ['@docusaurus/theme-mermaid'], + headTags: [ { tagName: 'meta', @@ -259,6 +264,9 @@ const config: Config = { colorMode: { respectPrefersColorScheme: true, }, + mermaid: { + theme: {light: 'neutral', dark: 'dark'}, + }, navbar: { title: 'Authorizer', logo: { diff --git a/package-lock.json b/package-lock.json index 6b66413..f34b578 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@docusaurus/core": "3.9.2", "@docusaurus/preset-classic": "3.9.2", + "@docusaurus/theme-mermaid": "^3.9.2", "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", "prism-react-renderer": "^2.3.0", @@ -162,7 +163,6 @@ "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.49.1.tgz", "integrity": "sha512-Nt9hri7nbOo0RipAsGjIssHkpLMHHN/P7QqENywAq5TLsoYDzUyJGny8FEiD/9KJUxtGH8blGpMedilI6kK3rA==", "license": "MIT", - "peer": true, "dependencies": { "@algolia/client-common": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", @@ -260,6 +260,19 @@ "node": ">= 14.0.0" } }, + "node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -288,7 +301,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1974,6 +1986,18 @@ "node": ">=6.9.0" } }, + "node_modules/@braintree/sanitize-url": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", + "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", + "license": "MIT" + }, + "node_modules/@chevrotain/types": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.2.tgz", + "integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==", + "license": "Apache-2.0" + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -2091,7 +2115,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -2114,7 +2137,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -2224,7 +2246,6 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -2646,7 +2667,6 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -3627,7 +3647,6 @@ "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.9.2.tgz", "integrity": "sha512-C5wZsGuKTY8jEYsqdxhhFOe1ZDjH0uIYJ9T/jebHwkyxqnr4wW0jTkB72OMqNjsoQRcb0JN3PcSeTwFlVgzCZg==", "license": "MIT", - "peer": true, "dependencies": { "@docusaurus/core": "3.9.2", "@docusaurus/logger": "3.9.2", @@ -3919,6 +3938,34 @@ "react-dom": "^18.0.0 || ^19.0.0" } }, + "node_modules/@docusaurus/theme-mermaid": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-mermaid/-/theme-mermaid-3.9.2.tgz", + "integrity": "sha512-5vhShRDq/ntLzdInsQkTdoKWSzw8d1jB17sNPYhA/KvYYFXfuVEGHLM6nrf8MFbV8TruAHDG21Fn3W4lO8GaDw==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/module-type-aliases": "3.9.2", + "@docusaurus/theme-common": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "mermaid": ">=11.6.0", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "@mermaid-js/layout-elk": "^0.1.9", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@mermaid-js/layout-elk": { + "optional": true + } + } + }, "node_modules/@docusaurus/theme-search-algolia": { "version": "3.9.2", "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.9.2.tgz", @@ -4085,6 +4132,23 @@ "@hapi/hoek": "^9.0.0" } }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.4.tgz", + "integrity": "sha512-b1S7B1k9ohZ+iNTi2ATxbRYG9fTrJmUT0rc46bvVnNxqNRGW7dyo/vRREwyniI5IRN2RSJHDcm+s3BjWrSAjHw==", + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.1.0", + "@iconify/types": "^2.0.0", + "import-meta-resolve": "^4.2.0" + } + }, "node_modules/@jest/schemas": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", @@ -4626,7 +4690,6 @@ "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", "license": "MIT", - "peer": true, "dependencies": { "@types/mdx": "^2.0.0" }, @@ -4639,6 +4702,15 @@ "react": ">=16" } }, + "node_modules/@mermaid-js/parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.2.0.tgz", + "integrity": "sha512-oYPyv8A4As1yH5Bx+04iQEQxXuIQDe0GKCNSRgao6z8AM9jixXIfP0vsppRLvGf+nKIOb9/LdpWA4YuJiVvESA==", + "license": "MIT", + "dependencies": { + "@chevrotain/types": "~11.1.2" + } + }, "node_modules/@noble/hashes": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", @@ -5090,7 +5162,6 @@ "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -5248,6 +5319,259 @@ "@types/node": "*" } }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.4.tgz", + "integrity": "sha512-UHYId5WTCx4L4YNel7NU00XUXXgvgpgZOvp10PuvsQENjMDXhh2RyFc0KBjO7B45ne4Ha1yVH7ii0vnzKkuzWA==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -5316,6 +5640,12 @@ "@types/send": "*" } }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, "node_modules/@types/gtag.js": { "version": "0.0.12", "resolved": "https://registry.npmjs.org/@types/gtag.js/-/gtag.js-0.0.12.tgz", @@ -5453,7 +5783,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -5553,6 +5882,13 @@ "@types/node": "*" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -5589,6 +5925,16 @@ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "license": "ISC" }, + "node_modules/@upsetjs/venn.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", + "integrity": "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==", + "license": "MIT", + "optionalDependencies": { + "d3-selection": "^3.0.0", + "d3-transition": "^3.0.1" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", @@ -5795,7 +6141,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5863,7 +6208,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -5909,7 +6253,6 @@ "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.49.1.tgz", "integrity": "sha512-X3Pp2aRQhg4xUC6PQtkubn5NpRKuUPQ9FPDQlx36SmpFwwH2N0/tw4c+NXV3nw3PsgeUs+BuWGP0gjz3TvENLQ==", "license": "MIT", - "peer": true, "dependencies": { "@algolia/abtesting": "1.15.1", "@algolia/client-abtesting": "5.49.1", @@ -6389,7 +6732,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -7184,6 +7526,15 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "license": "MIT" }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "license": "MIT", + "dependencies": { + "layout-base": "^1.0.0" + } + }, "node_modules/cosmiconfig": { "version": "8.3.6", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", @@ -7355,7 +7706,6 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -7502,172 +7852,698 @@ "engines": { "node": ">= 6" }, - "funding": { - "url": "https://github.com/sponsors/fb55" + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssdb": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-8.8.0.tgz", + "integrity": "sha512-QbLeyz2Bgso1iRlh7IpWk6OKa3lLNGXsujVjDMPl9rOZpxKeiG69icLpbLCFxeURwmcdIfZqQyhlooKJYM4f8Q==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + } + ], + "license": "MIT-0" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssnano": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-6.1.2.tgz", + "integrity": "sha512-rYk5UeX7VAM/u0lNqewCdasdtPK81CgX8wJFLEIXHbV2oldWRgJAsZrdhRXkV1NJzA2g850KiFm9mMU2HxNxMA==", + "license": "MIT", + "dependencies": { + "cssnano-preset-default": "^6.1.2", + "lilconfig": "^3.1.1" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/cssnano" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-preset-advanced": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cssnano-preset-advanced/-/cssnano-preset-advanced-6.1.2.tgz", + "integrity": "sha512-Nhao7eD8ph2DoHolEzQs5CfRpiEP0xa1HBdnFZ82kvqdmbwVBUr2r1QuQ4t1pi+D1ZpqpcO4T+wy/7RxzJ/WPQ==", + "license": "MIT", + "dependencies": { + "autoprefixer": "^10.4.19", + "browserslist": "^4.23.0", + "cssnano-preset-default": "^6.1.2", + "postcss-discard-unused": "^6.0.5", + "postcss-merge-idents": "^6.0.3", + "postcss-reduce-idents": "^6.0.3", + "postcss-zindex": "^6.0.2" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-preset-default": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-6.1.2.tgz", + "integrity": "sha512-1C0C+eNaeN8OcHQa193aRgYexyJtU8XwbdieEjClw+J9d94E41LwT6ivKH0WT+fYwYWB0Zp3I3IZ7tI/BbUbrg==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "css-declaration-sorter": "^7.2.0", + "cssnano-utils": "^4.0.2", + "postcss-calc": "^9.0.1", + "postcss-colormin": "^6.1.0", + "postcss-convert-values": "^6.1.0", + "postcss-discard-comments": "^6.0.2", + "postcss-discard-duplicates": "^6.0.3", + "postcss-discard-empty": "^6.0.3", + "postcss-discard-overridden": "^6.0.2", + "postcss-merge-longhand": "^6.0.5", + "postcss-merge-rules": "^6.1.1", + "postcss-minify-font-values": "^6.1.0", + "postcss-minify-gradients": "^6.0.3", + "postcss-minify-params": "^6.1.0", + "postcss-minify-selectors": "^6.0.4", + "postcss-normalize-charset": "^6.0.2", + "postcss-normalize-display-values": "^6.0.2", + "postcss-normalize-positions": "^6.0.2", + "postcss-normalize-repeat-style": "^6.0.2", + "postcss-normalize-string": "^6.0.2", + "postcss-normalize-timing-functions": "^6.0.2", + "postcss-normalize-unicode": "^6.1.0", + "postcss-normalize-url": "^6.0.2", + "postcss-normalize-whitespace": "^6.0.2", + "postcss-ordered-values": "^6.0.2", + "postcss-reduce-initial": "^6.1.0", + "postcss-reduce-transforms": "^6.0.2", + "postcss-svgo": "^6.0.3", + "postcss-unique-selectors": "^6.0.4" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-utils": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-4.0.2.tgz", + "integrity": "sha512-ZR1jHg+wZ8o4c3zqf1SIUSTIvm/9mU343FMR6Obe/unskbvpGhZOo1J6d/r8D1pzkRQYuwbcH3hToOuoA2G7oQ==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "license": "MIT", + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "license": "CC0-1.0" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/cytoscape": { + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.0.tgz", + "integrity": "sha512-62rNSrioXw93uliKFBwjukeQyeWwH2PqDrTac31r2P6464u3AUvTk0xS4LVvT251g7IgkFunrI48ZEZGjywSOg==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "license": "MIT", + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "license": "MIT" + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-dsv/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" } }, - "node_modules/cssdb": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-8.8.0.tgz", - "integrity": "sha512-QbLeyz2Bgso1iRlh7IpWk6OKa3lLNGXsujVjDMPl9rOZpxKeiG69icLpbLCFxeURwmcdIfZqQyhlooKJYM4f8Q==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - } - ], - "license": "MIT-0" - }, - "node_modules/cssesc": { + "node_modules/d3-selection": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", "engines": { - "node": ">=4" + "node": ">=12" } }, - "node_modules/cssnano": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-6.1.2.tgz", - "integrity": "sha512-rYk5UeX7VAM/u0lNqewCdasdtPK81CgX8wJFLEIXHbV2oldWRgJAsZrdhRXkV1NJzA2g850KiFm9mMU2HxNxMA==", - "license": "MIT", + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", "dependencies": { - "cssnano-preset-default": "^6.1.2", - "lilconfig": "^3.1.1" + "d3-path": "^3.1.0" }, "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/cssnano" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=12" } }, - "node_modules/cssnano-preset-advanced": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/cssnano-preset-advanced/-/cssnano-preset-advanced-6.1.2.tgz", - "integrity": "sha512-Nhao7eD8ph2DoHolEzQs5CfRpiEP0xa1HBdnFZ82kvqdmbwVBUr2r1QuQ4t1pi+D1ZpqpcO4T+wy/7RxzJ/WPQ==", - "license": "MIT", + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", "dependencies": { - "autoprefixer": "^10.4.19", - "browserslist": "^4.23.0", - "cssnano-preset-default": "^6.1.2", - "postcss-discard-unused": "^6.0.5", - "postcss-merge-idents": "^6.0.3", - "postcss-reduce-idents": "^6.0.3", - "postcss-zindex": "^6.0.2" + "d3-array": "2 - 3" }, "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=12" } }, - "node_modules/cssnano-preset-default": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-6.1.2.tgz", - "integrity": "sha512-1C0C+eNaeN8OcHQa193aRgYexyJtU8XwbdieEjClw+J9d94E41LwT6ivKH0WT+fYwYWB0Zp3I3IZ7tI/BbUbrg==", - "license": "MIT", + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", "dependencies": { - "browserslist": "^4.23.0", - "css-declaration-sorter": "^7.2.0", - "cssnano-utils": "^4.0.2", - "postcss-calc": "^9.0.1", - "postcss-colormin": "^6.1.0", - "postcss-convert-values": "^6.1.0", - "postcss-discard-comments": "^6.0.2", - "postcss-discard-duplicates": "^6.0.3", - "postcss-discard-empty": "^6.0.3", - "postcss-discard-overridden": "^6.0.2", - "postcss-merge-longhand": "^6.0.5", - "postcss-merge-rules": "^6.1.1", - "postcss-minify-font-values": "^6.1.0", - "postcss-minify-gradients": "^6.0.3", - "postcss-minify-params": "^6.1.0", - "postcss-minify-selectors": "^6.0.4", - "postcss-normalize-charset": "^6.0.2", - "postcss-normalize-display-values": "^6.0.2", - "postcss-normalize-positions": "^6.0.2", - "postcss-normalize-repeat-style": "^6.0.2", - "postcss-normalize-string": "^6.0.2", - "postcss-normalize-timing-functions": "^6.0.2", - "postcss-normalize-unicode": "^6.1.0", - "postcss-normalize-url": "^6.0.2", - "postcss-normalize-whitespace": "^6.0.2", - "postcss-ordered-values": "^6.0.2", - "postcss-reduce-initial": "^6.1.0", - "postcss-reduce-transforms": "^6.0.2", - "postcss-svgo": "^6.0.3", - "postcss-unique-selectors": "^6.0.4" + "d3-time": "1 - 3" }, "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=12" } }, - "node_modules/cssnano-utils": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-4.0.2.tgz", - "integrity": "sha512-ZR1jHg+wZ8o4c3zqf1SIUSTIvm/9mU343FMR6Obe/unskbvpGhZOo1J6d/r8D1pzkRQYuwbcH3hToOuoA2G7oQ==", - "license": "MIT", + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=12" } }, - "node_modules/csso": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", - "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", - "license": "MIT", + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", "dependencies": { - "css-tree": "~2.2.0" + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" }, "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" } }, - "node_modules/csso/node_modules/css-tree": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", - "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", - "license": "MIT", + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", "dependencies": { - "mdn-data": "2.0.28", - "source-map-js": "^1.0.1" + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" }, "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" + "node": ">=12" } }, - "node_modules/csso/node_modules/mdn-data": { - "version": "2.0.28", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", - "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", - "license": "CC0-1.0" + "node_modules/dagre-d3-es": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz", + "integrity": "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==", + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "lodash-es": "^4.17.21" + } }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "node_modules/dayjs": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", "license": "MIT" }, "node_modules/debounce": { @@ -7831,6 +8707,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delaunator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -7969,6 +8854,15 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "3.4.12", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.12.tgz", + "integrity": "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/domutils": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", @@ -8165,6 +9059,16 @@ "node": ">= 0.4" } }, + "node_modules/es-toolkit": { + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.49.0.tgz", + "integrity": "sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/esast-util-from-estree": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", @@ -8720,7 +9624,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -9202,6 +10105,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/hachure-fill": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", + "license": "MIT" + }, "node_modules/handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -9835,6 +10744,16 @@ "node": ">=8" } }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -9883,6 +10802,15 @@ "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", "license": "MIT" }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -10378,6 +11306,31 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/katex": { + "version": "0.16.47", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -10387,6 +11340,11 @@ "json-buffer": "3.0.1" } }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -10430,6 +11388,12 @@ "shell-quote": "^1.8.3" } }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "license": "MIT" + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -10505,6 +11469,12 @@ "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "license": "MIT" }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -10597,6 +11567,18 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/marked": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -11076,6 +12058,48 @@ "node": ">= 8" } }, + "node_modules/mermaid": { + "version": "11.16.0", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.16.0.tgz", + "integrity": "sha512-Zvm3kbstgdpvIJPPItlL7fppIZ3kibvc1oZIGxdvk9t6UFz6flv+Jw7FtRGKwfcI8OckmH04LqG6LlS6X4B1pA==", + "license": "MIT", + "dependencies": { + "@braintree/sanitize-url": "^7.1.2", + "@iconify/utils": "^3.0.2", + "@mermaid-js/parser": "^1.2.0", + "@types/d3": "^7.4.3", + "@upsetjs/venn.js": "^2.0.0", + "cytoscape": "^3.33.3", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.14", + "dayjs": "^1.11.20", + "dompurify": "^3.3.3", + "es-toolkit": "^1.45.1", + "katex": "^0.16.45", + "khroma": "^2.1.0", + "marked": "^16.3.0", + "roughjs": "^4.6.6", + "stylis": "^4.3.6", + "ts-dedent": "^2.2.0", + "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" + } + }, + "node_modules/mermaid/node_modules/uuid": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz", + "integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -13151,7 +14175,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -13440,6 +14463,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-manager-detector": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.8.0.tgz", + "integrity": "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==", + "license": "MIT" + }, "node_modules/param-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", @@ -13567,6 +14596,12 @@ "tslib": "^2.0.3" } }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", + "license": "MIT" + }, "node_modules/path-exists": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", @@ -13665,6 +14700,22 @@ "node": ">=16.0.0" } }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", + "license": "MIT" + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", + "license": "MIT", + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, "node_modules/postcss": { "version": "8.5.8", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", @@ -13684,7 +14735,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -14588,7 +15638,6 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -15405,7 +16454,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -15415,7 +16463,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -15471,7 +16518,6 @@ "resolved": "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-6.0.0.tgz", "integrity": "sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ==", "license": "MIT", - "peer": true, "dependencies": { "@types/react": "*" }, @@ -15500,7 +16546,6 @@ "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.12.13", "history": "^4.9.0", @@ -16100,6 +17145,24 @@ "node": ">=0.10.0" } }, + "node_modules/robust-predicates": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", + "license": "Unlicense" + }, + "node_modules/roughjs": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", + "license": "MIT", + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, "node_modules/rtlcss": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.3.0.tgz", @@ -16153,6 +17216,12 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -16985,6 +18054,12 @@ "postcss": "^8.4.31" } }, + "node_modules/stylis": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz", + "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -17183,6 +18258,15 @@ "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", "license": "MIT" }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinypool": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", @@ -17258,12 +18342,20 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/ts-dedent": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.3.0.tgz", + "integrity": "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==", + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tsyringe": { "version": "4.10.0", @@ -17344,7 +18436,6 @@ "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -17686,7 +18777,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -17885,7 +18975,6 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.3.tgz", "integrity": "sha512-LLBBA4oLmT7sZdHiYE/PeVuifOxYyE2uL/V+9VQP7YSYdJU7bSf7H8bZRRxW8kEPMkmVjnrXmoR3oejIdX0xbg==", "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", diff --git a/package.json b/package.json index 2b5e59b..82884ee 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "dependencies": { "@docusaurus/core": "3.9.2", "@docusaurus/preset-classic": "3.9.2", + "@docusaurus/theme-mermaid": "^3.9.2", "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", "prism-react-renderer": "^2.3.0", diff --git a/sidebars.ts b/sidebars.ts index 121ad3a..f856202 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -39,8 +39,10 @@ const sidebars: SidebarsConfig = { label: 'Enterprise', items: [ 'enterprise/organizations', + 'enterprise/org-domains', 'enterprise/org-sso-oidc', 'enterprise/org-saml', + 'enterprise/saml-idp', 'enterprise/scim', 'enterprise/token-exchange', 'enterprise/workload-identity', @@ -108,6 +110,7 @@ const sidebars: SidebarsConfig = { label: 'authorizer-go', items: [ 'sdks/authorizer-go/index', + 'sdks/authorizer-go/functions', 'sdks/authorizer-go/example', 'sdks/authorizer-go/admin', ],