diff --git a/api-gateways/apigee/README.md b/api-gateways/apigee/README.md index b2d59e5..d8be2c0 100644 --- a/api-gateways/apigee/README.md +++ b/api-gateways/apigee/README.md @@ -9,7 +9,26 @@ ## Pattern -Apigee intercepts the API request → SharedFlow runs VerifyJWT against `https://.projects.oryapis.com/.well-known/jwks.json` → on valid JWT, proxy to backend; on invalid, return 401. Apigee caches the JWKS automatically. +Apigee intercepts the API request → SharedFlow runs VerifyJWT against `https://.projects.oryapis.com/.well-known/jwks.json` → on valid JWT, proxy to backend; on invalid, return 401. Apigee caches the JWKS for 300 seconds. + +## Prerequisites for JWT validation + +Ory issues opaque access tokens (`ory_at_…`) by default, and the `aud` claim is empty unless the client requests an audience. To issue JWTs that Apigee can verify: + +```bash +# 1. Switch the project to JWT access tokens. +ory patch oauth2-config --project --replace '/strategies/access_token="jwt"' + +# 2. Create a client that may request your API's audience. +ory create oauth2-client --project --name "" \ + --grant-type client_credentials --audience https://api.example.com + +# 3. Request the audience when you get a token. +curl https://.projects.oryapis.com/oauth2/token -u ":" \ + -d grant_type=client_credentials -d audience=https://api.example.com +``` + +The token's `iss` claim is `https://.projects.oryapis.com`, without a trailing slash. The JWKS can hold more than one key, so select the key that matches the token's `kid` header. For the client credentials grant, `sub` is the client ID. See [JWT access tokens](https://www.ory.com/docs/oauth2-oidc/jwt-access-token). ## Setup outline @@ -17,13 +36,13 @@ Apigee intercepts the API request → SharedFlow runs VerifyJWT against `https:/ - Algorithm: RS256. - PublicKey JWKS URL: `https://.projects.oryapis.com/.well-known/jwks.json`. - Issuer: `https://.projects.oryapis.com`. - - Audience: your API's audience identifier (configured in Ory's OAuth2 client). + - Audience: the audience you set on the Ory OAuth2 client, for example `https://api.example.com`. 2. Attach the SharedFlow to your API proxy's PreFlow. 3. In your application, configure clients to call the API with the bearer token issued by Ory's OAuth2 server (Hydra). ## Notable -- JWKS rotation: Apigee caches the JWKS by URL; tune `CacheTTLInSeconds` on the VerifyJWT policy to balance freshness vs. KMS calls. +- JWKS rotation: Apigee caches a JWKS fetched from a URL for 300 seconds and selects the key that matches the token's `kid` header. - Apigee's KVM (Key Value Map) can hold per-environment audience values without redeploying the shared flow. - For finer-grained authorization (claim-based access control), add an `AssignMessage` or `JavaScript` policy after VerifyJWT to inspect the JWT claims. diff --git a/api-gateways/apigee/registry.entry.yaml b/api-gateways/apigee/registry.entry.yaml index 5a48925..76d6af8 100644 --- a/api-gateways/apigee/registry.entry.yaml +++ b/api-gateways/apigee/registry.entry.yaml @@ -15,8 +15,8 @@ useCase: >- An enterprise API team running Apigee wants OAuth2 access tokens issued by Ory (Hydra) validated at the gateway layer so backend services don't need to re-validate every request. coreFunctionality: |- - - SharedFlow with VerifyJWT policy: PublicKey JWKS URL points at Ory's https://.projects.oryapis.com/.well-known/jwks.json; Issuer = the Ory project URL; Audience = the API's audience identifier from Ory's OAuth2 client. - - Algorithm RS256; Apigee caches the JWKS by URL — tune CacheTTLInSeconds for the freshness/KMS-call trade-off. + - SharedFlow with VerifyJWT policy: PublicKey JWKS URL points at Ory's https://.projects.oryapis.com/.well-known/jwks.json; Issuer = https://.projects.oryapis.com (no trailing slash); Audience = the audience set on the Ory OAuth2 client and requested in the token request. Requires JWT access tokens enabled on the project. + - Algorithm RS256; Apigee caches a JWKS fetched from a URL for 300 seconds and selects the key by the token's kid. - Apigee KVM holds per-environment audience values without redeploying the shared flow. - For claim-based authorization (per-scope or per-role checks), add an AssignMessage or JavaScript policy after VerifyJWT to inspect the JWT claims. - SharedFlow attached to the API proxy's PreFlow runs before every request. diff --git a/api-gateways/aws-api-gateway/README.md b/api-gateways/aws-api-gateway/README.md index 752852e..602cbbb 100644 --- a/api-gateways/aws-api-gateway/README.md +++ b/api-gateways/aws-api-gateway/README.md @@ -11,18 +11,38 @@ API Gateway invokes the Lambda authorizer on each request → authorizer fetches and caches Ory's JWKS → verifies signature, issuer, audience → returns an IAM policy (Allow or Deny). API Gateway caches the policy by token for `authorizerResultTtlInSeconds` (typically 300s) to avoid invoking the authorizer on every request. +## Prerequisites for JWT validation + +Ory issues opaque access tokens (`ory_at_…`) by default, and the `aud` claim is empty unless the client requests an audience. To issue JWTs that API Gateway can verify: + +```bash +# 1. Switch the project to JWT access tokens. +ory patch oauth2-config --project --replace '/strategies/access_token="jwt"' + +# 2. Create a client that may request your API's audience. +ory create oauth2-client --project --name "" \ + --grant-type client_credentials --audience https://api.example.com + +# 3. Request the audience when you get a token. +curl https://.projects.oryapis.com/oauth2/token -u ":" \ + -d grant_type=client_credentials -d audience=https://api.example.com +``` + +The token's `iss` claim is `https://.projects.oryapis.com`, without a trailing slash. The JWKS can hold more than one key, so select the key that matches the token's `kid` header. For the client credentials grant, `sub` is the client ID. See [JWT access tokens](https://www.ory.com/docs/oauth2-oidc/jwt-access-token). + ## Setup outline 1. Deploy a Lambda authorizer (Node.js or Python) that: - Pulls JWT from `Authorization: Bearer ` header. - Verifies against JWKS at `https://.projects.oryapis.com/.well-known/jwks.json` (cache in module scope, refresh on `kid` miss). + - Checks that `iss` is `https://.projects.oryapis.com`, that `aud` contains your API's audience, and that `exp` is in the future. Pin the algorithm to `RS256`. - Returns the IAM policy + principalId (= JWT `sub`). 2. Wire as a **Token authorizer** (REST API) or **Lambda authorizer** (HTTP API) on the API. 3. Configure `authorizerResultTtlInSeconds` (REST) or `enableSimpleResponses: false` + `resultsCacheTtlInSeconds` (HTTP) for caching. ## Notable -- **HTTP API** has native JWT authorizers that don't need a Lambda — point them at Ory's issuer URL directly. Lambda authorizer is only needed when you require custom validation logic beyond standard JWT verification. +- **HTTP API** has native JWT authorizers that don't need a Lambda — set the issuer to `https://.projects.oryapis.com` and the audience to your API's audience. Lambda authorizer is only needed when you require custom validation logic beyond standard JWT verification. - The authorizer's IAM policy controls the **resource pattern** that's allowed; emit a wildcard policy for the API to keep things simple, or a tight per-method policy for stricter authorization. - Cold-start latency matters — keep the authorizer minimal; ship JWKS verification with a lean library like `aws-jwt-verify`. diff --git a/api-gateways/aws-api-gateway/registry.entry.yaml b/api-gateways/aws-api-gateway/registry.entry.yaml index b0e0f09..2fb6bb7 100644 --- a/api-gateways/aws-api-gateway/registry.entry.yaml +++ b/api-gateways/aws-api-gateway/registry.entry.yaml @@ -16,7 +16,7 @@ useCase: >- backend services don't need to re-validate every request and can read the trusted `sub` from request context. coreFunctionality: |- - REST API: Lambda authorizer (Node.js / Python) verifies bearer JWT against https://.projects.oryapis.com/.well-known/jwks.json with kid-based key selection; returns IAM policy + principalId (= JWT sub). - - HTTP API: native JWT authorizer pointing directly at the Ory issuer URL — no Lambda required for standard verification. + - HTTP API: native JWT authorizer with issuer https://.projects.oryapis.com and your API's audience — no Lambda required for standard verification. Requires JWT access tokens and an audience requested in the token request. - Caching: REST uses authorizerResultTtlInSeconds (default 300s); HTTP uses resultsCacheTtlInSeconds. Tune for cold-start vs. revocation latency. - Use Lambda authorizer over native JWT when custom claim-based authorization logic is needed beyond signature/issuer/audience checks. - Cold start matters — keep the authorizer minimal; aws-jwt-verify is a lean JWKS library. diff --git a/api-gateways/envoy/README.md b/api-gateways/envoy/README.md index 5c66927..b39550c 100644 --- a/api-gateways/envoy/README.md +++ b/api-gateways/envoy/README.md @@ -9,6 +9,25 @@ > **Status: community / proposed — shape still being defined.** This entry documents the standard patterns; a full reference configuration may land later. +## Prerequisites for JWT validation + +Ory issues opaque access tokens (`ory_at_…`) by default, and the `aud` claim is empty unless the client requests an audience. To issue JWTs that Envoy can verify: + +```bash +# 1. Switch the project to JWT access tokens. +ory patch oauth2-config --project --replace '/strategies/access_token="jwt"' + +# 2. Create a client that may request your API's audience. +ory create oauth2-client --project --name "" \ + --grant-type client_credentials --audience https://api.example.com + +# 3. Request the audience when you get a token. +curl https://.projects.oryapis.com/oauth2/token -u ":" \ + -d grant_type=client_credentials -d audience=https://api.example.com +``` + +The token's `iss` claim is `https://.projects.oryapis.com`, without a trailing slash. The JWKS can hold more than one key, so select the key that matches the token's `kid` header. For the client credentials grant, `sub` is the client ID. See [JWT access tokens](https://www.ory.com/docs/oauth2-oidc/jwt-access-token). + ## Two paths ### Path A — OAuth2 access token validation (`jwt_authn` filter) @@ -18,8 +37,9 @@ For Ory Hydra-issued JWTs: 1. Add the **`jwt_authn`** HTTP filter to the listener's filter chain. 2. Define a provider with: - `remote_jwks.http_uri` → `https://.projects.oryapis.com/.well-known/jwks.json`. - - `issuer` → your Ory project URL. - - `audiences` → the API's audience identifier from the Ory OAuth2 client. + - `issuer` → `https://.projects.oryapis.com`, without a trailing slash. + - `audiences` → the audience you set on the Ory OAuth2 client, for example `https://api.example.com`. + - `remote_jwks.http_uri.cluster` → a cluster that reaches the project over TLS **with certificate validation**. Envoy doesn't verify upstream certificates unless the cluster's `UpstreamTlsContext` sets `common_tls_context.validation_context` (a `trusted_ca` bundle plus a `match_typed_subject_alt_names` DNS matcher for the project hostname) and `sni`. See the [docs page](https://www.ory.com/docs/integrates-with/api-gateways/envoy) for a complete listener and cluster example. 3. Envoy caches the JWKS and refreshes on `cache_duration`. 4. Optionally forward verified claims upstream via `payload_in_metadata` for downstream authorization. @@ -29,7 +49,7 @@ For end-user session cookies / tokens from Kratos: 1. Add the **`ext_authz`** HTTP filter pointing at Ory Oathkeeper (or a small validator service). 2. The authorizer calls `https://.projects.oryapis.com/sessions/whoami` with the session cookie. -3. On 200, inject identity headers downstream (`X-User-Id`, `X-User-Email`) and forward; on 401, reject at the edge. +3. On 200, inject identity headers downstream (`X-User-Id`, `X-User-Email`) and forward; on 401, reject at the edge. `whoami` sets only the `X-Kratos-Authenticated-Identity-Id` response header, so the authorizer reads other identity fields from the response body. Configure `ext_authz` to overwrite or remove identity headers the client sends. ## Notable diff --git a/api-gateways/envoy/registry.entry.yaml b/api-gateways/envoy/registry.entry.yaml index e77820b..7d2a7fc 100644 --- a/api-gateways/envoy/registry.entry.yaml +++ b/api-gateways/envoy/registry.entry.yaml @@ -17,8 +17,8 @@ useCase: >- validated at the proxy so upstream services receive a trusted identity (or a 401 from the edge) without re-validating every request themselves. coreFunctionality: |- - - Path A (Hydra JWTs): `jwt_authn` HTTP filter with a remote_jwks pointing at https://.projects.oryapis.com/.well-known/jwks.json; set issuer to the Ory project URL and audiences to the API's audience. Envoy caches the JWKS and refreshes on the configured interval. - - Path B (Kratos sessions): `ext_authz` HTTP filter calls Ory Oathkeeper (or a small validator) which hits /sessions/whoami; on success Envoy forwards identity headers (X-User-Id, X-User-Email) upstream, on failure returns 401 at the edge. + - Path A (Hydra JWTs): `jwt_authn` HTTP filter with a remote_jwks pointing at https://.projects.oryapis.com/.well-known/jwks.json; set issuer to https://.projects.oryapis.com (no trailing slash) and audiences to the audience requested in the token request. The remote_jwks cluster must use an UpstreamTlsContext with sni and a validation_context (trusted_ca plus a DNS SAN matcher). Requires JWT access tokens enabled on the Ory project. Envoy caches the JWKS and refreshes on the configured interval. + - Path B (Kratos sessions): `ext_authz` HTTP filter calls Ory Oathkeeper (or a small validator) which hits /sessions/whoami; on success Envoy forwards identity headers (X-User-Id, X-User-Email) read from the whoami response body upstream, on failure returns 401 at the edge. - Verified JWT claims can be forwarded upstream via the filter's payload_in_metadata / forwarding settings for downstream authorization. - Works the same whether Envoy is standalone, an Istio sidecar/gateway, or the data plane under a managed gateway. - Tetrate Agent Router Enterprise (see agent-identity/tetrate) is built on Envoy AI Gateway and applies this pattern to AI-agent traffic. diff --git a/api-gateways/kong/README.md b/api-gateways/kong/README.md index 2212855..89570cc 100644 --- a/api-gateways/kong/README.md +++ b/api-gateways/kong/README.md @@ -7,34 +7,64 @@ **Type:** config (Kong plugin configuration — no webhook code) **Docs page:** [ory.com/docs/integrates-with/api-gateways/kong](https://www.ory.com/docs/integrates-with/api-gateways/kong) -## Two paths +## Prerequisites for JWT validation -### Path A — OAuth2 access token validation (Kong's `jwt` plugin) +Ory issues opaque access tokens (`ory_at_…`) by default, and the `aud` claim is empty unless the client requests an audience. To issue JWTs that Kong can verify: -For Ory Hydra-issued JWTs: +```bash +# 1. Switch the project to JWT access tokens. +ory patch oauth2-config --project --replace '/strategies/access_token="jwt"' -1. Enable the **jwt** plugin on the relevant Service / Route. -2. Create a Kong **Consumer** representing the Ory issuer. -3. Add a **JWT credential** to the consumer with: - - `algorithm: RS256`. - - `rsa_public_key`: from the Ory JWKS (or use the `jwt-keycloak` / `jwt-signer` community plugins for JWKS auto-fetch). -4. Configure the plugin's `claims_to_verify` (typically `exp`) and `key_claim_name` (typically `iss` or `kid`). +# 2. Create a client that may request your API's audience. +ory create oauth2-client --project --name "" \ + --grant-type client_credentials --audience https://api.example.com -### Path B — Kratos session validation (custom plugin) +# 3. Request the audience when you get a token. +curl https://.projects.oryapis.com/oauth2/token -u ":" \ + -d grant_type=client_credentials -d audience=https://api.example.com +``` + +The token's `iss` claim is `https://.projects.oryapis.com`, without a trailing slash. The JWKS can hold more than one key, so the key must match the token's `kid` header. For the client credentials grant, `sub` is the client ID. See [JWT access tokens](https://www.ory.com/docs/oauth2-oidc/jwt-access-token). + +## Path A — OAuth2 access token validation (Kong's `jwt` plugin) + +[`config/kong.yaml`](config/kong.yaml) and [`config/docker-compose.yaml`](config/docker-compose.yaml) run Kong 3.9 in DB-less mode. The open source **jwt** plugin verifies the RS256 signature and `exp`. It does not fetch JWKS, so you store the public key on a Consumer's JWT credential. A **post-function** then requires your API's audience and `nbf`, and sets `X-User-Id` to the token's `sub`. + +1. Convert the signing key from the JWKS to PEM. This picks the key whose `kid` matches an access token's header: + + ```bash + export ACCESS_TOKEN= + curl -s https://.projects.oryapis.com/.well-known/jwks.json | node -e ' + const { createPublicKey } = require("node:crypto") + const { kid } = JSON.parse(Buffer.from(process.env.ACCESS_TOKEN.split(".")[0], "base64url")) + const { keys } = JSON.parse(require("node:fs").readFileSync(0, "utf8")) + const jwk = keys.find((k) => k.kid === kid) + process.stdout.write(createPublicKey({ key: jwk, format: "jwk" }).export({ type: "spki", format: "pem" })) + ' + ``` + +2. In `kong.yaml`, paste the complete PEM under `rsa_public_key`, set `key` to the issuer (`https://.projects.oryapis.com`), and replace `https://api.example.com` with your audience. The plugin selects the credential by the token's `iss` claim, because `key_claim_name` defaults to `iss`. +3. Allow the post-function to load Kong's JWT parser in the Lua sandbox: `untrusted_lua_sandbox_requires = kong.plugins.jwt.jwt_parser`. The compose file sets it with `KONG_UNTRUSTED_LUA_SANDBOX_REQUIRES`. If you already allow other modules, append this one. +4. `docker compose up` from `config/`. + +Keep both plugins on every protected route and don't enable anonymous access. Post-Function runs after other access plugins, so don't combine this setup with plugins that produce a response, such as Proxy Cache. + +Kong selects the credential by `iss`, not by `kid`, and each credential holds one key. When Ory signs tokens with a different key from the JWKS, Kong rejects them until you update `rsa_public_key`. To pick up keys automatically, use Kong Enterprise's **OpenID Connect** plugin — see [`config/kong-oidc-enterprise.yaml`](config/kong-oidc-enterprise.yaml). + +## Path B — Kratos session validation (custom plugin) For end-user session cookies / tokens from Kratos: -1. Use Kong's **request-transformer** + **pre-function** plugins (or a small custom plugin) to call `https://.projects.oryapis.com/sessions/whoami` with the user's session cookie. -2. On 200, inject identity headers downstream (`X-User-Id`, `X-User-Email`) and forward. +1. Write a small custom plugin or a `pre-function` that calls `https://.projects.oryapis.com/sessions/whoami` with the user's session cookie or token. +2. On 200, set identity headers downstream (`X-User-Id`, `X-User-Email`) from the response body and forward. Always overwrite or clear these headers so a client can't supply them. 3. On 401, return 401 without forwarding. -Kong Enterprise's **OIDC** plugin handles much of Path A automatically with JWKS auto-discovery. +`whoami` sets only the `X-Kratos-Authenticated-Identity-Id` response header, so read other identity fields from the response body. ## Notable -- Kong supports **JWKS auto-refresh** via `jwt-signer` or `jwt-keycloak` community plugins — preferred over manually loading public keys. -- For Kratos session validation, cache the `whoami` response per session cookie to avoid hammering Ory on every request (Kong's `proxy-cache` plugin or a custom Redis-backed cache). -- Rate-limit at the gateway, not the backend — Kong's `rate-limiting` plugin keyed by the validated `sub` claim is the cleanest pattern. +- To avoid calling Ory on every request, cache the `whoami` result per session inside your plugin. Kong's `proxy-cache` plugin caches upstream responses, not requests a plugin makes. +- With the **jwt** plugin, every token from one issuer maps to the same Consumer, so `rate-limiting` with `limit_by: consumer` limits all callers together, not each `sub`. ## Status diff --git a/api-gateways/kong/config/docker-compose.yaml b/api-gateways/kong/config/docker-compose.yaml index 0b608e0..bf453f9 100644 --- a/api-gateways/kong/config/docker-compose.yaml +++ b/api-gateways/kong/config/docker-compose.yaml @@ -3,21 +3,20 @@ # This runs Kong in DB-less declarative mode with the JWT plugin # configured to validate Ory-issued tokens. -version: "3.8" - services: kong: - image: kong:3.6 + image: kong:3.9 environment: KONG_DATABASE: "off" KONG_DECLARATIVE_CONFIG: /etc/kong/kong.yaml + KONG_UNTRUSTED_LUA: sandbox + KONG_UNTRUSTED_LUA_SANDBOX_REQUIRES: kong.plugins.jwt.jwt_parser KONG_PROXY_ACCESS_LOG: /dev/stdout KONG_ADMIN_ACCESS_LOG: /dev/stdout KONG_PROXY_ERROR_LOG: /dev/stderr KONG_ADMIN_ERROR_LOG: /dev/stderr - KONG_ADMIN_LISTEN: "0.0.0.0:8001" + KONG_ADMIN_LISTEN: "127.0.0.1:8001" ports: - "8000:8000" # Proxy - - "8001:8001" # Admin API volumes: - ./kong.yaml:/etc/kong/kong.yaml:ro diff --git a/api-gateways/kong/config/kong-oidc-enterprise.yaml b/api-gateways/kong/config/kong-oidc-enterprise.yaml index d8b2a8d..2625924 100644 --- a/api-gateways/kong/config/kong-oidc-enterprise.yaml +++ b/api-gateways/kong/config/kong-oidc-enterprise.yaml @@ -1,10 +1,11 @@ -# Kong Enterprise — OpenID Connect Plugin Configuration for Ory Hydra +# Kong Enterprise — OpenID Connect plugin configuration for Ory JWT access tokens # -# This uses Kong's OIDC plugin (Enterprise only) for full OpenID Connect -# integration with Ory Hydra. It handles token validation, introspection, -# and automatic token refresh. +# The OpenID Connect plugin (Enterprise only) discovers Ory's JWKS from the +# issuer and refreshes keys automatically, so it handles key rotation that the +# open source jwt plugin cannot. It validates bearer JWTs locally. # -# Replace YOUR_ORY_PROJECT with your actual Ory Network project slug. +# Replace YOUR_ORY_PROJECT with your Ory Network project slug and +# https://api.example.com with your API's audience. _format_version: "3.0" @@ -18,48 +19,17 @@ services: plugins: - name: openid-connect config: - # Ory Hydra's OIDC discovery endpoint - issuer: https://YOUR_ORY_PROJECT.projects.oryapis.com/ - - # Client credentials for token introspection - # IMPORTANT: Store these in Kong's vault or env vars - client_id: - - "${KONG_ORY_CLIENT_ID}" - client_secret: - - "${KONG_ORY_CLIENT_SECRET}" - - # Token validation methods (in priority order) + issuer: https://YOUR_ORY_PROJECT.projects.oryapis.com + issuers_allowed: + - https://YOUR_ORY_PROJECT.projects.oryapis.com auth_methods: - - bearer # Validate Bearer tokens - - session # Validate session cookies - - # Where to find the token + - bearer bearer_token_param_type: - - header # Authorization: Bearer - - # What to do with the validated identity - consumer_claim: - - sub # Map JWT subject to Kong consumer - - # Headers to set for upstream services + - header + audience_required: + - https://api.example.com upstream_headers_claims: - - sub:X-User-Id - - email:X-User-Email - - # Scopes required for access - scopes_required: - - openid - - # Token introspection endpoint (Ory Hydra) - introspection_endpoint: https://YOUR_ORY_PROJECT.projects.oryapis.com/admin/oauth2/introspect - - # JWKS endpoint for local JWT validation - # (faster than introspection, but doesn't check revocation) - token_endpoint_auth_method: client_secret_basic - - # Cache JWKS for 60 minutes + - sub + upstream_headers_names: + - X-User-Id cache_ttl: 3600 - - # Redirect unauthenticated users to login - login_redirect_mode: query - unauthorized_redirect_uri: https://your-app.com/login diff --git a/api-gateways/kong/config/kong.yaml b/api-gateways/kong/config/kong.yaml index 0c0cfac..1d62695 100644 --- a/api-gateways/kong/config/kong.yaml +++ b/api-gateways/kong/config/kong.yaml @@ -1,30 +1,33 @@ -# Kong Declarative Configuration for Ory Token Validation +# Kong declarative configuration for validating Ory-issued JWT access tokens. # -# This configures Kong to validate Ory-issued JWTs on protected routes -# using the built-in JWT plugin with Ory's JWKS endpoint. +# Kong's open source jwt plugin does not fetch JWKS. It verifies the RS256 +# signature with the PEM public key stored on the Consumer's JWT credential +# and selects that credential by the token's `iss` claim. The post-function +# then requires the API audience on the same token the jwt plugin accepted. # -# Two approaches are shown: -# 1. JWT plugin (open-source) — validates JWT signatures via JWKS -# 2. OpenID Connect plugin (Enterprise) — full OIDC flow with Ory Hydra -# -# Replace YOUR_ORY_PROJECT with your actual Ory Network project slug. +# Before you load this file: +# 1. Switch the Ory project to JWT access tokens and issue tokens with an +# audience (see the README). +# 2. Replace YOUR_ORY_PROJECT with your project slug and +# https://api.example.com with your API's audience. +# 3. Paste the complete PEM of the JWKS key that signs your tokens under +# rsa_public_key (see the README for a conversion command). +# 4. Allow the post-function to load the JWT parser in the Lua sandbox with +# untrusted_lua_sandbox_requires = kong.plugins.jwt.jwt_parser +# (KONG_UNTRUSTED_LUA_SANDBOX_REQUIRES in docker-compose.yaml). _format_version: "3.0" -# === Approach 1: JWT Plugin (Kong Open Source) === -# -# This approach validates JWT access tokens issued by Ory Hydra. -# The JWT plugin fetches the public keys from Ory's JWKS endpoint -# and verifies the token signature, expiration, and claims. - consumers: - - username: ory-jwt-consumer + - username: ory jwt_secrets: - algorithm: RS256 - # Ory's JWKS URL — Kong fetches public keys from here - # For Ory Network: https://YOUR_ORY_PROJECT.projects.oryapis.com/.well-known/jwks.json key: https://YOUR_ORY_PROJECT.projects.oryapis.com - secret: "unused-for-RS256" + secret: unused-for-rs256 + rsa_public_key: | + -----BEGIN PUBLIC KEY----- + REPLACE-WITH-THE-COMPLETE-PEM-FROM-THE-ORY-JWKS + -----END PUBLIC KEY----- services: - name: protected-api @@ -37,14 +40,54 @@ services: plugins: - name: jwt config: - # Where to find the JWT in the request header_names: - Authorization - # Claims to validate + uri_param_names: [] + cookie_names: [] claims_to_verify: - exp - # Forward decoded claims as headers to upstream - run_on_preflight: false + - name: post-function + config: + access: + - | + local token = kong.ctx.shared.authenticated_jwt_token + if type(token) ~= "string" then + return kong.response.exit(401, { message = "Unauthorized" }) + end + + local jwt = require("kong.plugins.jwt.jwt_parser"):new(token) + if not jwt then + return kong.response.exit(401, { message = "Unauthorized" }) + end + + local nbf = jwt.claims.nbf + if nbf ~= nil and (type(nbf) ~= "number" or nbf > ngx.time()) then + return kong.response.exit(401, { message = "Unauthorized" }) + end + + local expected = "https://api.example.com" + local audience = jwt.claims.aud + local allowed = audience == expected + if type(audience) == "table" then + for _, value in ipairs(audience) do + if type(value) ~= "string" then + return kong.response.exit(403, { message = "Forbidden" }) + end + if value == expected then + allowed = true + end + end + end + + if not allowed then + return kong.response.exit(403, { message = "Forbidden" }) + end + + local sub = jwt.claims.sub + if type(sub) ~= "string" then + return kong.response.exit(403, { message = "Forbidden" }) + end + kong.service.request.set_header("X-User-Id", sub) - name: public-api url: http://your-backend:8080 @@ -54,39 +97,8 @@ services: - /public - /health strip_path: false - -# === Post-authentication header injection === -# Use the post-function plugin to extract JWT claims and set upstream headers - - - name: protected-api-with-headers - url: http://your-backend:8080 - routes: - - name: api-headers-route - paths: - - /api/v2 - strip_path: false plugins: - - name: jwt - config: - header_names: - - Authorization - claims_to_verify: - - exp - - name: post-function + - name: pre-function config: access: - - | - -- Extract JWT claims and set upstream headers - local jwt_claims = kong.ctx.shared.authenticated_jwt_token - if jwt_claims then - local cjson = require("cjson") - local claims = cjson.decode( - ngx.decode_base64(jwt_claims:match("%.(.+)%."))) - kong.service.request.set_header("X-User-Id", claims.sub or "") - kong.service.request.set_header("X-User-Email", - claims.email or claims.ext and claims.ext.email or "") - if claims.ext and claims.ext.subscription_plan then - kong.service.request.set_header("X-User-Plan", - claims.ext.subscription_plan) - end - end + - kong.service.request.clear_header("X-User-Id") diff --git a/api-gateways/kong/registry.entry.yaml b/api-gateways/kong/registry.entry.yaml index a1e2968..0f296f4 100644 --- a/api-gateways/kong/registry.entry.yaml +++ b/api-gateways/kong/registry.entry.yaml @@ -8,18 +8,19 @@ category: api-gateways type: config maintainedBy: Community contributors description: >- - Validate Ory-issued JWTs or Kratos session cookies at the Kong API Gateway layer. JWTs via Kong's stock - `jwt` plugin (or jwt-signer / jwt-keycloak community plugins for JWKS auto-refresh); session validation via - request-transformer + pre-function calling /sessions/whoami. Documented at ory.com/docs/integrates-with/api-gateways/kong — universal pattern. + Validate Ory-issued JWTs or Kratos session cookies at the Kong API Gateway layer. JWTs via Kong's open source + `jwt` plugin plus a post-function audience check (or Kong Enterprise's OpenID Connect plugin for JWKS + auto-refresh); session validation via a custom plugin or pre-function calling /sessions/whoami. Documented at + ory.com/docs/integrates-with/api-gateways/kong — universal pattern. useCase: >- A team running APIs behind Kong wants Ory-issued OAuth2 access tokens (Hydra) or Kratos session cookies validated at the gateway so backend services don't need to re-validate every request. coreFunctionality: |- - - Path A (Hydra JWTs): Kong jwt plugin with RS256 algorithm, public key from Ory JWKS (prefer jwt-signer or jwt-keycloak community plugins for JWKS auto-fetch over manual key loading). Configure claims_to_verify (typically exp) and key_claim_name. - - Path B (Kratos sessions): request-transformer + pre-function plugins (or a small custom plugin) call https://.projects.oryapis.com/sessions/whoami with the session cookie; on 200 inject X-User-Id / X-User-Email headers downstream. - - Kong Enterprise's OIDC plugin handles Path A automatically with JWKS auto-discovery. - - Caching: cache whoami response per session cookie via Kong's proxy-cache plugin or a Redis-backed custom cache to avoid hammering Ory. - - Rate-limit at the gateway keyed by the validated `sub` claim — Kong's rate-limiting plugin is the cleanest pattern. + - Prerequisite: Ory issues opaque access tokens by default; switch the project to JWT access tokens and request an audience. + - Path A (Hydra JWTs): Kong jwt plugin with an RS256 Consumer credential keyed by the issuer and holding the PEM public key converted from the Ory JWKS; claims_to_verify exp. A post-function requires the API audience and nbf and sets X-User-Id from sub. Reference kong.yaml and docker-compose.yaml for Kong 3.9. + - The open source jwt plugin does not fetch JWKS, so rotate rsa_public_key when Ory's signing key changes. Kong Enterprise's OpenID Connect plugin discovers and refreshes the JWKS automatically (kong-oidc-enterprise.yaml). + - Path B (Kratos sessions): a custom plugin or pre-function calls https://.projects.oryapis.com/sessions/whoami with the session cookie or token; on 200 set X-User-Id / X-User-Email from the response body. + - Caching: cache the whoami result per session inside the plugin; Kong's proxy-cache plugin caches upstream responses, not plugin requests. - Status: community/proposed — documented at ory.com/docs/integrates-with/api-gateways/kong. oryMechanism: - standards diff --git a/api-gateways/traefik/README.md b/api-gateways/traefik/README.md index 577f1f3..2105050 100644 --- a/api-gateways/traefik/README.md +++ b/api-gateways/traefik/README.md @@ -37,9 +37,14 @@ npm install npm start ``` -The service listens on the port specified in `.env` (default 4181) and answers Traefik's ForwardAuth requests on **every path**: +Or build the container image with the included `Dockerfile`: -- `*` → Validates the session and emits identity headers, or returns 401/302. +```bash +docker build -t ory-forwardauth forwardauth/ +docker run -p 4181:4181 -e ORY_SDK_URL=https://.projects.oryapis.com ory-forwardauth +``` + +The service listens on the port specified in `.env` (default 4181) and answers Traefik's ForwardAuth requests on **every path**: it validates the session and emits identity headers, or returns 401/302. ## Wire up Traefik @@ -52,11 +57,19 @@ Reference Traefik configurations are in [`traefik/`](traefik): The middleware is set up to copy these headers from the auth response into the upstream request: - `X-User-Id` — Ory identity ID. -- `X-User-Email` — `identity.traits.email`. -- `X-User-Name` — concatenated `identity.traits.name.first` + `last`. +- `X-User-Email` — `identity.traits.email`. Omitted when it contains non-ASCII characters. +- `X-User-Name` — concatenated `identity.traits.name.first` + `last`. Omitted when it contains non-ASCII characters, because header values must be ASCII. - `X-Session-Id` — Ory session ID. - `X-Auth-AAL` — Authenticator Assurance Level (`aal1` / `aal2`). -- `X-User-Metadata` — JSON-stringified `identity.metadata_public` (when present and non-empty). +- `X-User-Metadata` — JSON-stringified `identity.metadata_public` (when present and non-empty), with non-ASCII characters escaped as `\uXXXX`. Decode it with a JSON parser. + +Traefik replaces each header listed in `authResponseHeaders` with the validator's value, or removes it when the validator doesn't set it, so a client can't supply these headers on protected routes. Keep every identity header your upstream trusts in that list. Routes that skip `ory-auth`, such as the public routes in the examples, use the `strip-identity-headers` middleware to remove them. + +On Traefik 3.7.12 or later, also set `http.aliasHeadersStrategy: delete` on each entrypoint (the compose file passes `--entrypoints..http.aliasHeadersStrategy=delete`). Traefik then deletes header names that contain dots, underscores, or other characters outside letters, digits, and hyphens, so a backend that normalizes header names can't read `X_User_Id` as `X-User-Id`. See [Traefik's entrypoint reference](https://doc.traefik.io/traefik/reference/install-configuration/entrypoints/#aliasheadersstrategy). + +Don't set `trustForwardHeader: true` on the middleware unless every client reaching Traefik is a trusted proxy. The validator reads the request path from `X-Forwarded-Uri` to decide which paths are public. + +Pointing `forwardAuth.address` straight at Ory's `/sessions/whoami` also works for gating, but `whoami` sets only the `X-Kratos-Authenticated-Identity-Id` response header, so the upstream receives no `X-User-*` headers. Use this validator to forward more identity data. Detailed setup with Kubernetes, sidecar deployments, and the JWKS-based alternative (for pure JWT validation without round-tripping Ory): see the [docs page](https://ory.com/docs/integrates-with/api-gateways/traefik). @@ -64,8 +77,8 @@ Detailed setup with Kubernetes, sidecar deployments, and the JWKS-based alternat - **Every request returns `401`** — the user's session cookie isn't reaching the service. Check Traefik's `forwardAuth` middleware includes `authRequestHeaders: [Cookie, Authorization, X-Session-Token]` (or omit the field so all headers are forwarded), and confirm the cookie domain covers the Traefik hostname. - **Service returns `503`** — `ORY_SDK_URL` is wrong or unreachable from the service's pod/container. Try `curl $ORY_SDK_URL/sessions/whoami` from inside the container. -- **Public paths still require auth** — `ALLOWED_PATHS` checks exact match or prefix match with a trailing `/`. `/health` matches `/health` and `/health/...` but not `/healthadmin`. -- **`302` redirects to login but `return_to` is wrong** — the service reads the original path from `X-Forwarded-Uri`. Ensure Traefik forwards it (default ForwardAuth behavior). +- **Public paths still require auth** — `ALLOWED_PATHS` checks the path without its query string, by exact match or prefix match with a trailing `/`. `/health` matches `/health` and `/health/...` but not `/healthadmin`. +- **`302` redirects to login but `return_to` is wrong** — the service reads the original path from `X-Forwarded-Uri`, which Traefik sets on every ForwardAuth request. - **Identity headers don't reach the upstream** — Traefik's `authResponseHeaders` doesn't list the header you expect. Add the header name to that list in the middleware definition. ## License diff --git a/api-gateways/traefik/forwardauth/.dockerignore b/api-gateways/traefik/forwardauth/.dockerignore new file mode 100644 index 0000000..37d7e73 --- /dev/null +++ b/api-gateways/traefik/forwardauth/.dockerignore @@ -0,0 +1,2 @@ +node_modules +.env diff --git a/api-gateways/traefik/forwardauth/Dockerfile b/api-gateways/traefik/forwardauth/Dockerfile new file mode 100644 index 0000000..e793988 --- /dev/null +++ b/api-gateways/traefik/forwardauth/Dockerfile @@ -0,0 +1,9 @@ +FROM node:22-alpine +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci --omit=dev +COPY tsconfig.json server.ts ./ +USER node +ENV PORT=4181 +EXPOSE 4181 +CMD ["node_modules/.bin/tsx", "server.ts"] diff --git a/api-gateways/traefik/forwardauth/server.ts b/api-gateways/traefik/forwardauth/server.ts index b264916..5338cb2 100644 --- a/api-gateways/traefik/forwardauth/server.ts +++ b/api-gateways/traefik/forwardauth/server.ts @@ -73,11 +73,12 @@ function unauthorized(res: Response, requestPath: string): void { res.status(401).json({ error: "unauthorized" }); } -app.all("*", async (req: Request, res: Response) => { - // Traefik exposes the original request path via X-Forwarded-Uri. - const requestPath = req.header("x-forwarded-uri") || req.path; +app.use(async (req: Request, res: Response) => { + // Traefik exposes the original request URI, including the query string, via X-Forwarded-Uri. + const requestPath = req.header("x-forwarded-uri") || req.originalUrl; + const { pathname } = new URL(requestPath, "http://localhost"); - if (isPublicPath(requestPath)) { + if (isPublicPath(pathname)) { res.status(200).end(); return; } @@ -121,23 +122,21 @@ app.all("*", async (req: Request, res: Response) => { const identity = session.identity ?? {}; const traits = identity.traits ?? {}; - res.setHeader("X-User-Id", identity.id ?? ""); - res.setHeader("X-Session-Id", session.id ?? ""); - if (traits.email) res.setHeader("X-User-Email", traits.email); + setIfHeaderSafe(res, "X-User-Id", identity.id); + setIfHeaderSafe(res, "X-Session-Id", session.id); + setIfHeaderSafe(res, "X-User-Email", traits.email); const fullName = [traits.name?.first, traits.name?.last] .filter((s): s is string => Boolean(s)) .join(" "); - if (fullName) res.setHeader("X-User-Name", fullName); + setIfHeaderSafe(res, "X-User-Name", fullName); + setIfHeaderSafe(res, "X-Auth-AAL", session.authenticator_assurance_level); - if (session.authenticator_assurance_level) { - res.setHeader("X-Auth-AAL", session.authenticator_assurance_level); - } if ( identity.metadata_public && Object.keys(identity.metadata_public).length > 0 ) { - res.setHeader("X-User-Metadata", JSON.stringify(identity.metadata_public)); + res.setHeader("X-User-Metadata", asciiJson(identity.metadata_public)); } res.status(200).end(); @@ -147,6 +146,21 @@ app.all("*", async (req: Request, res: Response) => { } }); +// HTTP header values must be visible ASCII. Optional values that are not, +// such as a non-Latin name, are omitted instead of failing the request. +function setIfHeaderSafe(res: Response, name: string, value: string | undefined): void { + if (value && /^[\x20-\x7e]*$/.test(value)) { + res.setHeader(name, value); + } +} + +function asciiJson(value: Record): string { + return JSON.stringify(value).replace( + /[\u007f-\uffff]/g, + (c) => "\\u" + c.charCodeAt(0).toString(16).padStart(4, "0"), + ); +} + app.listen(PORT, () => { console.log(`Traefik ForwardAuth service listening on port ${PORT}`); }); diff --git a/api-gateways/traefik/traefik/docker-compose.yaml b/api-gateways/traefik/traefik/docker-compose.yaml index 44ec9e6..8332aba 100644 --- a/api-gateways/traefik/traefik/docker-compose.yaml +++ b/api-gateways/traefik/traefik/docker-compose.yaml @@ -5,11 +5,13 @@ services: traefik: - image: traefik:v3.1 + image: traefik:v3.7 command: - --api.insecure=true - --entrypoints.web.address=:80 - --entrypoints.websecure.address=:443 + - --entrypoints.web.http.aliasHeadersStrategy=delete + - --entrypoints.websecure.http.aliasHeadersStrategy=delete - --providers.docker=true - --providers.docker.exposedByDefault=false - --providers.file.filename=/etc/traefik/dynamic-config.yaml diff --git a/api-gateways/traefik/traefik/dynamic-config.yaml b/api-gateways/traefik/traefik/dynamic-config.yaml index 4810747..7467bcf 100644 --- a/api-gateways/traefik/traefik/dynamic-config.yaml +++ b/api-gateways/traefik/traefik/dynamic-config.yaml @@ -1,16 +1,16 @@ # Traefik dynamic configuration. # # Wires up Traefik's forwardAuth middleware to the Ory session validator -# in forwardauth/server.js. Traefik copies the response headers we set -# (X-User-*, X-Session-Id, X-Auth-AAL, X-User-Metadata) into the upstream -# request via authResponseHeaders. +# in forwardauth/server.ts. Traefik replaces each header listed in +# authResponseHeaders with the validator's value, or removes it when the +# validator doesn't set it, so clients can't supply identity headers. +# Routes without ory-auth use strip-identity-headers instead. http: middlewares: ory-auth: forwardAuth: address: http://ory-forwardauth:4181 - trustForwardHeader: true authResponseHeaders: - X-User-Id - X-User-Email @@ -19,6 +19,16 @@ http: - X-Auth-AAL - X-User-Metadata + strip-identity-headers: + headers: + customRequestHeaders: + X-User-Id: "" + X-User-Email: "" + X-User-Name: "" + X-Session-Id: "" + X-Auth-AAL: "" + X-User-Metadata: "" + routers: api: rule: PathPrefix(`/api`) @@ -32,6 +42,8 @@ http: public: rule: PathPrefix(`/public`) || Path(`/health`) service: api-service + middlewares: + - strip-identity-headers entryPoints: - websecure tls: {} diff --git a/api-gateways/traefik/traefik/kubernetes-ingress.yaml b/api-gateways/traefik/traefik/kubernetes-ingress.yaml index a63be03..c734181 100644 --- a/api-gateways/traefik/traefik/kubernetes-ingress.yaml +++ b/api-gateways/traefik/traefik/kubernetes-ingress.yaml @@ -3,6 +3,13 @@ # Assumes Traefik is installed as the cluster's ingress controller with CRDs # enabled, and that ory-forwardauth (the service in ../forwardauth/) is # deployed as a Service named ory-forwardauth in the default namespace. +# +# Set http.aliasHeadersStrategy to delete on the websecure entrypoint (Traefik +# 3.7.12 or later), for example with the Helm value +# additionalArguments: +# - --entrypoints.websecure.http.aliasHeadersStrategy=delete +# Otherwise a backend that normalizes header names can read a client-sent +# X_User_Id as X-User-Id. apiVersion: traefik.io/v1alpha1 kind: Middleware @@ -12,7 +19,6 @@ metadata: spec: forwardAuth: address: http://ory-forwardauth.default.svc.cluster.local:4181 - trustForwardHeader: true authResponseHeaders: - X-User-Id - X-User-Email @@ -21,6 +27,22 @@ spec: - X-Auth-AAL - X-User-Metadata +--- +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: strip-identity-headers + namespace: default +spec: + headers: + customRequestHeaders: + X-User-Id: "" + X-User-Email: "" + X-User-Name: "" + X-Session-Id: "" + X-Auth-AAL: "" + X-User-Metadata: "" + --- apiVersion: traefik.io/v1alpha1 kind: IngressRoute @@ -56,5 +78,7 @@ spec: services: - name: backend-service port: 8080 + middlewares: + - name: strip-identity-headers tls: certResolver: letsencrypt diff --git a/cloud-infrastructure/aws/README.md b/cloud-infrastructure/aws/README.md index 591b255..2f6db34 100644 --- a/cloud-infrastructure/aws/README.md +++ b/cloud-infrastructure/aws/README.md @@ -2,7 +2,7 @@ > **Maintained by:** Community contributors -Deployment patterns for running on AWS with Ory. This is **not a vendor integration** — Ory Network is reached over HTTPS like any SaaS, and self-hosted Ory products run on standard Kubernetes (EKS) with RDS Postgres and (optionally) ElastiCache. This directory captures the two common patterns and the AWS-side glue. +Deployment patterns for running on AWS with Ory. This is **not a vendor integration** — Ory Network is reached over HTTPS like any SaaS, and self-hosted Ory products run on standard Kubernetes (EKS) with RDS Postgres. This directory captures the two common patterns and the AWS-side glue. **Type:** config (deployment pattern) **Docs page:** [ory.com/docs/integrates-with/cloud-infrastructure/aws](https://www.ory.com/docs/integrates-with/cloud-infrastructure/aws) @@ -17,13 +17,12 @@ Deployment patterns for running on AWS with Ory. This is **not a vendor integrat Standard SaaS pattern: - Outbound HTTPS (port 443) from your VPC to `*.oryapis.com`. Most security groups need an explicit egress rule for this. - Store the Ory API key in **AWS Secrets Manager** (or SSM Parameter Store) and inject it into the workload via task/pod IAM. -- Use **Route 53** to map your custom auth domain (e.g. `auth.example.com`) to `custom.oryapis.com` (CNAME). Configure the custom domain on the Ory Network side via `ory update project --custom-domain`. +- Use **Route 53** to map your custom auth domain (e.g. `auth.example.com`) to `.projects.oryapis.com` (CNAME). Add the domain to your project in the Ory Console. See [Set up custom domains](https://www.ory.com/docs/guides/custom-domains). ### Pattern B — Self-hosted Ory on EKS - **EKS** for the cluster (`eksctl create cluster --with-oidc` is the simplest start). - **RDS PostgreSQL** Multi-AZ for Kratos/Hydra/Keto datastores. Tune `max_connections` and `shared_buffers` for the workload. -- **ElastiCache for Redis** (optional) for session caching with `at_rest_encryption_enabled` and `transit_encryption_enabled`. - Deploy via the official Helm charts (`helm repo add ory https://k8s.ory.sh/helm/charts`). - **AWS Load Balancer Controller** (ALB Ingress) for HTTPS termination with an ACM certificate. diff --git a/cloud-infrastructure/aws/registry.entry.yaml b/cloud-infrastructure/aws/registry.entry.yaml index 0276bdd..66fba80 100644 --- a/cloud-infrastructure/aws/registry.entry.yaml +++ b/cloud-infrastructure/aws/registry.entry.yaml @@ -9,15 +9,15 @@ type: config maintainedBy: Community contributors description: >- Deployment patterns for running with Ory on AWS — both the managed Ory Network case (AWS app reaching out to - Ory over HTTPS) and the self-hosted case (Ory on EKS with RDS Postgres and optional ElastiCache). Not a + Ory over HTTPS) and the self-hosted case (Ory on EKS with RDS Postgres). Not a vendor integration in the registry sense; documentation of the AWS-side glue around standard Ory deployment. useCase: >- An AWS-resident product team needs the AWS-specific configuration to either reach Ory Network from inside a - VPC, or to self-host the open-source Ory stack on EKS with the typical AWS managed services (RDS, ElastiCache, + VPC, or to self-host the open-source Ory stack on EKS with the typical AWS managed services (RDS, ALB, ACM, Secrets Manager, Route 53). coreFunctionality: |- - - Pattern A (Ory Network from AWS): outbound HTTPS egress to *.oryapis.com; API key in AWS Secrets Manager (IAM-bound, not env vars); Route 53 CNAME to custom.oryapis.com for the custom auth domain. - - Pattern B (self-hosted on EKS): EKS cluster with --with-oidc, RDS PostgreSQL Multi-AZ for Kratos/Hydra/Keto datastores, optional ElastiCache Redis with at-rest + transit encryption. + - Pattern A (Ory Network from AWS): outbound HTTPS egress to *.oryapis.com; API key in AWS Secrets Manager (IAM-bound, not env vars); Route 53 CNAME from the custom auth domain to .projects.oryapis.com, with the domain added to the project in the Ory Console. + - Pattern B (self-hosted on EKS): EKS cluster with --with-oidc, RDS PostgreSQL Multi-AZ for Kratos/Hydra/Keto datastores. - Deploy via the official Helm charts (helm repo add ory https://k8s.ory.sh/helm/charts). - AWS Load Balancer Controller (ALB Ingress) for HTTPS termination with an ACM certificate. - Documented at ory.com/docs/integrates-with/cloud-infrastructure/aws; the Ory self-hosted/operations pages cover the cloud-agnostic concerns. diff --git a/cloud-infrastructure/azure/README.md b/cloud-infrastructure/azure/README.md index daedeb9..2aea8bd 100644 --- a/cloud-infrastructure/azure/README.md +++ b/cloud-infrastructure/azure/README.md @@ -16,13 +16,12 @@ Deployment patterns for running with Ory on Azure. **Not a vendor integration** - Outbound HTTPS (port 443) from your VNet/subnet to `*.oryapis.com`. NSG rule + (if locked-down egress) Azure Firewall application rule. - Store the Ory API key in **Azure Key Vault**; inject via Workload Identity (AKS) or Container Apps secret references — no env-var secrets in source. -- Use **Azure DNS** to map your custom auth domain to `custom.oryapis.com` (CNAME). Configure on the Ory side via `ory update project --custom-domain`. +- Use **Azure DNS** to map your custom auth domain to `.projects.oryapis.com` (CNAME). Add the domain to your project in the Ory Console. See [Set up custom domains](https://www.ory.com/docs/guides/custom-domains). ### Pattern B — Self-hosted Ory on AKS - **AKS** with Azure CNI, Workload Identity enabled. - **Azure Database for PostgreSQL — Flexible Server** for Kratos/Hydra/Keto datastores; pick the Zone-Redundant HA tier for production. -- **Azure Cache for Redis** (optional) for session caching. - Deploy via the official Helm charts (`helm repo add ory https://k8s.ory.sh/helm/charts`). - **Application Gateway Ingress Controller** (AGIC) for HTTPS termination with a Key Vault-stored cert. diff --git a/cloud-infrastructure/azure/registry.entry.yaml b/cloud-infrastructure/azure/registry.entry.yaml index 6f5ed3b..cba16b4 100644 --- a/cloud-infrastructure/azure/registry.entry.yaml +++ b/cloud-infrastructure/azure/registry.entry.yaml @@ -16,8 +16,8 @@ useCase: >- a VNet, or to self-host the open-source Ory stack on AKS with the typical Azure managed services (PostgreSQL Flexible Server, Key Vault, Workload Identity, Application Gateway Ingress). coreFunctionality: |- - - Pattern A (Ory Network from Azure): outbound HTTPS egress to *.oryapis.com (NSG + optional Azure Firewall app rule); API key in Azure Key Vault; Azure DNS CNAME to custom.oryapis.com. - - Pattern B (self-hosted on AKS): AKS with Workload Identity, Azure Database for PostgreSQL Flexible Server (Zone-Redundant HA in production), optional Azure Cache for Redis. + - Pattern A (Ory Network from Azure): outbound HTTPS egress to *.oryapis.com (NSG + optional Azure Firewall app rule); API key in Azure Key Vault; Azure DNS CNAME from the custom auth domain to .projects.oryapis.com. + - Pattern B (self-hosted on AKS): AKS with Workload Identity, Azure Database for PostgreSQL Flexible Server (Zone-Redundant HA in production). - Deploy via the official Helm charts (helm repo add ory https://k8s.ory.sh/helm/charts). - Application Gateway Ingress Controller (AGIC) for HTTPS termination with a Key Vault-stored certificate. - For Microsoft Entra ID identity provider configuration, see social-sign-in/microsoft and enterprise-sso/microsoft-entra-id (separate concerns from cloud deployment). diff --git a/cloud-infrastructure/gcp/README.md b/cloud-infrastructure/gcp/README.md index 3d8d412..9208963 100644 --- a/cloud-infrastructure/gcp/README.md +++ b/cloud-infrastructure/gcp/README.md @@ -16,13 +16,12 @@ Deployment patterns for running with Ory on Google Cloud. **Not a vendor integra - Outbound HTTPS to `*.oryapis.com` from the workload's VPC. Default GKE/Cloud Run egress allows this; restrict via Cloud NAT + Firewall rules if needed. - Store the Ory API key in **Secret Manager**; mount via Workload Identity (GKE) or runtime secret references (Cloud Run). -- Use **Cloud DNS** to map your custom auth domain to `custom.oryapis.com` (CNAME). Configure on the Ory side via `ory update project --custom-domain`. +- Use **Cloud DNS** to map your custom auth domain to `.projects.oryapis.com` (CNAME). Add the domain to your project in the Ory Console. See [Set up custom domains](https://www.ory.com/docs/guides/custom-domains). ### Pattern B — Self-hosted Ory on GKE - **GKE Autopilot** or Standard with Workload Identity enabled. - **Cloud SQL for PostgreSQL** (HA configuration) for Kratos/Hydra/Keto datastores. Connect via the Cloud SQL Auth Proxy sidecar (preferred) or private IP with VPC peering. -- **Memorystore for Redis** (optional) for session caching. - Deploy via the official Helm charts (`helm repo add ory https://k8s.ory.sh/helm/charts`). - **GKE Gateway** (or Ingress with GCLB) for HTTPS termination with a Google-managed certificate. diff --git a/cloud-infrastructure/gcp/registry.entry.yaml b/cloud-infrastructure/gcp/registry.entry.yaml index d470950..1b51f3e 100644 --- a/cloud-infrastructure/gcp/registry.entry.yaml +++ b/cloud-infrastructure/gcp/registry.entry.yaml @@ -16,8 +16,8 @@ useCase: >- VPC, or to self-host the open-source Ory stack on GKE with the typical GCP managed services (Cloud SQL, Secret Manager, Workload Identity, GKE Gateway). coreFunctionality: |- - - Pattern A (Ory Network from GCP): outbound HTTPS egress to *.oryapis.com; API key in Secret Manager via Workload Identity; Cloud DNS CNAME to custom.oryapis.com. - - Pattern B (self-hosted on GKE): GKE Autopilot or Standard with Workload Identity, Cloud SQL for PostgreSQL HA, optional Memorystore Redis. Connect to Cloud SQL via the Cloud SQL Auth Proxy sidecar (preferred) or private IP with VPC peering. + - Pattern A (Ory Network from GCP): outbound HTTPS egress to *.oryapis.com; API key in Secret Manager via Workload Identity; Cloud DNS CNAME from the custom auth domain to .projects.oryapis.com. + - Pattern B (self-hosted on GKE): GKE Autopilot or Standard with Workload Identity, Cloud SQL for PostgreSQL HA. Connect to Cloud SQL via the Cloud SQL Auth Proxy sidecar (preferred) or private IP with VPC peering. - Deploy via the official Helm charts (helm repo add ory https://k8s.ory.sh/helm/charts). - GKE Gateway or Ingress with GCLB for HTTPS termination with a Google-managed certificate. - For Google identity provider configuration, see social-sign-in/google and enterprise-sso/google-workspace (separate concerns from cloud deployment). diff --git a/containerization/docker/README.md b/containerization/docker/README.md index f3e8590..0b44bb0 100644 --- a/containerization/docker/README.md +++ b/containerization/docker/README.md @@ -2,18 +2,18 @@ > **Maintained by:** Community contributors -Run the open-source Ory stack locally or in containerized deployments using the official Docker images at `oryd/kratos`, `oryd/hydra`, `oryd/keto`, `oryd/oathkeeper`. This directory captures the standard `docker-compose.yml` for the full self-hosted stack. +Run the open-source Ory stack locally or in containerized deployments using the official Docker images at `oryd/kratos`, `oryd/hydra`, `oryd/keto`, `oryd/oathkeeper`. This directory points to the Docker Compose quickstarts that each product ships in its own repository. **Type:** config (deployment pattern) **Docs page:** [ory.com/docs/integrates-with/containerization/docker](https://www.ory.com/docs/integrates-with/containerization/docker) - [Kratos install](https://www.ory.com/docs/oss/kratos/install) -- [Hydra install](https://www.ory.com/docs/hydra/self-hosted/install) and [Hydra quickstart](https://www.ory.com/docs/hydra/self-hosted/quickstart) (Docker Compose-based) +- [Hydra quickstart](https://www.ory.com/docs/hydra/self-hosted/quickstart) (Docker Compose-based) - [Keto install](https://www.ory.com/docs/oss/keto/install) - [Oathkeeper install](https://www.ory.com/docs/oathkeeper/install) -## What's in this directory +## Docker Compose quickstarts -A reference `docker-compose.yml` that brings up the full stack against a local Postgres with mailslurper for email testing. Useful for: +The [Kratos quickstart](https://www.ory.com/docs/oss/kratos/quickstart) and the [Hydra quickstart](https://www.ory.com/docs/hydra/self-hosted/quickstart) each start the product with Docker Compose from its repository. Useful for: - Local development against a self-hosted stack. - Reproducing CI failures locally. diff --git a/containerization/docker/registry.entry.yaml b/containerization/docker/registry.entry.yaml index 12e3087..776971a 100644 --- a/containerization/docker/registry.entry.yaml +++ b/containerization/docker/registry.entry.yaml @@ -9,15 +9,15 @@ type: config maintainedBy: Community contributors description: >- Run the self-hosted Ory stack (Kratos, Hydra, Keto, Oathkeeper) using the official oryd/* Docker images. This - directory holds a reference docker-compose.yml for local development and CI reproduction; production - deployments should use the Helm charts. + entry points to the Docker Compose quickstarts in each product's repository for local development and CI + reproduction; production deployments should use the Helm charts. useCase: >- A developer wants to bring up the open-source Ory stack locally — for development, CI reproduction, or trying upgrades before promoting to a real cluster — without standing up Kubernetes. coreFunctionality: |- - - Reference docker-compose.yml runs Kratos, Hydra, Keto, Oathkeeper against a local Postgres with mailslurper for email testing. + - The Kratos and Hydra quickstarts start each product with Docker Compose from its repository. - Uses the official oryd/kratos, oryd/hydra, oryd/keto, oryd/oathkeeper images from Docker Hub. - - Per-product Docker setup is documented in each product's install guide (Kratos, Hydra, Keto, Oathkeeper) on ory.com/docs. + - Per-product Docker setup is documented in each product's install guide (Kratos, Keto, Oathkeeper) on ory.com/docs. - Hydra ships a Docker-Compose-based quickstart at /docs/hydra/self-hosted/quickstart. - For production, use the Helm charts under iac-devops/helm — docker-compose lacks the probes, rolling updates, and secret handling you'll want. oryMechanism: diff --git a/containerization/kubernetes/README.md b/containerization/kubernetes/README.md index 4514333..c6123d5 100644 --- a/containerization/kubernetes/README.md +++ b/containerization/kubernetes/README.md @@ -7,18 +7,27 @@ Raw Kubernetes manifests for deploying the open-source Ory stack (Kratos, Hydra, **Type:** config (deployment pattern) **Docs page:** [ory.com/docs/integrates-with/containerization/kubernetes](https://www.ory.com/docs/integrates-with/containerization/kubernetes) - [Kratos install](https://www.ory.com/docs/oss/kratos/install) -- [Hydra install](https://www.ory.com/docs/hydra/self-hosted/install) +- [Hydra Helm chart guide](https://www.ory.com/docs/hydra/self-hosted/kubernetes-helm-chart) - [Keto install](https://www.ory.com/docs/oss/keto/install) - [Oathkeeper install](https://www.ory.com/docs/oathkeeper/install) -## What's in this directory +## Generate the manifests -Reference manifests for the full stack — Deployments, Services (split public/admin), ConfigMaps, Secrets, optional Ingress. Pattern: each Ory component as a separate Deployment, public APIs exposed via Service of type `ClusterIP` and reached through an Ingress. +To start from known-good resources, render the official Helm chart with `helm template` and manage the output with your GitOps tooling: + +```bash +helm repo add ory https://k8s.ory.sh/helm/charts +helm repo update +helm show values ory/kratos > kratos-values.yaml +helm template kratos ory/kratos --namespace ory -f kratos-values.yaml > kratos.yaml +``` + +Repeat for `ory/hydra`, `ory/keto`, and `ory/oathkeeper`. The output follows the usual pattern: each Ory component as a separate Deployment, with split public/admin Services of type `ClusterIP` and the public API reached through an Ingress. ## When to use this vs. Helm - **Helm** ([`iac-devops/helm`](../../iac-devops/helm/)) is the officially maintained path. Pin the chart version, override values, done. -- **Raw manifests** here when you need: GitOps with full visibility into each resource, customizations the Helm chart doesn't expose, or tight coupling with cluster-specific tooling (e.g. Kustomize overlays per environment). +- **Raw manifests** when you need: GitOps with full visibility into each resource, customizations the Helm chart doesn't expose, or tight coupling with cluster-specific tooling (e.g. Kustomize overlays per environment). For every other case, prefer Helm. diff --git a/containerization/kubernetes/registry.entry.yaml b/containerization/kubernetes/registry.entry.yaml index 28c68de..3cbd7d5 100644 --- a/containerization/kubernetes/registry.entry.yaml +++ b/containerization/kubernetes/registry.entry.yaml @@ -16,7 +16,7 @@ useCase: >- resources beyond what the Ory Helm charts expose, and is willing to maintain the manifests instead of using Helm. coreFunctionality: |- - - Reference manifests for the full stack: Deployments, public/admin Services (type ClusterIP), ConfigMaps, Secrets, optional Ingress. + - Generate manifests by rendering the official Helm charts with `helm template`, then manage the output with GitOps tooling. - Pattern: each Ory component (Kratos, Hydra, Keto, Oathkeeper) as a separate Deployment with split public/admin Services. - For most teams the Helm charts (iac-devops/helm) are the recommended path — choose raw manifests only when GitOps visibility or non-default customization justifies the maintenance burden. oryMechanism: diff --git a/data-persistence/cockroachdb/README.md b/data-persistence/cockroachdb/README.md index 44263a4..222ac7c 100644 --- a/data-persistence/cockroachdb/README.md +++ b/data-persistence/cockroachdb/README.md @@ -2,31 +2,32 @@ > **Maintained by:** Community contributors -[CockroachDB](https://www.cockroachlabs.com) is a distributed SQL database — horizontal scalability, strong consistency, multi-region survivability, with PostgreSQL wire-protocol compatibility. Ory components (Kratos, Hydra, Keto, Oathkeeper) use PostgreSQL as the primary datastore, so CockroachDB is a viable backend for deployments needing geographic distribution or high-availability that exceeds what a single Postgres can deliver. +[CockroachDB](https://www.cockroachlabs.com) is a distributed SQL database — horizontal scalability, strong consistency, multi-region survivability, with PostgreSQL wire-protocol compatibility. Ory Kratos, Ory Hydra, and Ory Keto support CockroachDB as a datastore, so CockroachDB is a viable backend for deployments needing geographic distribution or high-availability that exceeds what a single Postgres can deliver. **Type:** config (DSN substitution + Cockroach-aware tuning — no source code) **Docs page:** [ory.com/docs/integrates-with/data-persistence/cockroachdb](https://www.ory.com/docs/integrates-with/data-persistence/cockroachdb) ## Pattern -Cockroach speaks the Postgres wire protocol, so Ory products accept a Cockroach DSN with no changes: +Ory connects to CockroachDB with the `cockroach://` DSN scheme: ``` -postgres://user:password@cockroach-host:26257/ory_kratos?sslmode=verify-full&options=--cluster%3D +cockroach://user:password@cockroach-host:26257/ory_kratos?sslmode=verify-full ``` -Migrations run with `kratos migrate sql` (and similar for Hydra/Keto) — Cockroach accepts the same DDL Ory emits for Postgres, with a few caveats. +Use `cockroach://`, never `postgres://`. The scheme selects CockroachDB-specific migrations; a `postgres://` DSN selects the PostgreSQL migrations, which can fail partway and leave the database partly migrated. + +Run migrations with `kratos migrate sql` (and the equivalent for Hydra and Keto) against this DSN. ## Cockroach-aware tuning - **Foreign-key constraints**: Cockroach is more particular than Postgres about FK ordering during high concurrency. Ory's schema is FK-heavy; expect some `40001 retry` errors under load and configure clients to retry transactions per Cockroach's standard pattern. - **`SERIAL` vs `UUID`**: Ory schemas use UUIDs (good for Cockroach distribution); legacy `SERIAL` columns would hot-shard a single node — verify no Ory product is using SERIAL in your version. -- **Cluster setting `kv.transaction.write_pipelining.enabled`** off can simplify transaction debugging during initial rollout; turn back on for production. - **Serverless vs Self-hosted**: CockroachDB Serverless has request-per-second limits that bite high-traffic Ory deployments hard — Self-hosted (Dedicated) is the right tier for production identity workloads. ## Status -Community / proposed — no dedicated Ory documentation. Pattern is "use Postgres DSN; tune for Cockroach's distributed-transaction model." +Community / proposed — no dedicated Ory documentation. Pattern is "use a `cockroach://` DSN; tune for Cockroach's distributed-transaction model." ## License diff --git a/data-persistence/cockroachdb/registry.entry.yaml b/data-persistence/cockroachdb/registry.entry.yaml index 012cb3a..9c7ddcd 100644 --- a/data-persistence/cockroachdb/registry.entry.yaml +++ b/data-persistence/cockroachdb/registry.entry.yaml @@ -8,17 +8,16 @@ category: data-persistence type: config maintainedBy: Community contributors description: >- - Use CockroachDB as the primary datastore for self-hosted Ory components (Kratos, Hydra, Keto, Oathkeeper) - via its PostgreSQL wire-protocol compatibility. Right choice for deployments needing geographic distribution - or HA beyond a single Postgres node. Documented at ory.com/docs/integrates-with/data-persistence/cockroachdb — same DSN format Ory uses for Postgres works - for Cockroach with Cockroach-aware tuning. + Use CockroachDB as the primary datastore for self-hosted Ory Kratos, Ory Hydra, and Ory Keto with a + cockroach:// DSN. Right choice for deployments needing geographic distribution + or HA beyond a single Postgres node. Documented at ory.com/docs/integrates-with/data-persistence/cockroachdb — cockroach:// DSN plus Cockroach-aware tuning. useCase: >- A self-hosted Ory deployment needs multi-region survivability, horizontal scalability beyond a single Postgres node, or strong consistency under geographic distribution — and is willing to tune for Cockroach's distributed-transaction model. coreFunctionality: |- - - Cockroach speaks Postgres wire protocol — Ory product DSN format unchanged: postgres://user:password@cockroach-host:26257/?sslmode=verify-full&options=--cluster%3D. - - Migrations run with `kratos migrate sql` (etc.) against Cockroach without changes. + - DSN: cockroach://user:password@cockroach-host:26257/?sslmode=verify-full. The cockroach:// scheme selects CockroachDB-specific migrations; a postgres:// DSN selects the PostgreSQL migrations, which can fail partway. + - Migrations run with `kratos migrate sql` (etc.) against the cockroach:// DSN. - FK-heavy schema: expect some 40001 retry errors under high concurrency; clients should retry transactions per Cockroach's standard pattern. - UUID PKs (good — distribute across nodes) vs SERIAL (would hot-shard) — verify no Ory product version uses SERIAL. - Serverless tier has request-per-second limits unsuitable for production identity workloads; use Self-hosted (Dedicated) tier. diff --git a/edge-token-validation/akamai-edgeworkers/README.md b/edge-token-validation/akamai-edgeworkers/README.md index 7b11def..b7a98df 100644 --- a/edge-token-validation/akamai-edgeworkers/README.md +++ b/edge-token-validation/akamai-edgeworkers/README.md @@ -7,6 +7,25 @@ **Type:** session-validation (edge token validation pattern — see [`edge-token-validation/cloudflare-workers`](../cloudflare-workers/) for the canonical reference implementation) **Docs page:** [ory.com/docs/integrates-with/edge-token-validation/akamai-edgeworkers](https://www.ory.com/docs/integrates-with/edge-token-validation/akamai-edgeworkers) +## Prerequisites for JWT validation + +Ory issues opaque access tokens (`ory_at_…`) by default, and the `aud` claim is empty unless the client requests an audience. To issue JWTs that the EdgeWorker can verify: + +```bash +# 1. Switch the project to JWT access tokens. +ory patch oauth2-config --project --replace '/strategies/access_token="jwt"' + +# 2. Create a client that may request your API's audience. +ory create oauth2-client --project --name "" \ + --grant-type client_credentials --audience https://api.example.com + +# 3. Request the audience when you get a token. +curl https://.projects.oryapis.com/oauth2/token -u ":" \ + -d grant_type=client_credentials -d audience=https://api.example.com +``` + +The token's `iss` claim is `https://.projects.oryapis.com`, without a trailing slash. The JWKS can hold more than one key, so select the key that matches the token's `kid` header. For the client credentials grant, `sub` is the client ID. See [JWT access tokens](https://www.ory.com/docs/oauth2-oidc/jwt-access-token). + ## Pattern Same shape as the Cloudflare Workers reference but using Akamai EdgeWorkers' constraints (no Node fetch by default; use the EdgeWorkers `httpRequest` API; V8 isolate without Web Crypto on older runtimes). @@ -18,11 +37,13 @@ Same shape as the Cloudflare Workers reference but using Akamai EdgeWorkers' con 5. On valid: inject `X-User-Id` header into the request before it proceeds to origin. 6. On invalid: respond with 401 directly from the edge. +Apply the same validation as the [Cloudflare reference](../cloudflare-workers/#what-the-worker-checks): pin `RS256`, select the key by `kid` and refetch the JWKS on an unknown `kid`, verify `iss` (no trailing slash), `aud`, `exp`, and `nbf`, remove client-supplied identity headers before forwarding, and cache `whoami` responses only under a key derived from the session cookie or token. On Ory Network the session cookie is `ory_session_`, where `` is your project slug without dashes. + ## Differences from the Cloudflare reference - **No `fetch()` global** — use the EdgeWorkers `httpRequest` API. - **`EdgeKV`** for cross-edge cache; falls back to per-isolate `Map` if not provisioned. -- **CPU budget** is stricter (typically 50ms wall-clock); JWKS verification fits comfortably but `whoami` calls eat into this budget. +- **Limits depend on the resource tier.** Each event handler gets 10 ms (Basic Compute), 20 ms (Dynamic Compute), or 70 ms (Enterprise Compute) of CPU time and a capped number of HTTP sub-requests. Each `whoami` call is a sub-request. - **Akamai Property Manager** wires the EdgeWorker to a request behavior on the relevant hostname / path. ## Status diff --git a/edge-token-validation/akamai-edgeworkers/registry.entry.yaml b/edge-token-validation/akamai-edgeworkers/registry.entry.yaml index 57c95b9..dcadfb9 100644 --- a/edge-token-validation/akamai-edgeworkers/registry.entry.yaml +++ b/edge-token-validation/akamai-edgeworkers/registry.entry.yaml @@ -20,7 +20,7 @@ coreFunctionality: |- - Hydra JWT: verify signature against cached Ory JWKS using EdgeWorkers Crypto API. - Kratos session: call https://.projects.oryapis.com/sessions/whoami via httpRequest (EdgeWorkers doesn't have a fetch global). Cache responses in EdgeKV for 30-60s (fallback: per-isolate Map). - On valid: inject X-User-Id header; on invalid: 401 from the edge. - - CPU budget ~50ms wall-clock — JWKS verification fits comfortably; whoami calls eat into this. + - CPU time per event handler depends on the resource tier (10 ms Basic, 20 ms Dynamic, 70 ms Enterprise Compute), and HTTP sub-requests are capped; each whoami call is a sub-request. - Wired via Akamai Property Manager attaching the EdgeWorker to a behavior on the relevant hostname/path. - Reference implementation: edge-token-validation/cloudflare-workers (same pattern, different runtime). - Status: community/proposed — documented at ory.com/docs/integrates-with/edge-token-validation/akamai-edgeworkers. diff --git a/edge-token-validation/cloudflare-workers/README.md b/edge-token-validation/cloudflare-workers/README.md index 180f4ef..339de51 100644 --- a/edge-token-validation/cloudflare-workers/README.md +++ b/edge-token-validation/cloudflare-workers/README.md @@ -1,19 +1,19 @@ # Cloudflare Workers — Ory Session & JWT Validation at the Edge -Validate Ory Network session tokens and JWTs at Cloudflare's edge, before requests reach your origin server. +Validate Ory Network sessions and JWT access tokens at Cloudflare's edge, before requests reach your origin server. **Docs page:** [ory.com/docs/integrates-with/edge-token-validation/cloudflare-workers](https://www.ory.com/docs/integrates-with/edge-token-validation/cloudflare-workers) ## Overview -This integration deploys a Cloudflare Worker that intercepts incoming requests, validates Ory session cookies or Bearer JWTs against your Ory Network project, and forwards authenticated requests to your origin with enriched headers. Unauthenticated requests are rejected at the edge with zero load on your backend. +This integration deploys a Cloudflare Worker ([`src/worker.ts`](src/worker.ts)) that intercepts incoming requests, validates Ory sessions or Bearer JWTs against your Ory Network project, and forwards authenticated requests to your origin with identity headers. Unauthenticated requests are rejected at the edge. | Feature | Details | |---------|---------| | Platform | Cloudflare Workers | -| Runtime | V8 isolates (JavaScript/TypeScript) | -| Validation modes | Session cookie, JWT (access token) | -| JWKS caching | Cloudflare Cache API (edge-cached) | +| Runtime | V8 isolates (TypeScript, Web Crypto) | +| Validation modes | JWT access token (local, against the JWKS); session cookie or session token (`/sessions/whoami`) | +| Caching | JWKS and active sessions in isolate memory | | Ory Platform | Ory Network (managed cloud) | ## Architecture @@ -21,353 +21,106 @@ This integration deploys a Cloudflare Worker that intercepts incoming requests, ``` Client | - | (request with ory_session cookie or Bearer token) + | (request with ory_session_ cookie, X-Session-Token, or Bearer token) v Cloudflare Edge (Worker) | - |-- 1. Extract session cookie or Authorization header - |-- 2. Validate token: - | - Session cookie → call Ory /sessions/whoami - | - JWT → validate signature against JWKS (cached at edge) - |-- 3. Reject 401 if invalid - |-- 4. Forward to origin with X-User-Id, X-User-Email headers + |-- 1. Remove client-supplied identity headers + |-- 2. Validate the credential: + | - Bearer JWT → verify against the JWKS and check alg, iss, aud, exp, nbf + | - Session cookie → call Ory /sessions/whoami + | - Session token → call Ory /sessions/whoami + |-- 3. Reject with 401 if invalid (503 if Ory is unreachable) + |-- 4. Forward to origin with X-User-Id and related headers v Origin Server ``` -## Prerequisites +## Prerequisites for JWT validation -- Ory Network project with a custom domain or SDK URL -- Cloudflare account with Workers enabled -- Node.js >= 18 and npm -- Wrangler CLI (`npm install -g wrangler`) - -## Project Setup +Ory issues opaque access tokens (`ory_at_…`) by default, and the `aud` claim is empty unless the client requests an audience. To issue JWTs that the Worker can verify: ```bash -mkdir ory-edge-auth && cd ory-edge-auth -npm init -y -npm install wrangler --save-dev -npm install jose -``` - -## wrangler.toml - -```toml -name = "ory-edge-auth" -main = "src/worker.js" -compatibility_date = "2024-12-01" - -[vars] -ORY_SDK_URL = "https://your-project.projects.oryapis.com" -# Cookie name used by Ory (default: ory_session_*) -ORY_SESSION_COOKIE_PREFIX = "ory_session_" - -# For JWT validation -ORY_JWKS_URL = "https://your-project.projects.oryapis.com/.well-known/jwks.json" -ORY_ISSUER = "https://your-project.projects.oryapis.com" +# 1. Switch the project to JWT access tokens. +ory patch oauth2-config --project --replace '/strategies/access_token="jwt"' -# Paths that do not require authentication (comma-separated) -PUBLIC_PATHS = "/health,/public,/.well-known" +# 2. Create a client that may request your API's audience. +ory create oauth2-client --project --name "" \ + --grant-type client_credentials --audience https://api.example.com -# Where to redirect unauthenticated browser requests (empty = return 401 JSON) -LOGIN_REDIRECT_URL = "" - -# JWKS cache TTL in seconds -JWKS_CACHE_TTL = "3600" +# 3. Request the audience when you get a token. +curl https://.projects.oryapis.com/oauth2/token -u ":" \ + -d grant_type=client_credentials -d audience=https://api.example.com ``` -## Worker Script - -```javascript -// src/worker.js -import { createRemoteJWKSet, jwtVerify } from "jose"; - -// ---------- JWKS Cache ---------- - -let cachedJWKS = null; -let jwksCachedAt = 0; - -function getJWKS(env) { - const ttl = parseInt(env.JWKS_CACHE_TTL || "3600", 10) * 1000; - const now = Date.now(); - - if (!cachedJWKS || now - jwksCachedAt > ttl) { - cachedJWKS = createRemoteJWKSet(new URL(env.ORY_JWKS_URL)); - jwksCachedAt = now; - } - - return cachedJWKS; -} - -// ---------- Helpers ---------- - -function isPublicPath(pathname, env) { - const publicPaths = (env.PUBLIC_PATHS || "").split(",").map((p) => p.trim()); - return publicPaths.some( - (p) => pathname === p || pathname.startsWith(p + "/") - ); -} - -function getSessionCookie(request, env) { - const cookieHeader = request.headers.get("Cookie") || ""; - const prefix = env.ORY_SESSION_COOKIE_PREFIX || "ory_session_"; - const cookies = cookieHeader.split(";").map((c) => c.trim()); - const sessionCookie = cookies.find((c) => c.startsWith(prefix)); - return sessionCookie || null; -} - -function getBearerToken(request) { - const auth = request.headers.get("Authorization") || ""; - if (auth.startsWith("Bearer ")) { - return auth.slice(7); - } - return null; -} - -function unauthorizedResponse(message, env, request) { - // If LOGIN_REDIRECT_URL is set and request accepts HTML, redirect - if (env.LOGIN_REDIRECT_URL && request.headers.get("Accept")?.includes("text/html")) { - const returnTo = encodeURIComponent(request.url); - return Response.redirect( - `${env.LOGIN_REDIRECT_URL}?return_to=${returnTo}`, - 302 - ); - } - - return new Response(JSON.stringify({ error: message }), { - status: 401, - headers: { "Content-Type": "application/json" }, - }); -} - -// ---------- Session Cookie Validation ---------- - -async function validateSessionCookie(request, env) { - const sessionCookie = getSessionCookie(request, env); - if (!sessionCookie) return null; - - const response = await fetch(`${env.ORY_SDK_URL}/sessions/whoami`, { - headers: { - Cookie: sessionCookie, - Accept: "application/json", - }, - }); - - if (!response.ok) return null; - - const session = await response.json(); - if (!session.active) return null; - - return { - subject: session.identity.id, - email: session.identity.traits?.email || "", - session_id: session.id, - metadata: session.identity.metadata_public || {}, - }; -} - -// ---------- JWT Validation ---------- - -async function validateJWT(token, env) { - try { - const jwks = getJWKS(env); - const { payload } = await jwtVerify(token, jwks, { - issuer: env.ORY_ISSUER, - }); - - return { - subject: payload.sub, - email: payload.email || "", - session_id: payload.sid || "", - metadata: payload.ext || {}, - }; - } catch (err) { - console.error("JWT validation failed:", err.message); - return null; - } -} - -// ---------- Main Handler ---------- - -export default { - async fetch(request, env, ctx) { - const url = new URL(request.url); - - // Allow public paths through without auth - if (isPublicPath(url.pathname, env)) { - return fetch(request); - } - - // Try JWT first (API clients), then session cookie (browser clients) - let identity = null; - const bearerToken = getBearerToken(request); - - if (bearerToken) { - identity = await validateJWT(bearerToken, env); - } else { - identity = await validateSessionCookie(request, env); - } - - if (!identity) { - return unauthorizedResponse("Authentication required", env, request); - } - - // Clone request and add identity headers for the origin - const modifiedHeaders = new Headers(request.headers); - modifiedHeaders.set("X-User-Id", identity.subject); - modifiedHeaders.set("X-User-Email", identity.email); - modifiedHeaders.set("X-Session-Id", identity.session_id); - modifiedHeaders.set( - "X-User-Metadata", - JSON.stringify(identity.metadata) - ); - - // Remove the original auth headers so the origin does not see raw tokens - // (optional — remove these lines if your origin also needs the raw token) - // modifiedHeaders.delete("Authorization"); - // modifiedHeaders.delete("Cookie"); - - const modifiedRequest = new Request(request, { - headers: modifiedHeaders, - }); - - return fetch(modifiedRequest); - }, -}; -``` +The token's `iss` claim is `https://.projects.oryapis.com`, without a trailing slash. For the client credentials grant, `sub` is the client ID. See [JWT access tokens](https://www.ory.com/docs/oauth2-oidc/jwt-access-token). -## Advanced: JWKS Caching with Cloudflare Cache API - -For high-traffic deployments, cache the JWKS response in Cloudflare's edge cache to eliminate repeated fetches to Ory: - -```javascript -// src/jwks-cache.js - -/** - * Fetch JWKS with Cloudflare Cache API. - * The JWKS response is cached at the edge for the configured TTL. - */ -export async function fetchCachedJWKS(env, ctx) { - const cache = caches.default; - const cacheKey = new Request(env.ORY_JWKS_URL, { method: "GET" }); - - // Check edge cache - let response = await cache.match(cacheKey); - if (response) { - return response.json(); - } - - // Cache miss — fetch from Ory - response = await fetch(env.ORY_JWKS_URL, { - headers: { Accept: "application/json" }, - }); - - if (!response.ok) { - throw new Error(`Failed to fetch JWKS: ${response.status}`); - } - - // Clone and cache the response - const ttl = parseInt(env.JWKS_CACHE_TTL || "3600", 10); - const cachedResponse = new Response(response.body, { - headers: { - "Content-Type": "application/json", - "Cache-Control": `public, max-age=${ttl}`, - }, - }); - - ctx.waitUntil(cache.put(cacheKey, cachedResponse.clone())); - - return cachedResponse.json(); -} -``` +## What the Worker checks -## Deployment +- **JWTs:** `alg` must be `RS256`. The key is selected by the token's `kid`. When the `kid` isn't in the cached key set, the Worker fetches the JWKS again (at most once every 30 seconds) before rejecting the token. `iss` must equal `JWT_ISSUER` (default: `ORY_SDK_URL`), `aud` must contain `JWT_AUDIENCE`, `exp` must be in the future, and `nbf`, when present, must not be in the future. Without `JWT_AUDIENCE`, every JWT is rejected. +- **Sessions:** cookies named `ory_session_*` (Ory Network, where the suffix is your project slug without dashes) or `ory_kratos_session` (self-hosted Kratos), or a session token (`ory_st_…`) in `X-Session-Token` or as a Bearer token. Only the `ory_` cookies are sent to Ory. Active sessions are cached for 30 seconds under a SHA-256 hash of the credential, in isolate memory, bounded to 1,000 entries. A revoked session can therefore pass for up to 30 seconds. +- **Identity headers:** `X-User-Id`, `X-User-Email`, `X-User-Name`, `X-Session-Id`, `X-Auth-AAL`, and `X-User-Metadata` are removed from every incoming request, including public paths, and set only from validated data. `X-User-Metadata` is JSON with non-ASCII characters escaped. `X-User-Email` and `X-User-Name` are omitted when they contain non-ASCII characters. -```bash -# Authenticate with Cloudflare -wrangler login +## Configuration -# Set secrets (not stored in wrangler.toml) -wrangler secret put ORY_API_KEY +[`wrangler.toml`](wrangler.toml): -# Deploy to production -wrangler deploy +| Variable | Description | +|----------|-------------| +| `ORY_SDK_URL` | Your project URL, for example `https://.projects.oryapis.com`. | +| `JWT_AUDIENCE` | The audience your API requires. Required for JWT validation. | +| `JWT_ISSUER` | Optional. Defaults to `ORY_SDK_URL`. Set it when tokens carry a custom-domain issuer. | +| `PUBLIC_PATHS` | Comma-separated paths that skip authentication. Matches the exact path or a prefix followed by `/`. | +| `LOGIN_REDIRECT_URL` | Optional. Browser requests (`Accept: text/html`) without a valid session are redirected here with `return_to`. | -# Deploy to staging -wrangler deploy --env staging +## Develop and deploy -# Tail logs -wrangler tail +```bash +npm install +npm run dev # wrangler dev, runs src/worker.ts +npm run typecheck +npx wrangler login +npm run deploy # wrangler deploy ``` ## Testing ```bash -# Test with session cookie +# Session cookie curl -v https://your-worker.your-subdomain.workers.dev/api/me \ - -H "Cookie: ory_session_projectslug=" + -H "Cookie: ory_session_=" -# Test with JWT +# Session token +curl -v https://your-worker.your-subdomain.workers.dev/api/me \ + -H "X-Session-Token: " + +# JWT access token curl -v https://your-worker.your-subdomain.workers.dev/api/me \ -H "Authorization: Bearer " -# Test public path (should pass through) +# Public path (passes through) curl -v https://your-worker.your-subdomain.workers.dev/health -# Test unauthenticated (should return 401) +# Unauthenticated (401) curl -v https://your-worker.your-subdomain.workers.dev/api/me ``` -Expected 401 response: -```json -{ - "error": "Authentication required" -} -``` - -Expected authenticated response headers forwarded to origin: -``` -X-User-Id: 7a3b1c2d-... -X-User-Email: user@example.com -X-Session-Id: 9f8e7d6c-... -X-User-Metadata: {"plan":"pro"} -``` - -## Multi-Environment Configuration - -```toml -# wrangler.toml - -[env.staging] -name = "ory-edge-auth-staging" -vars = { ORY_SDK_URL = "https://staging-project.projects.oryapis.com", ORY_JWKS_URL = "https://staging-project.projects.oryapis.com/.well-known/jwks.json", ORY_ISSUER = "https://staging-project.projects.oryapis.com", PUBLIC_PATHS = "/health,/public", JWKS_CACHE_TTL = "300" } - -[env.production] -name = "ory-edge-auth-production" -vars = { ORY_SDK_URL = "https://prod-project.projects.oryapis.com", ORY_JWKS_URL = "https://prod-project.projects.oryapis.com/.well-known/jwks.json", ORY_ISSUER = "https://prod-project.projects.oryapis.com", PUBLIC_PATHS = "/health", JWKS_CACHE_TTL = "3600" } -``` - -## Performance Considerations +## Performance considerations | Concern | Mitigation | |---------|-----------| -| JWKS fetch latency | Edge-cached via Cache API; refreshed every `JWKS_CACHE_TTL` seconds | -| `/sessions/whoami` latency | Ory Network has edge presence; consider JWT mode for lowest latency | -| Cold starts | V8 isolates start in <5ms; negligible | -| Worker CPU time | JWT validation uses <1ms CPU; session validation depends on Ory response time | +| JWKS fetch latency | Cached per isolate for one hour; the subrequest is edge-cached for five minutes | +| `/sessions/whoami` latency | Active sessions cached per isolate for 30 seconds; use JWT mode for the lowest latency | +| Worker CPU time | Workers on the Free plan get 10 ms of CPU time per request. Waiting on `fetch()` doesn't count toward CPU time | -## Security Considerations +## Security considerations -- Always use HTTPS for your Ory SDK URL -- Store the `ORY_API_KEY` as a Wrangler secret, not in `wrangler.toml` -- Strip forwarded identity headers from incoming requests to prevent spoofing (the Worker overwrites them) -- Consider IP-allowlisting or mTLS between the Worker and your origin -- Rotate JWKS cache on key rotation by reducing `JWKS_CACHE_TTL` or deploying a cache-busting update +- Use HTTPS for `ORY_SDK_URL`. +- Make sure the origin is reachable only through the Worker, for example with IP allowlisting or mTLS, so clients can't bypass it and set identity headers themselves. +- The Worker forwards the original `Authorization` and `Cookie` headers to origin. Remove them in `forward()` if your origin doesn't need them. ## References -- [Cloudflare Workers Documentation](https://developers.cloudflare.com/workers/) -- [Ory Session Validation](https://www.ory.sh/docs/kratos/session-management/overview) -- [Ory JWKS Endpoint](https://www.ory.sh/docs/hydra/jwks) -- [jose (JavaScript JOSE library)](https://github.com/panva/jose) +- [Cloudflare Workers documentation](https://developers.cloudflare.com/workers/) +- [Ory session management](https://www.ory.com/docs/kratos/session-management/overview) +- [Ory JWT access tokens](https://www.ory.com/docs/oauth2-oidc/jwt-access-token) diff --git a/edge-token-validation/cloudflare-workers/registry.entry.yaml b/edge-token-validation/cloudflare-workers/registry.entry.yaml index 650667a..b711961 100644 --- a/edge-token-validation/cloudflare-workers/registry.entry.yaml +++ b/edge-token-validation/cloudflare-workers/registry.entry.yaml @@ -22,10 +22,11 @@ useCase: >- and adds identity headers so downstream services can attribute the request without re-validating. coreFunctionality: |- - Cloudflare Worker deployed via wrangler — runs in V8 isolates at the Cloudflare edge. - - Two validation modes: session cookie (forwards to Ory /sessions/whoami with 30 s edge cache) and JWT Bearer token (validates RSASSA-PKCS1-v1_5 signatures against Ory's JWKS, JWKS cached at the edge for 1 h). - - Forwards authenticated requests to origin with X-User-Id, X-Session-Id, X-User-Email, X-User-Name, X-Auth-AAL, X-User-Metadata headers. - - Configurable PUBLIC_PATHS to skip auth on health, public, and static paths. - - Optional LOGIN_REDIRECT_URL turns 401 into a 302 to login with return_to. + - JWT Bearer tokens: RS256 only, key selected by kid with JWKS refresh on unknown kid, and iss, aud (JWT_AUDIENCE), exp, and nbf enforced. Requires JWT access tokens enabled on the Ory project. + - Sessions: ory_session_* or ory_kratos_session cookies and ory_st_ session tokens are checked with Ory /sessions/whoami; active sessions are cached per credential hash for 30 s in bounded isolate memory. + - Removes client-supplied identity headers, then forwards authenticated requests to origin with X-User-Id, X-Session-Id, X-User-Email, X-User-Name, X-Auth-AAL, X-User-Metadata headers. + - Configurable PUBLIC_PATHS (exact path or prefix followed by /) to skip auth on health, public, and static paths. + - Optional LOGIN_REDIRECT_URL redirects browser requests to login with return_to. oryMechanism: - session-validation protocol: diff --git a/edge-token-validation/cloudflare-workers/src/worker.ts b/edge-token-validation/cloudflare-workers/src/worker.ts index 5155de6..83bfa55 100644 --- a/edge-token-validation/cloudflare-workers/src/worker.ts +++ b/edge-token-validation/cloudflare-workers/src/worker.ts @@ -1,21 +1,25 @@ /** * Cloudflare Worker — Ory Edge Token Validation * - * Validates Ory session cookies or JWT Bearer tokens at the Cloudflare edge. - * On success, enriches the request with identity headers before forwarding to origin. + * Validates Ory sessions or JWT access tokens at the Cloudflare edge. + * On success, forwards the request to origin with identity headers. * On failure, returns 401 or redirects to login. * - * Two validation modes: - * 1. Session cookie → calls Ory's /sessions/whoami (proxied via CF cache) - * 2. JWT Bearer token → validates locally using Ory's JWKS (cached at edge) - * - * SECURITY: ORY_SDK_URL should be set as a Cloudflare Workers secret: - * wrangler secret put ORY_SDK_URL + * Validation modes: + * 1. JWT Bearer token: verified locally against Ory's JWKS. The Worker + * requires RS256, a known `kid`, the expected `iss` and `aud`, and a + * valid `exp` and `nbf`. + * 2. Session cookie (`ory_session_*` or `ory_kratos_session`) or session + * token (`X-Session-Token` or a non-JWT Bearer token): checked with + * Ory's /sessions/whoami. Active sessions are cached per credential in + * isolate memory. */ export interface Env { ORY_SDK_URL: string; - PUBLIC_PATHS: string; + JWT_AUDIENCE?: string; + JWT_ISSUER?: string; + PUBLIC_PATHS?: string; LOGIN_REDIRECT_URL?: string; } @@ -24,239 +28,301 @@ interface OrySession { active: boolean; identity: { id: string; - traits: { + traits?: { email?: string; name?: { first?: string; last?: string }; }; - metadata_public?: Record; + metadata_public?: Record | null; }; authenticator_assurance_level?: string; } -interface JWKSResponse { - keys: Array<{ - kty: string; - kid: string; - use: string; - alg: string; - n: string; - e: string; - }>; +interface Identity { + userId: string; + sessionId?: string; + email?: string; + name?: string; + aal?: string; + metadata?: Record; } -// JWKS cache — persists across requests in the same Worker isolate -let jwksCache: { keys: CryptoKey[]; fetchedAt: number } | null = null; -const JWKS_CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour +interface JsonWebKeyWithKid extends JsonWebKey { + kid?: string; + use?: string; +} -export default { - async fetch(request: Request, env: Env): Promise { - const url = new URL(request.url); +const IDENTITY_HEADERS = [ + "X-User-Id", + "X-User-Email", + "X-User-Name", + "X-Session-Id", + "X-Auth-AAL", + "X-User-Metadata", +]; - // Allow public paths - const publicPaths = (env.PUBLIC_PATHS || "/health").split(",").map(p => p.trim()); - if (publicPaths.some(p => url.pathname.startsWith(p))) { - return fetch(request); - } +const JWKS_TTL_MS = 60 * 60 * 1000; +const JWKS_MIN_REFRESH_INTERVAL_MS = 30 * 1000; +const SESSION_CACHE_TTL_MS = 30 * 1000; +const SESSION_CACHE_MAX_ENTRIES = 1000; - // Try session cookie first, then Bearer token - const cookie = request.headers.get("Cookie"); - const authHeader = request.headers.get("Authorization"); +let jwks: { keys: Map; fetchedAt: number } | null = null; +const sessionCache = new Map(); - if (!cookie && !authHeader) { - return handleUnauthorized(url.pathname, env); - } +class UpstreamError extends Error {} - // Mode 1: Session cookie validation via Ory's whoami - if (cookie && cookie.includes("ory_kratos_session")) { - return validateSession(request, cookie, env); - } +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); - // Mode 2: JWT Bearer token validation - if (authHeader && authHeader.startsWith("Bearer ")) { - return validateJwt(request, authHeader.slice(7), env); + if (isPublicPath(url.pathname, env)) { + return forward(request, null); } - return handleUnauthorized(url.pathname, env); + const authHeader = request.headers.get("Authorization") ?? ""; + const bearer = authHeader.startsWith("Bearer ") ? authHeader.slice(7).trim() : ""; + const sessionToken = request.headers.get("X-Session-Token") ?? ""; + const oryCookies = (request.headers.get("Cookie") ?? "") + .split(";") + .map((c) => c.trim()) + .filter((c) => c.startsWith("ory_session_") || c.startsWith("ory_kratos_session=")) + .join("; "); + + try { + let identity: Identity | null = null; + if (bearer && bearer.split(".").length === 3) { + identity = await validateJwt(bearer, env); + } else if (bearer || sessionToken) { + identity = await validateSession( + { "X-Session-Token": sessionToken || bearer }, + env, + ); + } else if (oryCookies) { + identity = await validateSession({ Cookie: oryCookies }, env); + } + + if (!identity) { + return unauthorized(request, env); + } + return forward(request, identity); + } catch (err) { + if (err instanceof UpstreamError) { + return json({ error: "Authentication service unavailable" }, 503); + } + return unauthorized(request, env); + } }, }; +function isPublicPath(pathname: string, env: Env): boolean { + return (env.PUBLIC_PATHS ?? "/health") + .split(",") + .map((p) => p.trim()) + .filter(Boolean) + .some((p) => pathname === p || pathname.startsWith(p + "/")); +} + async function validateSession( - request: Request, - cookie: string, + credential: Record, env: Env, -): Promise { +): Promise { + const cacheKey = await sha256Hex(JSON.stringify(credential)); + const cached = sessionCache.get(cacheKey); + if (cached && cached.expiresAt > Date.now()) { + return cached.identity; + } + sessionCache.delete(cacheKey); + + let res: Response; try { - const whoamiUrl = `${env.ORY_SDK_URL}/sessions/whoami`; - - // Cloudflare caches the whoami response per cf.cacheTtl below, keyed on the - // request URL + cookie — no need to construct a manual cache key. - const sessionRes = await fetch(whoamiUrl, { - headers: { - Cookie: cookie, - Accept: "application/json", - }, - // Cache the whoami response for 30 seconds at the edge - cf: { cacheTtl: 30, cacheEverything: true }, + res = await fetch(`${trimSlash(env.ORY_SDK_URL)}/sessions/whoami`, { + headers: { ...credential, Accept: "application/json" }, }); - - if (!sessionRes.ok) { - return handleUnauthorized(new URL(request.url).pathname, env); - } - - const session = (await sessionRes.json()) as OrySession; - if (!session.active) { - return handleUnauthorized(new URL(request.url).pathname, env); - } - - return forwardWithIdentityHeaders(request, session); } catch { - return new Response( - JSON.stringify({ error: "Authentication service unavailable" }), - { status: 503, headers: { "Content-Type": "application/json" } }, - ); + throw new UpstreamError(); } -} - -async function validateJwt( - request: Request, - token: string, - env: Env, -): Promise { - try { - // Parse JWT (header.payload.signature) — bail if shape isn't right. - const parts = token.split("."); - if (parts.length !== 3) { - return handleUnauthorized(new URL(request.url).pathname, env); - } - const [headerPart, payloadPart, signaturePart] = parts as [string, string, string]; - const header = JSON.parse(atob(headerPart.replace(/-/g, "+").replace(/_/g, "/"))); - - // Fetch or use cached JWKS - const publicKey = await getPublicKey(header.kid, env); - if (!publicKey) { - return handleUnauthorized(new URL(request.url).pathname, env); - } - - // Verify the JWT signature - const data = new TextEncoder().encode(`${headerPart}.${payloadPart}`); - const signature = base64UrlDecode(signaturePart); - const valid = await crypto.subtle.verify( - { name: "RSASSA-PKCS1-v1_5" }, - publicKey, - signature, - data, - ); - - if (!valid) { - return handleUnauthorized(new URL(request.url).pathname, env); - } - - // Decode and validate claims - const payload = JSON.parse( - atob(payloadPart.replace(/-/g, "+").replace(/_/g, "/")), - ); + if (res.status === 401 || res.status === 403) { + return null; + } + if (!res.ok) { + throw new UpstreamError(); + } - // Check expiration - if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) { - return handleUnauthorized(new URL(request.url).pathname, env); - } + const session = (await res.json()) as OrySession; + if (!session.active || !session.identity?.id) { + return null; + } - // Build a session-like object from JWT claims - const session: OrySession = { - id: payload.jti || "jwt", - active: true, - identity: { - id: payload.sub, - traits: { - email: payload.email || payload.ext?.email, - }, - metadata_public: payload.ext, - }, - }; + const traits = session.identity.traits ?? {}; + const identity: Identity = { + userId: session.identity.id, + sessionId: session.id, + email: traits.email, + name: [traits.name?.first, traits.name?.last].filter(Boolean).join(" ") || undefined, + aal: session.authenticator_assurance_level, + metadata: session.identity.metadata_public ?? undefined, + }; - return forwardWithIdentityHeaders(request, session); - } catch { - return handleUnauthorized(new URL(request.url).pathname, env); + if (sessionCache.size >= SESSION_CACHE_MAX_ENTRIES) { + const oldest = sessionCache.keys().next().value; + if (oldest !== undefined) sessionCache.delete(oldest); } + sessionCache.set(cacheKey, { identity, expiresAt: Date.now() + SESSION_CACHE_TTL_MS }); + return identity; } -async function getPublicKey(kid: string, env: Env): Promise { - // Return cached key if still fresh - if (jwksCache && Date.now() - jwksCache.fetchedAt < JWKS_CACHE_TTL_MS) { - return jwksCache.keys.find((_, i) => i === 0) || null; // Simplified +async function validateJwt(token: string, env: Env): Promise { + if (!env.JWT_AUDIENCE) { + return null; } - const jwksUrl = `${env.ORY_SDK_URL}/.well-known/jwks.json`; - const res = await fetch(jwksUrl, { - cf: { cacheTtl: 3600, cacheEverything: true }, - }); - - if (!res.ok) return null; + const [headerPart, payloadPart, signaturePart] = token.split(".") as [string, string, string]; + const header = JSON.parse(base64UrlDecodeString(headerPart)) as { alg?: unknown; kid?: unknown }; + if (header.alg !== "RS256" || typeof header.kid !== "string") { + return null; + } - const jwks = (await res.json()) as JWKSResponse; - const jwk = jwks.keys.find(k => k.kid === kid && k.use === "sig"); - if (!jwk) return null; + const key = await getPublicKey(header.kid, env); + if (!key) { + return null; + } - const key = await crypto.subtle.importKey( - "jwk", - jwk, - { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, - false, - ["verify"], + const valid = await crypto.subtle.verify( + { name: "RSASSA-PKCS1-v1_5" }, + key, + base64UrlDecode(signaturePart), + new TextEncoder().encode(`${headerPart}.${payloadPart}`), ); + if (!valid) { + return null; + } - jwksCache = { keys: [key], fetchedAt: Date.now() }; - return key; + const claims = JSON.parse(base64UrlDecodeString(payloadPart)) as Record; + const now = Math.floor(Date.now() / 1000); + const issuer = trimSlash(env.JWT_ISSUER || env.ORY_SDK_URL); + const audiences = Array.isArray(claims.aud) ? claims.aud : [claims.aud]; + + if (claims.iss !== issuer) return null; + if (!audiences.includes(env.JWT_AUDIENCE)) return null; + if (typeof claims.exp !== "number" || claims.exp <= now) return null; + if (claims.nbf !== undefined && (typeof claims.nbf !== "number" || claims.nbf > now)) return null; + if (typeof claims.sub !== "string" || !claims.sub) return null; + + const ext = claims.ext && typeof claims.ext === "object" ? (claims.ext as Record) : undefined; + return { + userId: claims.sub, + sessionId: typeof claims.jti === "string" ? claims.jti : undefined, + email: typeof ext?.email === "string" ? ext.email : undefined, + metadata: ext, + }; } -function forwardWithIdentityHeaders( - request: Request, - session: OrySession, -): Promise { - const headers = new Headers(request.headers); - - headers.set("X-User-Id", session.identity.id); - headers.set("X-Session-Id", session.id); +async function getPublicKey(kid: string, env: Env): Promise { + const now = Date.now(); + const fresh = jwks !== null && now - jwks.fetchedAt < JWKS_TTL_MS; + if (fresh && jwks!.keys.has(kid)) { + return jwks!.keys.get(kid)!; + } + // An unknown kid triggers a refetch so rotated keys are picked up, but at most + // once per interval so unknown kids can't force a JWKS fetch on every request. + if (fresh && now - jwks!.fetchedAt < JWKS_MIN_REFRESH_INTERVAL_MS) { + return null; + } - if (session.identity.traits.email) { - headers.set("X-User-Email", session.identity.traits.email); + let res: Response; + try { + res = await fetch(`${trimSlash(env.ORY_SDK_URL)}/.well-known/jwks.json`, { + cf: { cacheTtl: 300, cacheEverything: true }, + }); + } catch { + throw new UpstreamError(); + } + if (!res.ok) { + throw new UpstreamError(); } - const name = [ - session.identity.traits.name?.first, - session.identity.traits.name?.last, - ].filter(Boolean).join(" "); - if (name) { - headers.set("X-User-Name", name); + const body = (await res.json()) as { keys?: JsonWebKeyWithKid[] }; + const keys = new Map(); + for (const jwk of body.keys ?? []) { + if (jwk.kty !== "RSA" || !jwk.kid || (jwk.use && jwk.use !== "sig")) continue; + if (jwk.alg && jwk.alg !== "RS256") continue; + keys.set( + jwk.kid, + await crypto.subtle.importKey( + "jwk", + { kty: jwk.kty, n: jwk.n, e: jwk.e }, + { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, + false, + ["verify"], + ), + ); } + jwks = { keys, fetchedAt: now }; + return keys.get(kid) ?? null; +} - if (session.authenticator_assurance_level) { - headers.set("X-Auth-AAL", session.authenticator_assurance_level); +function forward(request: Request, identity: Identity | null): Promise { + const headers = new Headers(request.headers); + for (const name of IDENTITY_HEADERS) { + headers.delete(name); } - if (session.identity.metadata_public) { - headers.set("X-User-Metadata", JSON.stringify(session.identity.metadata_public)); + if (identity) { + headers.set("X-User-Id", identity.userId); + setIfHeaderSafe(headers, "X-Session-Id", identity.sessionId); + setIfHeaderSafe(headers, "X-User-Email", identity.email); + setIfHeaderSafe(headers, "X-User-Name", identity.name); + setIfHeaderSafe(headers, "X-Auth-AAL", identity.aal); + if (identity.metadata && Object.keys(identity.metadata).length > 0) { + headers.set("X-User-Metadata", asciiJson(identity.metadata)); + } } - // Forward to origin with enriched headers - const modifiedRequest = new Request(request, { headers }); - return fetch(modifiedRequest); + return fetch(new Request(request, { headers })); } -function handleUnauthorized(path: string, env: Env): Response { - if (env.LOGIN_REDIRECT_URL) { - return Response.redirect( - `${env.LOGIN_REDIRECT_URL}?return_to=${encodeURIComponent(path)}`, - 302, - ); +function setIfHeaderSafe(headers: Headers, name: string, value: string | undefined): void { + if (value && /^[\x20-\x7e]*$/.test(value)) { + headers.set(name, value); } - return new Response( - JSON.stringify({ error: "Unauthorized" }), - { status: 401, headers: { "Content-Type": "application/json" } }, +} + +function asciiJson(value: Record): string { + return JSON.stringify(value).replace( + /[\u007f-￿]/g, + (c) => "\\u" + c.charCodeAt(0).toString(16).padStart(4, "0"), ); } +function unauthorized(request: Request, env: Env): Response { + if (env.LOGIN_REDIRECT_URL && request.headers.get("Accept")?.includes("text/html")) { + const target = new URL(env.LOGIN_REDIRECT_URL); + target.searchParams.set("return_to", request.url); + return Response.redirect(target.toString(), 302); + } + return json({ error: "Unauthorized" }, 401); +} + +function json(body: unknown, status: number): Response { + return new Response(JSON.stringify(body), { + status, + headers: { "Content-Type": "application/json" }, + }); +} + +function trimSlash(url: string): string { + return url.replace(/\/+$/, ""); +} + +async function sha256Hex(input: string): Promise { + const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(input)); + return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join(""); +} + +function base64UrlDecodeString(str: string): string { + return new TextDecoder().decode(base64UrlDecode(str)); +} + function base64UrlDecode(str: string): ArrayBuffer { const base64 = str.replace(/-/g, "+").replace(/_/g, "/"); const padding = "=".repeat((4 - (base64.length % 4)) % 4); diff --git a/edge-token-validation/cloudflare-workers/wrangler.toml b/edge-token-validation/cloudflare-workers/wrangler.toml index cd868e1..70536d7 100644 --- a/edge-token-validation/cloudflare-workers/wrangler.toml +++ b/edge-token-validation/cloudflare-workers/wrangler.toml @@ -4,18 +4,20 @@ # BEFORE traffic reaches your origin server, reducing latency and offloading auth. name = "ory-edge-auth" -main = "dist/worker.js" +main = "src/worker.ts" compatibility_date = "2024-01-01" -# Environment variables -# IMPORTANT: Store ORY_SDK_URL as a secret, not a plain-text var. -# wrangler secret put ORY_SDK_URL [vars] +ORY_SDK_URL = "https://YOUR_PROJECT_SLUG.projects.oryapis.com" +# Required for JWT validation. Requests with a JWT are rejected when unset. +JWT_AUDIENCE = "https://api.example.com" +# Defaults to ORY_SDK_URL. Set it when your project uses a custom domain as issuer. +# JWT_ISSUER = "https://auth.example.com" PUBLIC_PATHS = "/health,/public,/favicon.ico" # LOGIN_REDIRECT_URL = "https://your-app.com/login" # For production, use different environments: # [env.production] -# vars = { PUBLIC_PATHS = "/health,/public" } +# vars = { ORY_SDK_URL = "https://prod-project.projects.oryapis.com", JWT_AUDIENCE = "https://api.example.com", PUBLIC_PATHS = "/health,/public" } # [env.staging] -# vars = { PUBLIC_PATHS = "/health,/public,/debug" } +# vars = { ORY_SDK_URL = "https://staging-project.projects.oryapis.com", JWT_AUDIENCE = "https://api.staging.example.com", PUBLIC_PATHS = "/health,/public,/debug" } diff --git a/edge-token-validation/fastly-compute/README.md b/edge-token-validation/fastly-compute/README.md index 38cbfa2..6813f5f 100644 --- a/edge-token-validation/fastly-compute/README.md +++ b/edge-token-validation/fastly-compute/README.md @@ -7,6 +7,25 @@ **Type:** session-validation (edge token validation pattern — see [`edge-token-validation/cloudflare-workers`](../cloudflare-workers/) for the canonical reference implementation) **Docs page:** [ory.com/docs/integrates-with/edge-token-validation/fastly-compute](https://www.ory.com/docs/integrates-with/edge-token-validation/fastly-compute) +## Prerequisites for JWT validation + +Ory issues opaque access tokens (`ory_at_…`) by default, and the `aud` claim is empty unless the client requests an audience. To issue JWTs that the Compute service can verify: + +```bash +# 1. Switch the project to JWT access tokens. +ory patch oauth2-config --project --replace '/strategies/access_token="jwt"' + +# 2. Create a client that may request your API's audience. +ory create oauth2-client --project --name "" \ + --grant-type client_credentials --audience https://api.example.com + +# 3. Request the audience when you get a token. +curl https://.projects.oryapis.com/oauth2/token -u ":" \ + -d grant_type=client_credentials -d audience=https://api.example.com +``` + +The token's `iss` claim is `https://.projects.oryapis.com`, without a trailing slash. The JWKS can hold more than one key, so select the key that matches the token's `kid` header. For the client credentials grant, `sub` is the client ID. See [JWT access tokens](https://www.ory.com/docs/oauth2-oidc/jwt-access-token). + ## Pattern Same shape as the Cloudflare Workers reference but compiled to WASM and using Fastly's host backends API. @@ -18,12 +37,14 @@ Same shape as the Cloudflare Workers reference but compiled to WASM and using Fa 5. On valid: inject `X-User-Id` header and forward to origin backend. 6. On invalid: synthesize a 401 response at the edge. +Apply the same validation as the [Cloudflare reference](../cloudflare-workers/#what-the-worker-checks): pin `RS256`, select the key by `kid` and refetch the JWKS on an unknown `kid`, verify `iss` (no trailing slash), `aud`, `exp`, and `nbf`, remove client-supplied identity headers before forwarding, and cache `whoami` responses only under a key derived from the session cookie or token. On Ory Network the session cookie is `ory_session_`, where `` is your project slug without dashes. + ## Fastly-specific bits -- **Backend registration** — the Ory hostname must be registered as a named Fastly backend in the service config; Compute@Edge cannot make ad-hoc outbound calls. +- **Backend registration** — register the Ory hostname as a named Fastly backend in the service config. To create backends at runtime instead, have Fastly enable dynamic backends for the service, then enable them in your application with the relevant SDK setting. See [Fastly's backend guide](https://www.fastly.com/documentation/guides/integrations/non-fastly-services/developer-guide-backends/). - **KV Store** for cross-region cache; per-execution memory only persists for one request. -- **Edge dictionaries** for non-secret config (Ory project URL, audience IDs); **Edge secrets** for the JWKS pinning info or service tokens. -- **Rust** is the most performant runtime; **JavaScript** (via Quick.js) is the easiest path if you're porting from the Cloudflare reference. +- **Config Store** for non-secret config (Ory project URL, audience IDs); **Secret Store** for secrets such as service tokens. +- **Rust** is the most performant runtime; **JavaScript** (SpiderMonkey compiled to WebAssembly) is the easiest path if you're porting from the Cloudflare reference. ## Status diff --git a/edge-token-validation/fastly-compute/registry.entry.yaml b/edge-token-validation/fastly-compute/registry.entry.yaml index e4c832c..fe4eeed 100644 --- a/edge-token-validation/fastly-compute/registry.entry.yaml +++ b/edge-token-validation/fastly-compute/registry.entry.yaml @@ -17,11 +17,11 @@ useCase: >- coreFunctionality: |- - WASM module (Rust / JS / AssemblyScript / TinyGo) fires on every request; reads session cookie or Authorization: Bearer. - Hydra JWT: verify signature against cached Ory JWKS using language-native JWT library compiled to WASM. - - Kratos session: call https://.projects.oryapis.com/sessions/whoami via a registered Fastly backend. Compute@Edge cannot make ad-hoc outbound calls — the Ory hostname MUST be registered as a named backend in the service config. + - Kratos session: call https://.projects.oryapis.com/sessions/whoami via a registered Fastly backend. Register the Ory hostname as a named backend in the service config, or have Fastly enable dynamic backends for the service and enable them in the SDK. - Caching: Fastly KV Store keyed by cookie hash for 30-60s; per-execution memory is request-scoped only. - - Edge dictionaries for non-secret config (Ory project URL, audience IDs); edge secrets for JWKS pinning / service tokens. + - Config Store for non-secret config (Ory project URL, audience IDs); Secret Store for service tokens. - On valid: inject X-User-Id header; on invalid: synthesize 401 at the edge. - - Rust is the most performant; JavaScript (via Quick.js) is the easiest path porting from the Cloudflare reference. + - Rust is the most performant; JavaScript (SpiderMonkey compiled to WebAssembly) is the easiest path porting from the Cloudflare reference. - Status: community/proposed — documented at ory.com/docs/integrates-with/edge-token-validation/fastly-compute. oryMechanism: - standards diff --git a/feature-flags/launchdarkly/README.md b/feature-flags/launchdarkly/README.md index cf1e7c9..5bf69c0 100644 --- a/feature-flags/launchdarkly/README.md +++ b/feature-flags/launchdarkly/README.md @@ -93,25 +93,24 @@ function buildLDContext(orySession) { name: [traits.name?.first, traits.name?.last].filter(Boolean).join(" "), firstName: traits.name?.first, lastName: traits.name?.last, - // Custom attributes from traits/metadata - plan: metadata.subscription_plan || "free", + // Custom attributes from metadata_public + tier: metadata.subscription_tier, + groups: metadata.groups, companyName: metadata.enrichment?.company?.name, companySize: metadata.enrichment?.company?.size, industry: metadata.enrichment?.company?.industry, country: traits.address?.country || metadata.enrichment?.person?.country, - hasMfa: - (identity.credentials?.totp?.identifiers?.length || 0) > 0 || - (identity.credentials?.webauthn?.identifiers?.length || 0) > 0, aal: orySession.authenticator_assurance_level, createdAt: identity.created_at, }, ]; // Add organization context if available - if (orySession.identity.organization_id) { + const orgId = metadata.org_id || identity.organization_id; + if (orgId) { contexts.push({ kind: "organization", - key: orySession.identity.organization_id, + key: orgId, name: metadata.organization_name, }); } @@ -130,7 +129,7 @@ function buildLDContext(orySession) { * Evaluate a feature flag using Ory session context. */ async function evaluateFlag(flagKey, orySession, defaultValue) { - await ldClient.waitForInitialization(); + await ldClient.waitForInitialization({ timeout: 10 }); const context = buildLDContext(orySession); return ldClient.variation(flagKey, context, defaultValue); } @@ -141,12 +140,15 @@ async function evaluateFlag(flagKey, orySession, defaultValue) { function featureFlagMiddleware(oryProjectUrl) { return async (req, res, next) => { try { - // Get Ory session - const sessionCookie = req.headers.cookie; + // Get the Ory session from a browser cookie or a session token + const headers = {}; + if (req.headers.cookie) headers.Cookie = req.headers.cookie; + if (req.headers.authorization) headers.Authorization = req.headers.authorization; + if (req.headers["x-session-token"]) headers["X-Session-Token"] = req.headers["x-session-token"]; const { data: session } = await axios.get( `${oryProjectUrl}/sessions/whoami`, { - headers: { Cookie: sessionCookie }, + headers, validateStatus: (s) => s === 200 || s === 401, } ); @@ -155,7 +157,7 @@ function featureFlagMiddleware(oryProjectUrl) { req.orySession = session; req.ldContext = buildLDContext(session); req.featureFlag = async (key, defaultValue) => { - await ldClient.waitForInitialization(); + await ldClient.waitForInitialization({ timeout: 10 }); return ldClient.variation(key, req.ldContext, defaultValue); }; } @@ -210,11 +212,11 @@ In the LaunchDarkly dashboard, create targeting rules that reference Ory identit | Rule | Attribute | Operator | Value | Variation | |------|-----------|----------|-------|-----------| -| Enterprise users | `plan` | is one of | `enterprise`, `business` | `true` | +| Enterprise users | `tier` | is one of | `enterprise`, `business` | `true` | +| Beta testers | `groups` | contains | `beta` | `true` | | Large companies | `companySize` | greater than | `500` | `true` | | Specific org | `organization.key` | is | `org-uuid-123` | `true` | -| MFA users only | `hasMfa` | is | `true` | `true` | -| High assurance | `aal` | is | `aal2` | `true` | +| High assurance (MFA) | `aal` | is | `aal2` | `true` | | Country rollout | `country` | is one of | `US`, `CA` | `true` | ### Step 5: Client-Side Integration (Optional) @@ -228,7 +230,7 @@ app.get("/api/feature-flags", async (req, res) => { return res.status(401).json({ error: "Not authenticated" }); } - await ldClient.waitForInitialization(); + await ldClient.waitForInitialization({ timeout: 10 }); const context = buildLDContext(req.orySession); const allFlags = await ldClient.allFlagsState(context); @@ -279,17 +281,17 @@ console.log(`new-dashboard flag: ${result}`); 1. Go to **Users** in LaunchDarkly dashboard 2. Search for your Ory identity ID -3. Verify attributes (email, plan, companySize, etc.) are present +3. Verify attributes (email, tier, groups, companySize, etc.) are present ## Troubleshooting | Issue | Cause | Solution | |-------|-------|----------| -| **All flags return defaults** | SDK not initialized | Await `ldClient.waitForInitialization()` | +| **All flags return defaults** | SDK not initialized | Await `ldClient.waitForInitialization({ timeout: 10 })` | | **Missing user attributes** | Session traits not populated | Verify Ory identity schema includes required traits | | **Context key is undefined** | Session not validated | Check that Ory session is active before building context | | **Targeting rules not matching** | Attribute type mismatch | Ensure numeric attributes are sent as numbers, not strings | -| **Organization context missing** | User not in an organization | Only add organization context when `organization_id` exists | +| **Organization context missing** | User not in an organization | Only add organization context when `metadata_public.org_id` or `organization_id` exists | ## Resources diff --git a/feature-flags/launchdarkly/registry.entry.yaml b/feature-flags/launchdarkly/registry.entry.yaml index 11d62c7..4fa67c0 100644 --- a/feature-flags/launchdarkly/registry.entry.yaml +++ b/feature-flags/launchdarkly/registry.entry.yaml @@ -17,13 +17,13 @@ description: >- multi-kind context shape so feature-flag evaluation rules can target on Ory traits, metadata_public, and authenticator_assurance_level without ad-hoc mapping in every call site. useCase: >- - A product wants feature flags that target on identity attributes (subscription plan, role, MFA status) sourced from + A product wants feature flags that target on identity attributes (subscription tier, groups, organization, AAL) sourced from Ory. Rather than writing the Ory → LaunchDarkly context mapping in every request handler, the helper centralizes the mapping into one class and exposes typed boolVariation / stringVariation / numberVariation / jsonVariation / allFlagsState methods that accept an Ory session directly. coreFunctionality: |- - OryLaunchDarkly class wraps an LDClient, takes Ory session shape (id, identity, traits, metadata_public, authenticator_assurance_level). - - Builds a LaunchDarkly multi-kind context: { user: {key=identity.id, email, firstName, lastName, plan, ...metadata}, session: {key=session.id, aal} }. + - Builds a LaunchDarkly multi-kind context: { user: {key=identity.id, email, firstName, lastName, tier=metadata_public.subscription_tier, groups, ...metadata}, organization: {key=metadata_public.org_id} when present, session: {key=session.id, aal} }. - boolVariation / stringVariation / numberVariation / jsonVariation pass through to LDClient.variation with the constructed context. - allFlagsState lets you bootstrap client-side flag state for the current Ory session in one call. - Ships as an importable library (peer-depends on @launchdarkly/node-server-sdk ^9) — not a deployed HTTP service. diff --git a/feature-flags/launchdarkly/src/index.ts b/feature-flags/launchdarkly/src/index.ts index 491132a..8114dfe 100644 --- a/feature-flags/launchdarkly/src/index.ts +++ b/feature-flags/launchdarkly/src/index.ts @@ -16,7 +16,8 @@ * false, // default value * ); * - * The wrapper maps Ory identity traits (email, plan, role, etc.) to + * The wrapper maps Ory identity traits and metadata_public (email, name, + * subscription_tier, groups, org_id) to * LaunchDarkly context attributes for targeting rules. * * SECURITY: Store LAUNCHDARKLY_SDK_KEY in a secret manager. @@ -46,6 +47,17 @@ export interface OrySession { authenticator_assurance_level?: string; } +const RESERVED_USER_ATTRIBUTES = new Set([ + "key", + "kind", + "_meta", + "anonymous", + "subscription_tier", + "org_id", + "billing", + "risk_assessment", +]); + /** * Ory + LaunchDarkly wrapper. * Maps Ory sessions to LaunchDarkly multi-context for flag evaluation. @@ -60,43 +72,43 @@ export class OryLaunchDarkly { /** * Build a LaunchDarkly multi-kind context from an Ory session. * - * Creates two context kinds: - * - "user": keyed by Ory identity ID, with traits as attributes - * - "session": keyed by session ID, with AAL and session metadata + * Creates up to three context kinds: + * - "user": keyed by Ory identity ID, with traits and metadata_public as + * attributes (`tier` from subscription_tier, `groups`) + * - "organization": keyed by metadata_public.org_id, when present + * - "session": keyed by session ID, with the AAL */ buildContext(session: OrySession): LDMultiKindContext { const identity = session.identity; const traits = identity.traits; - const metadata = identity.metadata_public || {}; + const metadata = identity.metadata_public ?? {}; - return { + const context: LDMultiKindContext = { kind: "multi", user: { + ...Object.fromEntries( + Object.entries(metadata).filter(([k]) => !RESERVED_USER_ATTRIBUTES.has(k)), + ), key: identity.id, email: traits.email, firstName: traits.name?.first, lastName: traits.name?.last, name: [traits.name?.first, traits.name?.last].filter(Boolean).join(" ") || undefined, - // Map metadata fields as custom attributes for targeting - plan: metadata.billing && typeof metadata.billing === "object" - ? (metadata.billing as any).plan - : undefined, - subscriptionStatus: metadata.billing && typeof metadata.billing === "object" - ? (metadata.billing as any).status - : undefined, - // Ory-specific attributes + tier: metadata.subscription_tier, + groups: metadata.groups, schemaId: identity.schema_id, createdAt: identity.created_at, - // Spread any other metadata as custom attributes - ...Object.fromEntries( - Object.entries(metadata).filter(([k]) => k !== "billing" && k !== "risk_assessment"), - ), } as LDContextCommon, session: { key: session.id, aal: session.authenticator_assurance_level, } as LDContextCommon, }; + + if (typeof metadata.org_id === "string" && metadata.org_id) { + context.organization = { key: metadata.org_id }; + } + return context; } /** Evaluate a boolean flag using the Ory session as context */ diff --git a/feature-flags/split-io/README.md b/feature-flags/split-io/README.md index 1b51be2..bdf9eef 100644 --- a/feature-flags/split-io/README.md +++ b/feature-flags/split-io/README.md @@ -12,20 +12,24 @@ This is **not a webhook integration** — feature evaluation happens in your application via the Split.io SDK. The integration is the convention for what to pass: ```typescript -// In your app, after Ory session resolution: -const session = await ory.sessions.toSession(); -const identity = session.data.identity; +// In your app, after Ory session resolution with @ory/client-fetch: +const session = await ory.toSession(); +const identity = session.identity!; +const meta = (identity.metadata_public ?? {}) as Record; const attributes = { email: identity.traits.email, - org_id: identity.metadata_public?.org_id, - subscription_tier: identity.metadata_public?.recurly?.subscription_tier, - // ... + org_id: meta.org_id, + tier: meta.subscription_tier, + groups: meta.groups, }; +await splitClient.whenReady(); const treatment = splitClient.getTreatment(identity.id, "my-feature", attributes); ``` +With `@ory/client`, `toSession()` returns an Axios response, so read the identity from `session.data.identity`. + ## What to pass to Split | Ory source | Split attribute | Use case | @@ -39,7 +43,8 @@ const treatment = splitClient.getTreatment(identity.id, "my-feature", attributes ## Notable - Pass **stable identifiers** as the `key` (use `identity.id`, not email — email can change). -- For server-side evaluation, use the Split server SDK; for client-side, use the Split JavaScript SDK and pass attributes via the `Track` API. +- Wait for `whenReady()` before you evaluate flags. Until the SDK is ready, `getTreatment` returns `control`. +- Use the Split server SDK on the server side and the Split JavaScript SDK on the client side. Pass attributes to `getTreatment`; in the JavaScript SDK you can also bind them once with `setAttributes()`. The `track()` method records events for metrics. It doesn't set attributes. - Split's **Impact measurement** features need event-stream integration too — that's a separate concern; see [`cdp-analytics/segment`](../../cdp-analytics/segment/) for event ingestion patterns. - Compare with [`feature-flags/launchdarkly`](../launchdarkly/) — same pattern, different vendor. diff --git a/feature-flags/split-io/registry.entry.yaml b/feature-flags/split-io/registry.entry.yaml index ca5f9d3..0e6b319 100644 --- a/feature-flags/split-io/registry.entry.yaml +++ b/feature-flags/split-io/registry.entry.yaml @@ -19,7 +19,7 @@ coreFunctionality: |- - sdk-client integration: not a webhook — application code resolves the Ory session, extracts attributes, and passes them to the Split SDK at evaluation time. - Stable key: use identity.id (not email) so bucketing is stable across identity changes. - Common attribute mapping: identity.traits.email → email; metadata_public.org_id → org_id; metadata_public.subscription_tier → tier; metadata_public.groups → groups. - - Server-side: Split server SDK; client-side: Split JavaScript SDK with Track API for attributes. + - Server-side: Split server SDK; client-side: Split JavaScript SDK. Pass attributes to getTreatment (or setAttributes in the JavaScript SDK) after whenReady(); track() records metric events, not attributes. - Impact measurement features need event-stream integration too — separate concern; see cdp-analytics/segment. - Status: community/proposed — convention guide, not turnkey code. oryMechanism: diff --git a/iac-devops/helm/README.md b/iac-devops/helm/README.md index 8f9e7b0..25354c1 100644 --- a/iac-devops/helm/README.md +++ b/iac-devops/helm/README.md @@ -22,6 +22,9 @@ Official Helm charts for deploying the self-hosted Ory stack (Kratos, Hydra, Ket helm repo add ory https://k8s.ory.sh/helm/charts helm repo update +helm show values ory/kratos > kratos-values.yaml +helm show values ory/hydra > hydra-values.yaml +# Edit the values files, then install the charts: helm install kratos ory/kratos --namespace ory --create-namespace -f kratos-values.yaml helm install hydra ory/hydra --namespace ory -f hydra-values.yaml ``` diff --git a/iac-devops/pulumi/README.md b/iac-devops/pulumi/README.md index 101ef00..ba95b71 100644 --- a/iac-devops/pulumi/README.md +++ b/iac-devops/pulumi/README.md @@ -13,7 +13,15 @@ Pulumi can consume any Terraform provider via [`pulumi-terraform-bridge`](https://github.com/pulumi/pulumi-terraform-bridge), turning the official [`ory/ory` Terraform provider](https://registry.terraform.io/providers/ory/ory) into a Pulumi-callable SDK. This is the closest you get to a first-class experience without a hand-written Pulumi provider. -The trade-off: you maintain the bridge build for your team or generate the SDK on demand; you do not get an official, regularly published Pulumi package. +Generate a local SDK from the Terraform provider: + +```bash +pulumi package add terraform-provider ory/ory +``` + +Replace `` with a provider release, for example `26.3.5`. In a TypeScript project, import the generated SDK as `@pulumi/ory`. `pulumi install` regenerates the SDK in a fresh checkout. + +The trade-off: there's no published Ory Pulumi package, so each project generates its own SDK. ### Option 2 — Call the Ory Network REST API from a `dynamic` Pulumi resource diff --git a/iac-devops/pulumi/registry.entry.yaml b/iac-devops/pulumi/registry.entry.yaml index 20c32ed..195a72e 100644 --- a/iac-devops/pulumi/registry.entry.yaml +++ b/iac-devops/pulumi/registry.entry.yaml @@ -16,7 +16,7 @@ useCase: >- same codebase and deployment pipeline as the rest of their infrastructure. coreFunctionality: |- - NO official Ory Pulumi provider exists today. - - Recommended path: bridge the official ory/ory Terraform provider via pulumi-terraform-bridge to generate a Pulumi-callable SDK. + - Recommended path: generate a local SDK from the official ory/ory Terraform provider with `pulumi package add terraform-provider ory/ory ` (pulumi-terraform-bridge); import it as @pulumi/ory in TypeScript. - Alternative: write a Pulumi `dynamic` provider that calls the Ory Network REST API using @ory/client (TypeScript) or ory-client (Go/Python). State diffing and CRUD logic is the implementer's responsibility. - Escape hatch: keep Ory resources in Terraform via the official provider (iac-devops/terraform) and only use Pulumi for the rest. oryMechanism: diff --git a/iac-devops/terraform/README.md b/iac-devops/terraform/README.md index d5cbc39..0687ef1 100644 --- a/iac-devops/terraform/README.md +++ b/iac-devops/terraform/README.md @@ -2,7 +2,7 @@ > **Maintained by:** Ory Engineering -Official Terraform provider for managing **Ory Network** resources as infrastructure-as-code — identity schemas, OAuth2 clients, project configuration, identities, organizations, permissions, and webhooks. Source at [github.com/ory/terraform-provider-ory](https://github.com/ory/terraform-provider-ory); registry source `ory/ory`. +Official Terraform provider for managing **Ory Network** resources as infrastructure-as-code — identity schemas, OAuth2 clients, project configuration, identities, organizations, permissions, and actions (webhooks). Source at [github.com/ory/terraform-provider-ory](https://github.com/ory/terraform-provider-ory); registry source `ory/ory`. **Type:** config (Terraform provider — no source code in this directory) **Docs page:** [ory.com/docs/integrates-with/iac-devops/terraform](https://www.ory.com/docs/integrates-with/iac-devops/terraform) @@ -13,32 +13,49 @@ Official Terraform provider for managing **Ory Network** resources as infrastruc ## Quick start +Set the provider's credentials and project in your environment: + +```bash +export ORY_WORKSPACE_API_KEY="" +export ORY_PROJECT_API_KEY="" +export ORY_PROJECT_ID="" +export ORY_PROJECT_SLUG="" +``` + +Use an empty provider block to read these values: + ```hcl terraform { required_providers { ory = { source = "ory/ory" - version = "~> 1.0" + version = "~> 26.3" } } } -provider "ory" { - # Or set the ORY_API_KEY env var. - api_key = var.ory_api_key - # Project slug or SDK URL — also via ORY_PROJECT / ORY_SDK_URL. - project = var.ory_project_slug -} +provider "ory" {} ``` -A canonical example layout (with `ory_identity_schema`, `ory_oauth2_client`, `ory_project_config`, `ory_webhook` resources and a multi-environment module) is in `examples/` alongside this README. +The provider arguments are `workspace_api_key`, `project_api_key`, `project_id`, and `project_slug`, if you prefer to set them in HCL. + +Examples in `examples/` alongside this README: + +- [`examples/basic`](examples/basic/) — `ory_project_config`, `ory_identity_schema`, `ory_oauth2_client`, and per-method `ory_action` webhooks with their Jsonnet request bodies. +- [`examples/multi-environment`](examples/multi-environment/) — one configuration applied to separate staging and production projects with per-environment `.tfvars` files. + +The [provider repository's `examples/` directory](https://github.com/ory/terraform-provider-ory/tree/main/examples) has an example for every resource. ## Notable behaviours - Many `ory_project_config` attributes were renamed to follow the OpenAPI spec naming convention. Old names still work but emit deprecation warnings; the [migration script](https://github.com/ory/terraform-provider-ory/blob/main/scripts/migrate-deprecated-attrs.sh) in the provider repo rewrites your `.tf` files. -- Ory uses two API key types — Workspace API keys and Project API keys. Use the right key for the right resource (Workspace keys can manage projects; Project keys can manage in-project resources). -- Existing resources (created via Console / CLI) can be adopted with `terraform import ory_. `. +- Ory uses two API key types, and each resource needs a specific one: + - A Workspace API key (`ory_wak_…`) manages projects, workspaces, organizations, identity schemas, project configuration, actions, social sign-in and SAML providers, SCIM clients, and email templates. + - A Project API key (`ory_pat_…`) manages identities, OAuth2 clients, and relationships. It can't read or change project configuration. +- To adopt resources created in the Ory Console or with the CLI, run `terraform import` with the import ID format from each resource's documentation. For example, `ory_oauth2_client` takes the client ID, and an `after` `ory_action` takes `::after:::` (a `before` action omits ``). Identity schemas can't be imported. ## License Apache-2.0. (Configuration / IaC — no source code in this directory.) + +The examples manage actions for each method in `auth_methods`; include only methods you enable and add `saml` when needed. Stripe registration runs with `response_ignore = true` after identity persistence. Segment and Splunk use separate body templates matching their companion handlers. diff --git a/iac-devops/terraform/examples/basic/jsonnet/login.jsonnet b/iac-devops/terraform/examples/basic/jsonnet/login.jsonnet new file mode 100644 index 0000000..68d3bce --- /dev/null +++ b/iac-devops/terraform/examples/basic/jsonnet/login.jsonnet @@ -0,0 +1,15 @@ +// Body for the Segment Ory Actions (registration, login, settings). The +// handler selects the Segment call by endpoint path. +function(ctx) { + identity: { + id: ctx.identity.id, + traits: ctx.identity.traits, + created_at: std.get(ctx.identity, 'created_at', null), + }, + flow: { + id: ctx.flow.id, + type: ctx.flow.type, + }, + client_ip: std.get(ctx.request_headers, 'True-Client-Ip', [null])[0], + user_agent: std.get(ctx.request_headers, 'User-Agent', [null])[0], +} diff --git a/iac-devops/terraform/examples/basic/jsonnet/registration.jsonnet b/iac-devops/terraform/examples/basic/jsonnet/registration.jsonnet new file mode 100644 index 0000000..9804133 --- /dev/null +++ b/iac-devops/terraform/examples/basic/jsonnet/registration.jsonnet @@ -0,0 +1,4 @@ +function(ctx) { + identity_id: ctx.identity.id, + email: std.get(ctx.identity.traits, 'email', null), +} diff --git a/iac-devops/terraform/examples/basic/main.tf b/iac-devops/terraform/examples/basic/main.tf index fe0c655..94fce42 100644 --- a/iac-devops/terraform/examples/basic/main.tf +++ b/iac-devops/terraform/examples/basic/main.tf @@ -1,12 +1,16 @@ # Terraform Configuration for Ory Network # # This example manages Ory Network project configuration as code, -# including identity schemas, OAuth2 clients, and webhook definitions. +# including identity schemas, OAuth2 clients, and webhook actions. # # Prerequisites: -# 1. Install the Ory Terraform provider -# 2. Set ORY_API_KEY environment variable (from Ory Console → Settings → API Keys) -# IMPORTANT: Store this key in a secret manager, not in .tfvars files. +# 1. Export the provider credentials (store the keys in a secret manager, +# not in .tfvars files): +# export ORY_WORKSPACE_API_KEY="ory_wak_..." # project config, schemas, actions +# export ORY_PROJECT_API_KEY="ory_pat_..." # OAuth2 clients, identities +# export ORY_PROJECT_ID="" +# export ORY_PROJECT_SLUG="" +# 2. export TF_VAR_webhook_secret="" # 3. terraform init && terraform plan terraform { @@ -14,22 +18,21 @@ terraform { required_providers { ory = { source = "ory/ory" - version = "~> 0.1" # Check https://registry.terraform.io/providers/ory/ory for latest + version = "~> 26.3" } } } -# The Ory provider authenticates via API key. -# SECURITY: Set ORY_API_KEY as an environment variable. -# export ORY_API_KEY="ory_pat_..." -# Or use Terraform's native secret management. +# Reads ORY_WORKSPACE_API_KEY, ORY_PROJECT_API_KEY, ORY_PROJECT_ID, and +# ORY_PROJECT_SLUG from the environment. provider "ory" {} # --- Variables --- -variable "project_id" { - description = "Ory Network project ID" - type = string +variable "auth_methods" { + description = "Authentication methods whose registration/login actions are managed" + type = set(string) + default = ["password", "oidc", "code", "passkey", "webauthn"] } variable "app_url" { @@ -52,40 +55,31 @@ variable "webhook_secret" { # --- Ory Project Configuration --- -resource "ory_project" "main" { - id = var.project_id - name = "My Application" - - # CORS configuration - cors_public { - enabled = true - origins = [ - var.app_url, - "http://localhost:3000", # Local development - ] - } +resource "ory_project_config" "main" { + cors_enabled = true + cors_origins = [ + var.app_url, + "http://localhost:3000", # Local development + ] - # Custom UI URLs - selfservice_ui { - login_url = "${var.app_url}/auth/login" - registration_url = "${var.app_url}/auth/registration" - recovery_url = "${var.app_url}/auth/recovery" - verification_url = "${var.app_url}/auth/verification" - settings_url = "${var.app_url}/auth/settings" - error_url = "${var.app_url}/auth/error" - } + selfservice_flows_login_ui_url = "${var.app_url}/auth/login" + selfservice_flows_registration_ui_url = "${var.app_url}/auth/registration" + selfservice_flows_recovery_ui_url = "${var.app_url}/auth/recovery" + selfservice_flows_verification_ui_url = "${var.app_url}/auth/verification" + selfservice_flows_settings_ui_url = "${var.app_url}/auth/settings" + selfservice_flows_error_ui_url = "${var.app_url}/auth/error" } # --- Identity Schema --- -resource "ory_identity_schema" "default" { - project_id = var.project_id - name = "default" +resource "ory_identity_schema" "customer" { + schema_id = "customer" + set_default = true schema = jsonencode({ - "$id" = "https://schemas.ory.sh/presets/kratos/identity.basic.schema.json" + "$id" = "https://example.com/schemas/customer.schema.json" "$schema" = "http://json-schema.org/draft-07/schema#" - title = "User" + title = "Customer" type = "object" properties = { traits = { @@ -111,7 +105,7 @@ resource "ory_identity_schema" "default" { } } } - required = ["email"] + required = ["email"] additionalProperties = false } } @@ -121,10 +115,8 @@ resource "ory_identity_schema" "default" { # --- OAuth2 Client (for your application) --- resource "ory_oauth2_client" "app" { - project_id = var.project_id - - client_name = "My Application" - grant_types = ["authorization_code", "refresh_token"] + client_name = "My Application" + grant_types = ["authorization_code", "refresh_token"] response_types = ["code"] redirect_uris = [ @@ -137,12 +129,10 @@ resource "ory_oauth2_client" "app" { ] scope = "openid offline_access email profile" - audience = ["${var.app_url}"] + audience = [var.app_url] token_endpoint_auth_method = "client_secret_post" - - # Access token settings - access_token_strategy = "jwt" + access_token_strategy = "jwt" metadata = jsonencode({ environment = "production" @@ -154,59 +144,40 @@ resource "ory_oauth2_client" "app" { # Stripe integration — create customer on registration resource "ory_action" "stripe_registration" { - project_id = var.project_id - flow = "registration" - hook = "after" - - config = jsonencode({ - hook = "web_hook" - config = { - url = "${var.webhook_base_url}/webhooks/stripe/registration" - method = "POST" - body = base64encode(file("${path.module}/jsonnet/registration.jsonnet")) - response = { - parse = true - ignore = false - } - can_interrupt = false - auth = { - type = "api_key" - config = { - name = "X-Webhook-Secret" - value = var.webhook_secret - in = "header" - } - } - } - }) + for_each = var.auth_methods + + flow = "registration" + timing = "after" + auth_method = each.value + url = "${var.webhook_base_url}/stripe/registration" + method = "POST" + body = file("${path.module}/jsonnet/registration.jsonnet") + response_ignore = true + can_interrupt = false + + webhook_auth_type = "api_key" + webhook_auth_api_key_name = "X-Webhook-Secret" + webhook_auth_api_key_value = var.webhook_secret + webhook_auth_api_key_in = "header" } # Segment integration — track login events resource "ory_action" "segment_login" { - project_id = var.project_id - flow = "login" - hook = "after" - - config = jsonencode({ - hook = "web_hook" - config = { - url = "${var.webhook_base_url}/webhooks/segment/login" - method = "POST" - body = base64encode(file("${path.module}/jsonnet/login.jsonnet")) - response = { - ignore = true - } - can_interrupt = false - auth = { - type = "api_key" - config = { - name = "X-Webhook-Secret" - value = var.webhook_secret - in = "header" - } - } - } - }) + for_each = var.auth_methods + + flow = "login" + timing = "after" + auth_method = each.value + url = "${var.webhook_base_url}/segment/login" + method = "POST" + body = file("${path.module}/jsonnet/login.jsonnet") + response_ignore = true + can_interrupt = false + + webhook_auth_type = "api_key" + webhook_auth_api_key_name = "X-Webhook-Secret" + webhook_auth_api_key_value = var.webhook_secret + webhook_auth_api_key_in = "header" } # --- Outputs --- diff --git a/iac-devops/terraform/examples/basic/terraform.tfvars.example b/iac-devops/terraform/examples/basic/terraform.tfvars.example index 5034be3..b2a3bb1 100644 --- a/iac-devops/terraform/examples/basic/terraform.tfvars.example +++ b/iac-devops/terraform/examples/basic/terraform.tfvars.example @@ -1,8 +1,10 @@ # Copy this to terraform.tfvars and fill in your values. # IMPORTANT: Never commit terraform.tfvars to version control. # Add it to .gitignore. +# +# The project and API keys come from ORY_PROJECT_ID, ORY_PROJECT_SLUG, +# ORY_WORKSPACE_API_KEY, and ORY_PROJECT_API_KEY in your environment. -project_id = "your-ory-project-id" app_url = "https://app.example.com" webhook_base_url = "https://webhooks.example.com" diff --git a/iac-devops/terraform/examples/multi-environment/jsonnet/login.jsonnet b/iac-devops/terraform/examples/multi-environment/jsonnet/login.jsonnet new file mode 100644 index 0000000..68d3bce --- /dev/null +++ b/iac-devops/terraform/examples/multi-environment/jsonnet/login.jsonnet @@ -0,0 +1,15 @@ +// Body for the Segment Ory Actions (registration, login, settings). The +// handler selects the Segment call by endpoint path. +function(ctx) { + identity: { + id: ctx.identity.id, + traits: ctx.identity.traits, + created_at: std.get(ctx.identity, 'created_at', null), + }, + flow: { + id: ctx.flow.id, + type: ctx.flow.type, + }, + client_ip: std.get(ctx.request_headers, 'True-Client-Ip', [null])[0], + user_agent: std.get(ctx.request_headers, 'User-Agent', [null])[0], +} diff --git a/iac-devops/terraform/examples/multi-environment/jsonnet/registration.jsonnet b/iac-devops/terraform/examples/multi-environment/jsonnet/registration.jsonnet new file mode 100644 index 0000000..9804133 --- /dev/null +++ b/iac-devops/terraform/examples/multi-environment/jsonnet/registration.jsonnet @@ -0,0 +1,4 @@ +function(ctx) { + identity_id: ctx.identity.id, + email: std.get(ctx.identity.traits, 'email', null), +} diff --git a/iac-devops/terraform/examples/multi-environment/jsonnet/splunk-login.jsonnet b/iac-devops/terraform/examples/multi-environment/jsonnet/splunk-login.jsonnet new file mode 100644 index 0000000..9457693 --- /dev/null +++ b/iac-devops/terraform/examples/multi-environment/jsonnet/splunk-login.jsonnet @@ -0,0 +1,19 @@ +// Body for the Splunk Ory Actions (registration, login, recovery, settings). +// The handler selects the event signature by endpoint path. Only login and +// settings actions carry a session. +function(ctx) + local session = std.get(ctx, 'session', null); + local methods = if session == null then null else std.get(session, 'authentication_methods', null); + { + identity: { + id: ctx.identity.id, + traits: ctx.identity.traits, + }, + flow: { + id: ctx.flow.id, + type: ctx.flow.type, + }, + authentication_methods: if methods == null then [] else [m.method for m in methods], + client_ip: std.get(ctx.request_headers, 'True-Client-Ip', [null])[0], + user_agent: std.get(ctx.request_headers, 'User-Agent', [null])[0], + } diff --git a/iac-devops/terraform/examples/multi-environment/main.tf b/iac-devops/terraform/examples/multi-environment/main.tf index 249243d..f624bcd 100644 --- a/iac-devops/terraform/examples/multi-environment/main.tf +++ b/iac-devops/terraform/examples/multi-environment/main.tf @@ -1,9 +1,9 @@ # Multi-Environment Terraform Configuration for Ory Network # # Manages separate Ory projects for staging and production -# using Terraform workspaces or separate .tfvars files. +# using Terraform workspaces and separate .tfvars files. # -# Usage: +# Export ORY_WORKSPACE_API_KEY (ory_wak_...) and TF_VAR_webhook_secret, then: # terraform workspace new staging # terraform apply -var-file=staging.tfvars # @@ -15,7 +15,7 @@ terraform { required_providers { ory = { source = "ory/ory" - version = "~> 0.1" + version = "~> 26.3" } } @@ -27,10 +27,18 @@ terraform { # } } -provider "ory" {} +provider "ory" { + project_id = var.project_id +} # --- Variables --- +variable "auth_methods" { + description = "Authentication methods whose registration/login actions are managed" + type = set(string) + default = ["password", "oidc", "code", "passkey", "webauthn"] +} + variable "environment" { description = "Environment name (staging, production)" type = string @@ -79,7 +87,6 @@ variable "enable_splunk_webhook" { locals { is_production = var.environment == "production" - project_name = "MyApp - ${title(var.environment)}" # Only enable debug-friendly CORS origins in non-production cors_origins = concat( @@ -88,110 +95,78 @@ locals { ) } -# --- Project --- - -resource "ory_project" "main" { - id = var.project_id - name = local.project_name +# --- Project Configuration --- - cors_public { - enabled = true - origins = local.cors_origins - } +resource "ory_project_config" "main" { + cors_enabled = true + cors_origins = local.cors_origins - selfservice_ui { - login_url = "${var.app_url}/auth/login" - registration_url = "${var.app_url}/auth/registration" - recovery_url = "${var.app_url}/auth/recovery" - verification_url = "${var.app_url}/auth/verification" - settings_url = "${var.app_url}/auth/settings" - error_url = "${var.app_url}/auth/error" - } + selfservice_flows_login_ui_url = "${var.app_url}/auth/login" + selfservice_flows_registration_ui_url = "${var.app_url}/auth/registration" + selfservice_flows_recovery_ui_url = "${var.app_url}/auth/recovery" + selfservice_flows_verification_ui_url = "${var.app_url}/auth/verification" + selfservice_flows_settings_ui_url = "${var.app_url}/auth/settings" + selfservice_flows_error_ui_url = "${var.app_url}/auth/error" } # --- Conditional Webhooks --- resource "ory_action" "stripe_registration" { - count = var.enable_stripe_webhook ? 1 : 0 - project_id = var.project_id - flow = "registration" - hook = "after" - - config = jsonencode({ - hook = "web_hook" - config = { - url = "${var.webhook_base_url}/webhooks/stripe/registration" - method = "POST" - response = { parse = true, ignore = false } - can_interrupt = false - auth = { - type = "api_key" - config = { - name = "X-Webhook-Secret" - value = var.webhook_secret - in = "header" - } - } - } - }) + for_each = var.enable_stripe_webhook ? var.auth_methods : toset([]) + + flow = "registration" + timing = "after" + auth_method = each.value + url = "${var.webhook_base_url}/stripe/registration" + method = "POST" + body = file("${path.module}/jsonnet/registration.jsonnet") + response_ignore = true + can_interrupt = false + + webhook_auth_type = "api_key" + webhook_auth_api_key_name = "X-Webhook-Secret" + webhook_auth_api_key_value = var.webhook_secret + webhook_auth_api_key_in = "header" } resource "ory_action" "segment_login" { - count = var.enable_segment_webhook ? 1 : 0 - project_id = var.project_id - flow = "login" - hook = "after" - - config = jsonencode({ - hook = "web_hook" - config = { - url = "${var.webhook_base_url}/webhooks/segment/login" - method = "POST" - response = { ignore = true } - can_interrupt = false - auth = { - type = "api_key" - config = { - name = "X-Webhook-Secret" - value = var.webhook_secret - in = "header" - } - } - } - }) + for_each = var.enable_segment_webhook ? var.auth_methods : toset([]) + + flow = "login" + timing = "after" + auth_method = each.value + url = "${var.webhook_base_url}/segment/login" + method = "POST" + body = file("${path.module}/jsonnet/login.jsonnet") + response_ignore = true + can_interrupt = false + + webhook_auth_type = "api_key" + webhook_auth_api_key_name = "X-Webhook-Secret" + webhook_auth_api_key_value = var.webhook_secret + webhook_auth_api_key_in = "header" } resource "ory_action" "splunk_login" { - count = var.enable_splunk_webhook ? 1 : 0 - project_id = var.project_id - flow = "login" - hook = "after" - - config = jsonencode({ - hook = "web_hook" - config = { - url = "${var.webhook_base_url}/webhooks/splunk/login" - method = "POST" - response = { ignore = true } - can_interrupt = false - auth = { - type = "api_key" - config = { - name = "X-Webhook-Secret" - value = var.webhook_secret - in = "header" - } - } - } - }) -} + for_each = var.enable_splunk_webhook ? var.auth_methods : toset([]) -# --- Outputs --- + flow = "login" + timing = "after" + auth_method = each.value + url = "${var.webhook_base_url}/splunk/login" + method = "POST" + body = file("${path.module}/jsonnet/splunk-login.jsonnet") + response_ignore = true + can_interrupt = false -output "project_name" { - value = local.project_name + webhook_auth_type = "api_key" + webhook_auth_api_key_name = "X-Webhook-Secret" + webhook_auth_api_key_value = var.webhook_secret + webhook_auth_api_key_in = "header" } +# --- Outputs --- + output "environment" { value = var.environment } diff --git a/iac-devops/terraform/registry.entry.yaml b/iac-devops/terraform/registry.entry.yaml index 7e8c406..63ae273 100644 --- a/iac-devops/terraform/registry.entry.yaml +++ b/iac-devops/terraform/registry.entry.yaml @@ -10,18 +10,18 @@ maintainedBy: Ory Engineering description: >- Officially maintained Terraform provider for managing Ory Network resources as infrastructure-as-code — identity schemas, OAuth2 clients, project configuration, identities, organizations, permissions, and - webhooks. Source at github.com/ory/terraform-provider-ory; published on the Terraform Registry as `ory/ory` + actions (webhooks). Source at github.com/ory/terraform-provider-ory; published on the Terraform Registry as `ory/ory` and on the OpenTofu registry (search.opentofu.org/provider/ory/ory) under the same source — works with `tofu` as well as `terraform`. Targets Ory Network only — does not manage self-hosted Ory deployments. useCase: >- A platform team running Ory Network wants their identity and OAuth2 configuration in Git (not the Console) for code review, drift detection, and per-environment (dev/staging/prod) reproducibility. coreFunctionality: |- - - Registry source: ory/ory. Authentication via the `api_key` provider attribute or ORY_API_KEY env var; project via `project` or ORY_PROJECT/ORY_SDK_URL. - - Resources cover identity schemas (ory_identity_schema), OAuth2 clients (ory_oauth2_client), project config (ory_project_config), identities, organizations, permissions, and webhooks (ory_webhook). + - Registry source: ory/ory (version ~> 26.3). Authentication via the workspace_api_key / project_api_key provider arguments or ORY_WORKSPACE_API_KEY / ORY_PROJECT_API_KEY env vars; project via project_id / project_slug or ORY_PROJECT_ID / ORY_PROJECT_SLUG. + - Resources cover identity schemas (ory_identity_schema), OAuth2 clients (ory_oauth2_client), project config (ory_project_config), identities, organizations, permissions, and actions/webhooks (ory_action). - Many ory_project_config attributes were renamed to match OpenAPI spec; old names still work but emit deprecation warnings. The provider repo ships a migrate-deprecated-attrs.sh script for bulk rewrites. - - Two API key types in Ory — Workspace keys (manage projects) vs Project keys (manage in-project resources); pick the right scope. - - Existing resources can be adopted via `terraform import ory_. `. + - Two API key types: Workspace keys (ory_wak_) manage projects, project configuration, identity schemas, actions, social/SAML providers, SCIM clients, and email templates; Project keys (ory_pat_) manage identities, OAuth2 clients, and relationships. + - Existing resources can be adopted with `terraform import` using each resource's import ID format (after-hook ory_action: project_id:flow:after:auth_method:method:url; before-hooks omit auth_method); identity schemas can't be imported. - Provider is for Ory Network only — does NOT manage self-hosted Kratos/Hydra/Keto/Oathkeeper. oryMechanism: - standards diff --git a/monitoring-observability/datadog/README.md b/monitoring-observability/datadog/README.md index 66f77f1..6af6a7e 100644 --- a/monitoring-observability/datadog/README.md +++ b/monitoring-observability/datadog/README.md @@ -15,15 +15,34 @@ Monitor self-hosted Ory in Datadog over OpenTelemetry. Ory's tracer emits OTLP; ## Ingestion paths -1. **Datadog Agent OTLP receiver** (recommended). Run the agent with `otlp_config.receiver.protocols.{grpc,http}` enabled (ports 4317 / 4318) and point Ory's OTLP exporter at it. -2. **Datadog OTLP intake** (agentless). Ship straight to Datadog with `DD-API-KEY` header — endpoints per region: - -| Region | OTLP gRPC | OTLP HTTP | -| --- | --- | --- | -| US1 | `otlp.datadoghq.com:4317` | `otlp.datadoghq.com:4318` | -| US3 | `otlp.us3.datadoghq.com:4317` | `otlp.us3.datadoghq.com:4318` | -| US5 | `otlp.us5.datadoghq.com:4317` | `otlp.us5.datadoghq.com:4318` | -| EU | `otlp.datadoghq.eu:4317` | `otlp.datadoghq.eu:4318` | +Ory's exporter sends OTLP over HTTP only. It can't use gRPC endpoints on port 4317. + +1. **Datadog Agent OTLP receiver** (recommended). Enable the agent's OTLP HTTP receiver (`otlp_config.receiver.protocols.http`, port 4318) and point Ory's exporter at it: + + ```bash + TRACING_PROVIDER=otel + TRACING_PROVIDERS_OTLP_SERVER_URL=datadog-agent:4318 + TRACING_PROVIDERS_OTLP_INSECURE=true + ``` + +2. **Datadog OTLP intake** (agentless). Send traces straight to Datadog over HTTPS on port 443 with the `dd-api-key` header: + + ```bash + TRACING_PROVIDER=otel + TRACING_PROVIDERS_OTLP_SERVER_URL=otlp.datadoghq.com:443 + OTEL_EXPORTER_OTLP_HEADERS=dd-api-key=${DD_API_KEY},compute_stats=true + ``` + + `compute_stats=true` turns on trace metrics, which the intake doesn't compute by default. Endpoints per region: + +| Region | `TRACING_PROVIDERS_OTLP_SERVER_URL` | +| --- | --- | +| US1 | `otlp.datadoghq.com:443` | +| US3 | `otlp.us3.datadoghq.com:443` | +| US5 | `otlp.us5.datadoghq.com:443` | +| EU | `otlp.datadoghq.eu:443` | + +`TRACING_PROVIDERS_OTLP_SERVER_URL` takes a host and port without a scheme. Set `TRACING_SERVICE_NAME` per service (for example `ory-kratos`); Ory ignores `OTEL_EXPORTER_OTLP_ENDPOINT` and `OTEL_SERVICE_NAME`. For Prometheus metrics, the Datadog Agent's OpenMetrics check can scrape `/metrics/prometheus` on each Ory service. diff --git a/monitoring-observability/datadog/registry.entry.yaml b/monitoring-observability/datadog/registry.entry.yaml index b4eaea3..ce34366 100644 --- a/monitoring-observability/datadog/registry.entry.yaml +++ b/monitoring-observability/datadog/registry.entry.yaml @@ -18,7 +18,7 @@ useCase: >- coreFunctionality: |- - There is no `datadog` tracing provider: TRACING_PROVIDER accepts otel, jaeger, or zipkin. Reach Datadog with TRACING_PROVIDER=otel over OTLP. - Ory's span helper sets Datadog-convention error attributes (error.message, error.stack, error.type), so OTLP traces render correctly in Datadog APM. - - Ingest either via the Datadog Agent's OTLP receiver (otlp_config.receiver, ports 4317/4318) or directly to Datadog's OTLP intake (DD-API-KEY header, region-specific endpoints). + - Ory's exporter is OTLP over HTTP only. Ingest either via the Datadog Agent's OTLP HTTP receiver (port 4318) or directly to Datadog's OTLP intake on port 443 (dd-api-key header via OTEL_EXPORTER_OTLP_HEADERS, region-specific hosts). Configure with TRACING_PROVIDERS_OTLP_SERVER_URL (host:port, no scheme). - Prometheus metrics on /metrics/prometheus (each Ory service, admin port); ingest via the Datadog Agent's OpenMetrics check. - Ory Network: not available — managed project config excludes operational settings (tracing, logging, port). oryMechanism: diff --git a/monitoring-observability/new-relic/README.md b/monitoring-observability/new-relic/README.md index ae5b88a..d7c1ca0 100644 --- a/monitoring-observability/new-relic/README.md +++ b/monitoring-observability/new-relic/README.md @@ -11,25 +11,28 @@ New Relic ingests OpenTelemetry over OTLP/HTTP. Self-hosted Ory products (Ory Kr | Setting | Value | | --- | --- | -| OTLP endpoint (US) | `https://otlp.nr-data.net:4318` | -| OTLP endpoint (EU) | `https://otlp.eu01.nr-data.net:4318` | +| OTLP endpoint (US) | `otlp.nr-data.net:4318` | +| OTLP endpoint (EU) | `otlp.eu01.nr-data.net:4318` | | Auth header | `api-key: ` | -| Protocols | OTLP/HTTP (recommended), OTLP/gRPC supported | +| Protocol | OTLP/HTTP (Ory's exporter doesn't support gRPC) | | Signals | traces (metrics come from the Prometheus endpoint, not OTLP) | ## Setup -Set OTel exporter env vars on each service: +Set these environment variables on each service: ```bash TRACING_PROVIDER=otel -OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp.nr-data.net:4318 -OTEL_EXPORTER_OTLP_HEADERS=api-key= -OTEL_SERVICE_NAME=ory-kratos # or ory-hydra / ory-keto / ory-oathkeeper -OTEL_RESOURCE_ATTRIBUTES=service.namespace=identity,deployment.environment=production +TRACING_PROVIDERS_OTLP_SERVER_URL=otlp.nr-data.net:4318 +OTEL_EXPORTER_OTLP_HEADERS=api-key=${NEW_RELIC_LICENSE_KEY} +TRACING_SERVICE_NAME=ory-kratos # or ory-hydra / ory-keto / ory-oathkeeper +TRACING_DEPLOYMENT_ENVIRONMENT=production +OTEL_RESOURCE_ATTRIBUTES=service.namespace=identity TRACING_PROVIDERS_OTLP_SAMPLING_SAMPLING_RATIO=1.0 # tune in production ``` +`TRACING_PROVIDERS_OTLP_SERVER_URL` takes a host and port without a scheme, and Ory connects over HTTPS. Ory ignores `OTEL_EXPORTER_OTLP_ENDPOINT` and `OTEL_SERVICE_NAME`, so use the `TRACING_*` variables instead. + For production, prefer running an OpenTelemetry Collector in front so batching, retries, and sampling happen in one place. ## Notes diff --git a/monitoring-observability/new-relic/registry.entry.yaml b/monitoring-observability/new-relic/registry.entry.yaml index c9137f3..c07a3c9 100644 --- a/monitoring-observability/new-relic/registry.entry.yaml +++ b/monitoring-observability/new-relic/registry.entry.yaml @@ -17,9 +17,9 @@ useCase: >- observability data — to debug latency in login/registration/MFA, alert on auth-error rates, and correlate identity events with downstream service calls. coreFunctionality: |- - - OTLP/HTTP endpoints: otlp.nr-data.net:4318 (US) or otlp.eu01.nr-data.net:4318 (EU). OTLP/gRPC also supported. + - OTLP/HTTP endpoints: otlp.nr-data.net:4318 (US) or otlp.eu01.nr-data.net:4318 (EU). Ory's exporter uses OTLP/HTTP only. - Auth via header `api-key: ` — use Account/Ingest-scope license keys, NEVER full user keys. - - Self-hosted Ory: standard OTel env vars (TRACING_PROVIDER=otel, OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS, OTEL_SERVICE_NAME). + - Self-hosted Ory: TRACING_PROVIDER=otel, TRACING_PROVIDERS_OTLP_SERVER_URL=otlp.nr-data.net:4318 (host:port, no scheme), OTEL_EXPORTER_OTLP_HEADERS=api-key=${NEW_RELIC_LICENSE_KEY}, TRACING_SERVICE_NAME. Ory ignores OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_SERVICE_NAME. - Ory Network: not available — managed project config excludes operational settings (tracing, logging, port). - For production, prefer an OpenTelemetry Collector in front for batching/retries/sampling. - New Relic enforces attribute count/size limits — identity.id fine, identity.traits as flat blob not. Sample at SDK/collector level, not backend side, to preserve head-of-trace context. diff --git a/monitoring-observability/opentelemetry/README.md b/monitoring-observability/opentelemetry/README.md index daf0f39..0975fc1 100644 --- a/monitoring-observability/opentelemetry/README.md +++ b/monitoring-observability/opentelemetry/README.md @@ -16,17 +16,24 @@ The OTLP exporter carries **traces**. Metrics are exposed separately on each ser ## Self-hosted Ory — native OTLP exporter -Each Ory product accepts standard OTel environment variables to ship traces directly to a collector or backend: +Each Ory product reads its exporter settings from the `tracing` configuration, which you can set with environment variables to ship traces directly to a collector or backend: ```bash TRACING_PROVIDER=otel -OTEL_EXPORTER_OTLP_ENDPOINT=https://otel-collector.example.com:4318 -OTEL_SERVICE_NAME=ory-kratos # or hydra/keto/oathkeeper -OTEL_RESOURCE_ATTRIBUTES=service.namespace=identity,deployment.environment=production +TRACING_PROVIDERS_OTLP_SERVER_URL=otel-collector.example.com:4318 +TRACING_PROVIDERS_OTLP_INSECURE=true +TRACING_SERVICE_NAME=ory-kratos # or ory-hydra / ory-keto / ory-oathkeeper +TRACING_DEPLOYMENT_ENVIRONMENT=production +OTEL_RESOURCE_ATTRIBUTES=service.namespace=identity TRACING_PROVIDERS_OTLP_SAMPLING_SAMPLING_RATIO=1.0 # tune in production ``` -`TRACING_PROVIDER` accepts `otel`, `jaeger`, or `zipkin`. Use `otel` for OTLP. +- `TRACING_PROVIDER` accepts `otel`, `jaeger`, or `zipkin`. Use `otel` for OTLP. +- The exporter sends OTLP over HTTP only, so point `TRACING_PROVIDERS_OTLP_SERVER_URL` at the collector's OTLP/HTTP port, 4318 by default. The value is a host and port without a scheme. +- Ory connects over HTTPS. Set `TRACING_PROVIDERS_OTLP_INSECURE=true` only for a collector that accepts plain HTTP. +- Ory ignores `OTEL_EXPORTER_OTLP_ENDPOINT` and `OTEL_SERVICE_NAME`. +- Ory merges `OTEL_RESOURCE_ATTRIBUTES` into its resource, but `service.name` and `deployment.environment.name` always come from `TRACING_SERVICE_NAME` and `TRACING_DEPLOYMENT_ENVIRONMENT`. +- Ory applies `OTEL_EXPORTER_OTLP_HEADERS` unless `TRACING_PROVIDERS_OTLP_AUTHORIZATION_HEADER` is set, which replaces those headers. ## Recommended pipeline diff --git a/monitoring-observability/opentelemetry/registry.entry.yaml b/monitoring-observability/opentelemetry/registry.entry.yaml index d5601a9..d5f7b14 100644 --- a/monitoring-observability/opentelemetry/registry.entry.yaml +++ b/monitoring-observability/opentelemetry/registry.entry.yaml @@ -9,13 +9,13 @@ type: config maintainedBy: Ory Engineering description: >- Ory natively supports OpenTelemetry across all self-hosted products — listed as the first/recommended - tracing backend in the Ory observability docs. Self-hosted products ship OTLP via standard - OTEL_EXPORTER_OTLP_* env vars. Self-hosted only: Ory Network does not export OTLP to customer collectors. + tracing backend in the Ory observability docs. Self-hosted products ship OTLP/HTTP configured + through the tracing config (TRACING_* env vars). Self-hosted only: Ory Network does not export OTLP to customer collectors. useCase: >- An Ory customer wants distributed traces in their own observability backend without being locked into a single vendor — using the OTLP standard so they can swap backends or run multiple in parallel. coreFunctionality: |- - - Self-hosted: standard OTel env vars (TRACING_PROVIDER=otel, OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_SERVICE_NAME, OTEL_RESOURCE_ATTRIBUTES) on each Ory product. + - Self-hosted: TRACING_PROVIDER=otel, TRACING_PROVIDERS_OTLP_SERVER_URL (host:port, no scheme, OTLP/HTTP port 4318), TRACING_PROVIDERS_OTLP_INSECURE for plain-HTTP collectors, TRACING_SERVICE_NAME. TRACING_DEPLOYMENT_ENVIRONMENT sets deployment.environment.name. OTEL_RESOURCE_ATTRIBUTES merges in other resource attributes; OTEL_EXPORTER_OTLP_HEADERS applies unless the OTLP authorization_header is set; OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_SERVICE_NAME are ignored. - TRACING_PROVIDER accepts otel, jaeger, or zipkin; use otel for OTLP. - OTLP carries traces only; metrics come from the Prometheus endpoint and logs from stdout. - Ory Network: not available — managed project config excludes operational settings (tracing, logging, port), so there is no customer-facing OTLP export. diff --git a/registry.yaml b/registry.yaml index e95f115..009718c 100644 --- a/registry.yaml +++ b/registry.yaml @@ -8,7 +8,7 @@ apiVersion: ory.com/integrations-v1 kind: Registry metadata: name: ory-integrates - generated: 2026-08-31T14:26:35Z + generated: 2026-10-08T18:28:36Z integrations: - name: skyfire displayName: Skyfire @@ -87,8 +87,8 @@ integrations: An enterprise API team running Apigee wants OAuth2 access tokens issued by Ory (Hydra) validated at the gateway layer so backend services don't need to re-validate every request. coreFunctionality: |- - - SharedFlow with VerifyJWT policy: PublicKey JWKS URL points at Ory's https://.projects.oryapis.com/.well-known/jwks.json; Issuer = the Ory project URL; Audience = the API's audience identifier from Ory's OAuth2 client. - - Algorithm RS256; Apigee caches the JWKS by URL — tune CacheTTLInSeconds for the freshness/KMS-call trade-off. + - SharedFlow with VerifyJWT policy: PublicKey JWKS URL points at Ory's https://.projects.oryapis.com/.well-known/jwks.json; Issuer = https://.projects.oryapis.com (no trailing slash); Audience = the audience set on the Ory OAuth2 client and requested in the token request. Requires JWT access tokens enabled on the project. + - Algorithm RS256; Apigee caches a JWKS fetched from a URL for 300 seconds and selects the key by the token's kid. - Apigee KVM holds per-environment audience values without redeploying the shared flow. - For claim-based authorization (per-scope or per-role checks), add an AssignMessage or JavaScript policy after VerifyJWT to inspect the JWT claims. - SharedFlow attached to the API proxy's PreFlow runs before every request. @@ -115,7 +115,7 @@ integrations: services don't need to re-validate every request and can read the trusted `sub` from request context. coreFunctionality: |- - REST API: Lambda authorizer (Node.js / Python) verifies bearer JWT against https://.projects.oryapis.com/.well-known/jwks.json with kid-based key selection; returns IAM policy + principalId (= JWT sub). - - HTTP API: native JWT authorizer pointing directly at the Ory issuer URL — no Lambda required for standard verification. + - HTTP API: native JWT authorizer with issuer https://.projects.oryapis.com and your API's audience — no Lambda required for standard verification. Requires JWT access tokens and an audience requested in the token request. - Caching: REST uses authorizerResultTtlInSeconds (default 300s); HTTP uses resultsCacheTtlInSeconds. Tune for cold-start vs. revocation latency. - Use Lambda authorizer over native JWT when custom claim-based authorization logic is needed beyond signature/issuer/audience checks. - Cold start matters — keep the authorizer minimal; aws-jwt-verify is a lean JWKS library. @@ -143,8 +143,8 @@ integrations: at the proxy so upstream services receive a trusted identity (or a 401 from the edge) without re-validating every request themselves. coreFunctionality: |- - - Path A (Hydra JWTs): `jwt_authn` HTTP filter with a remote_jwks pointing at https://.projects.oryapis.com/.well-known/jwks.json; set issuer to the Ory project URL and audiences to the API's audience. Envoy caches the JWKS and refreshes on the configured interval. - - Path B (Kratos sessions): `ext_authz` HTTP filter calls Ory Oathkeeper (or a small validator) which hits /sessions/whoami; on success Envoy forwards identity headers (X-User-Id, X-User-Email) upstream, on failure returns 401 at the edge. + - Path A (Hydra JWTs): `jwt_authn` HTTP filter with a remote_jwks pointing at https://.projects.oryapis.com/.well-known/jwks.json; set issuer to https://.projects.oryapis.com (no trailing slash) and audiences to the audience requested in the token request. The remote_jwks cluster must use an UpstreamTlsContext with sni and a validation_context (trusted_ca plus a DNS SAN matcher). Requires JWT access tokens enabled on the Ory project. Envoy caches the JWKS and refreshes on the configured interval. + - Path B (Kratos sessions): `ext_authz` HTTP filter calls Ory Oathkeeper (or a small validator) which hits /sessions/whoami; on success Envoy forwards identity headers (X-User-Id, X-User-Email) read from the whoami response body upstream, on failure returns 401 at the edge. - Verified JWT claims can be forwarded upstream via the filter's payload_in_metadata / forwarding settings for downstream authorization. - Works the same whether Envoy is standalone, an Istio sidecar/gateway, or the data plane under a managed gateway. - Tetrate Agent Router Enterprise (see agent-identity/tetrate) is built on Envoy AI Gateway and applies this pattern to AI-agent traffic. @@ -163,19 +163,19 @@ integrations: maintainedBy: Community contributors status: active description: >- - Validate Ory-issued JWTs or Kratos session cookies at the Kong API Gateway layer. JWTs via Kong's stock `jwt` - plugin (or jwt-signer / jwt-keycloak community plugins for JWKS auto-refresh); session validation via - request-transformer + pre-function calling /sessions/whoami. Documented at + Validate Ory-issued JWTs or Kratos session cookies at the Kong API Gateway layer. JWTs via Kong's open source + `jwt` plugin plus a post-function audience check (or Kong Enterprise's OpenID Connect plugin for JWKS + auto-refresh); session validation via a custom plugin or pre-function calling /sessions/whoami. Documented at ory.com/docs/integrates-with/api-gateways/kong — universal pattern. useCase: >- A team running APIs behind Kong wants Ory-issued OAuth2 access tokens (Hydra) or Kratos session cookies validated at the gateway so backend services don't need to re-validate every request. coreFunctionality: |- - - Path A (Hydra JWTs): Kong jwt plugin with RS256 algorithm, public key from Ory JWKS (prefer jwt-signer or jwt-keycloak community plugins for JWKS auto-fetch over manual key loading). Configure claims_to_verify (typically exp) and key_claim_name. - - Path B (Kratos sessions): request-transformer + pre-function plugins (or a small custom plugin) call https://.projects.oryapis.com/sessions/whoami with the session cookie; on 200 inject X-User-Id / X-User-Email headers downstream. - - Kong Enterprise's OIDC plugin handles Path A automatically with JWKS auto-discovery. - - Caching: cache whoami response per session cookie via Kong's proxy-cache plugin or a Redis-backed custom cache to avoid hammering Ory. - - Rate-limit at the gateway keyed by the validated `sub` claim — Kong's rate-limiting plugin is the cleanest pattern. + - Prerequisite: Ory issues opaque access tokens by default; switch the project to JWT access tokens and request an audience. + - Path A (Hydra JWTs): Kong jwt plugin with an RS256 Consumer credential keyed by the issuer and holding the PEM public key converted from the Ory JWKS; claims_to_verify exp. A post-function requires the API audience and nbf and sets X-User-Id from sub. Reference kong.yaml and docker-compose.yaml for Kong 3.9. + - The open source jwt plugin does not fetch JWKS, so rotate rsa_public_key when Ory's signing key changes. Kong Enterprise's OpenID Connect plugin discovers and refreshes the JWKS automatically (kong-oidc-enterprise.yaml). + - Path B (Kratos sessions): a custom plugin or pre-function calls https://.projects.oryapis.com/sessions/whoami with the session cookie or token; on 200 set X-User-Id / X-User-Email from the response body. + - Caching: cache the whoami result per session inside the plugin; Kong's proxy-cache plugin caches upstream responses, not plugin requests. - Status: community/proposed — documented at ory.com/docs/integrates-with/api-gateways/kong. oryMechanism: - standards @@ -353,15 +353,15 @@ integrations: status: active description: >- Deployment patterns for running with Ory on AWS — both the managed Ory Network case (AWS app reaching out to Ory - over HTTPS) and the self-hosted case (Ory on EKS with RDS Postgres and optional ElastiCache). Not a vendor - integration in the registry sense; documentation of the AWS-side glue around standard Ory deployment. + over HTTPS) and the self-hosted case (Ory on EKS with RDS Postgres). Not a vendor integration in the registry + sense; documentation of the AWS-side glue around standard Ory deployment. useCase: >- An AWS-resident product team needs the AWS-specific configuration to either reach Ory Network from inside a VPC, - or to self-host the open-source Ory stack on EKS with the typical AWS managed services (RDS, ElastiCache, ALB, - ACM, Secrets Manager, Route 53). + or to self-host the open-source Ory stack on EKS with the typical AWS managed services (RDS, ALB, ACM, Secrets + Manager, Route 53). coreFunctionality: |- - - Pattern A (Ory Network from AWS): outbound HTTPS egress to *.oryapis.com; API key in AWS Secrets Manager (IAM-bound, not env vars); Route 53 CNAME to custom.oryapis.com for the custom auth domain. - - Pattern B (self-hosted on EKS): EKS cluster with --with-oidc, RDS PostgreSQL Multi-AZ for Kratos/Hydra/Keto datastores, optional ElastiCache Redis with at-rest + transit encryption. + - Pattern A (Ory Network from AWS): outbound HTTPS egress to *.oryapis.com; API key in AWS Secrets Manager (IAM-bound, not env vars); Route 53 CNAME from the custom auth domain to .projects.oryapis.com, with the domain added to the project in the Ory Console. + - Pattern B (self-hosted on EKS): EKS cluster with --with-oidc, RDS PostgreSQL Multi-AZ for Kratos/Hydra/Keto datastores. - Deploy via the official Helm charts (helm repo add ory https://k8s.ory.sh/helm/charts). - AWS Load Balancer Controller (ALB Ingress) for HTTPS termination with an ACM certificate. - Documented at ory.com/docs/integrates-with/cloud-infrastructure/aws; the Ory self-hosted/operations pages cover the cloud-agnostic concerns. @@ -386,8 +386,8 @@ integrations: VNet, or to self-host the open-source Ory stack on AKS with the typical Azure managed services (PostgreSQL Flexible Server, Key Vault, Workload Identity, Application Gateway Ingress). coreFunctionality: |- - - Pattern A (Ory Network from Azure): outbound HTTPS egress to *.oryapis.com (NSG + optional Azure Firewall app rule); API key in Azure Key Vault; Azure DNS CNAME to custom.oryapis.com. - - Pattern B (self-hosted on AKS): AKS with Workload Identity, Azure Database for PostgreSQL Flexible Server (Zone-Redundant HA in production), optional Azure Cache for Redis. + - Pattern A (Ory Network from Azure): outbound HTTPS egress to *.oryapis.com (NSG + optional Azure Firewall app rule); API key in Azure Key Vault; Azure DNS CNAME from the custom auth domain to .projects.oryapis.com. + - Pattern B (self-hosted on AKS): AKS with Workload Identity, Azure Database for PostgreSQL Flexible Server (Zone-Redundant HA in production). - Deploy via the official Helm charts (helm repo add ory https://k8s.ory.sh/helm/charts). - Application Gateway Ingress Controller (AGIC) for HTTPS termination with a Key Vault-stored certificate. - For Microsoft Entra ID identity provider configuration, see social-sign-in/microsoft and enterprise-sso/microsoft-entra-id (separate concerns from cloud deployment). @@ -412,8 +412,8 @@ integrations: to self-host the open-source Ory stack on GKE with the typical GCP managed services (Cloud SQL, Secret Manager, Workload Identity, GKE Gateway). coreFunctionality: |- - - Pattern A (Ory Network from GCP): outbound HTTPS egress to *.oryapis.com; API key in Secret Manager via Workload Identity; Cloud DNS CNAME to custom.oryapis.com. - - Pattern B (self-hosted on GKE): GKE Autopilot or Standard with Workload Identity, Cloud SQL for PostgreSQL HA, optional Memorystore Redis. Connect to Cloud SQL via the Cloud SQL Auth Proxy sidecar (preferred) or private IP with VPC peering. + - Pattern A (Ory Network from GCP): outbound HTTPS egress to *.oryapis.com; API key in Secret Manager via Workload Identity; Cloud DNS CNAME from the custom auth domain to .projects.oryapis.com. + - Pattern B (self-hosted on GKE): GKE Autopilot or Standard with Workload Identity, Cloud SQL for PostgreSQL HA. Connect to Cloud SQL via the Cloud SQL Auth Proxy sidecar (preferred) or private IP with VPC peering. - Deploy via the official Helm charts (helm repo add ory https://k8s.ory.sh/helm/charts). - GKE Gateway or Ingress with GCLB for HTTPS termination with a Google-managed certificate. - For Google identity provider configuration, see social-sign-in/google and enterprise-sso/google-workspace (separate concerns from cloud deployment). @@ -570,15 +570,15 @@ integrations: status: active description: >- Run the self-hosted Ory stack (Kratos, Hydra, Keto, Oathkeeper) using the official oryd/* Docker images. This - directory holds a reference docker-compose.yml for local development and CI reproduction; production deployments - should use the Helm charts. + entry points to the Docker Compose quickstarts in each product's repository for local development and CI + reproduction; production deployments should use the Helm charts. useCase: >- A developer wants to bring up the open-source Ory stack locally — for development, CI reproduction, or trying upgrades before promoting to a real cluster — without standing up Kubernetes. coreFunctionality: |- - - Reference docker-compose.yml runs Kratos, Hydra, Keto, Oathkeeper against a local Postgres with mailslurper for email testing. + - The Kratos and Hydra quickstarts start each product with Docker Compose from its repository. - Uses the official oryd/kratos, oryd/hydra, oryd/keto, oryd/oathkeeper images from Docker Hub. - - Per-product Docker setup is documented in each product's install guide (Kratos, Hydra, Keto, Oathkeeper) on ory.com/docs. + - Per-product Docker setup is documented in each product's install guide (Kratos, Keto, Oathkeeper) on ory.com/docs. - Hydra ships a Docker-Compose-based quickstart at /docs/hydra/self-hosted/quickstart. - For production, use the Helm charts under iac-devops/helm — docker-compose lacks the probes, rolling updates, and secret handling you'll want. oryMechanism: @@ -601,7 +601,7 @@ integrations: A platform team running GitOps wants per-resource visibility into Ory deployments, or needs to customize resources beyond what the Ory Helm charts expose, and is willing to maintain the manifests instead of using Helm. coreFunctionality: |- - - Reference manifests for the full stack: Deployments, public/admin Services (type ClusterIP), ConfigMaps, Secrets, optional Ingress. + - Generate manifests by rendering the official Helm charts with `helm template`, then manage the output with GitOps tooling. - Pattern: each Ory component (Kratos, Hydra, Keto, Oathkeeper) as a separate Deployment with split public/admin Services. - For most teams the Helm charts (iac-devops/helm) are the recommended path — choose raw manifests only when GitOps visibility or non-default customization justifies the maintenance burden. oryMechanism: @@ -752,17 +752,16 @@ integrations: maintainedBy: Community contributors status: active description: >- - Use CockroachDB as the primary datastore for self-hosted Ory components (Kratos, Hydra, Keto, Oathkeeper) via its - PostgreSQL wire-protocol compatibility. Right choice for deployments needing geographic distribution or HA beyond - a single Postgres node. Documented at ory.com/docs/integrates-with/data-persistence/cockroachdb — same DSN format - Ory uses for Postgres works for Cockroach with Cockroach-aware tuning. + Use CockroachDB as the primary datastore for self-hosted Ory Kratos, Ory Hydra, and Ory Keto with a cockroach:// + DSN. Right choice for deployments needing geographic distribution or HA beyond a single Postgres node. Documented + at ory.com/docs/integrates-with/data-persistence/cockroachdb — cockroach:// DSN plus Cockroach-aware tuning. useCase: >- A self-hosted Ory deployment needs multi-region survivability, horizontal scalability beyond a single Postgres node, or strong consistency under geographic distribution — and is willing to tune for Cockroach's distributed-transaction model. coreFunctionality: |- - - Cockroach speaks Postgres wire protocol — Ory product DSN format unchanged: postgres://user:password@cockroach-host:26257/?sslmode=verify-full&options=--cluster%3D. - - Migrations run with `kratos migrate sql` (etc.) against Cockroach without changes. + - DSN: cockroach://user:password@cockroach-host:26257/?sslmode=verify-full. The cockroach:// scheme selects CockroachDB-specific migrations; a postgres:// DSN selects the PostgreSQL migrations, which can fail partway. + - Migrations run with `kratos migrate sql` (etc.) against the cockroach:// DSN. - FK-heavy schema: expect some 40001 retry errors under high concurrency; clients should retry transactions per Cockroach's standard pattern. - UUID PKs (good — distribute across nodes) vs SERIAL (would hot-shard) — verify no Ory product version uses SERIAL. - Serverless tier has request-per-second limits unsuitable for production identity workloads; use Self-hosted (Dedicated) tier. @@ -1006,7 +1005,7 @@ integrations: - Hydra JWT: verify signature against cached Ory JWKS using EdgeWorkers Crypto API. - Kratos session: call https://.projects.oryapis.com/sessions/whoami via httpRequest (EdgeWorkers doesn't have a fetch global). Cache responses in EdgeKV for 30-60s (fallback: per-isolate Map). - On valid: inject X-User-Id header; on invalid: 401 from the edge. - - CPU budget ~50ms wall-clock — JWKS verification fits comfortably; whoami calls eat into this. + - CPU time per event handler depends on the resource tier (10 ms Basic, 20 ms Dynamic, 70 ms Enterprise Compute), and HTTP sub-requests are capped; each whoami call is a sub-request. - Wired via Akamai Property Manager attaching the EdgeWorker to a behavior on the relevant hostname/path. - Reference implementation: edge-token-validation/cloudflare-workers (same pattern, different runtime). - Status: community/proposed — documented at ory.com/docs/integrates-with/edge-token-validation/akamai-edgeworkers. @@ -1034,10 +1033,11 @@ integrations: and adds identity headers so downstream services can attribute the request without re-validating. coreFunctionality: |- - Cloudflare Worker deployed via wrangler — runs in V8 isolates at the Cloudflare edge. - - Two validation modes: session cookie (forwards to Ory /sessions/whoami with 30 s edge cache) and JWT Bearer token (validates RSASSA-PKCS1-v1_5 signatures against Ory's JWKS, JWKS cached at the edge for 1 h). - - Forwards authenticated requests to origin with X-User-Id, X-Session-Id, X-User-Email, X-User-Name, X-Auth-AAL, X-User-Metadata headers. - - Configurable PUBLIC_PATHS to skip auth on health, public, and static paths. - - Optional LOGIN_REDIRECT_URL turns 401 into a 302 to login with return_to. + - JWT Bearer tokens: RS256 only, key selected by kid with JWKS refresh on unknown kid, and iss, aud (JWT_AUDIENCE), exp, and nbf enforced. Requires JWT access tokens enabled on the Ory project. + - Sessions: ory_session_* or ory_kratos_session cookies and ory_st_ session tokens are checked with Ory /sessions/whoami; active sessions are cached per credential hash for 30 s in bounded isolate memory. + - Removes client-supplied identity headers, then forwards authenticated requests to origin with X-User-Id, X-Session-Id, X-User-Email, X-User-Name, X-Auth-AAL, X-User-Metadata headers. + - Configurable PUBLIC_PATHS (exact path or prefix followed by /) to skip auth on health, public, and static paths. + - Optional LOGIN_REDIRECT_URL redirects browser requests to login with return_to. oryMechanism: - session-validation protocol: @@ -1061,11 +1061,11 @@ integrations: coreFunctionality: |- - WASM module (Rust / JS / AssemblyScript / TinyGo) fires on every request; reads session cookie or Authorization: Bearer. - Hydra JWT: verify signature against cached Ory JWKS using language-native JWT library compiled to WASM. - - Kratos session: call https://.projects.oryapis.com/sessions/whoami via a registered Fastly backend. Compute@Edge cannot make ad-hoc outbound calls — the Ory hostname MUST be registered as a named backend in the service config. + - Kratos session: call https://.projects.oryapis.com/sessions/whoami via a registered Fastly backend. Register the Ory hostname as a named backend in the service config, or have Fastly enable dynamic backends for the service and enable them in the SDK. - Caching: Fastly KV Store keyed by cookie hash for 30-60s; per-execution memory is request-scoped only. - - Edge dictionaries for non-secret config (Ory project URL, audience IDs); edge secrets for JWKS pinning / service tokens. + - Config Store for non-secret config (Ory project URL, audience IDs); Secret Store for service tokens. - On valid: inject X-User-Id header; on invalid: synthesize 401 at the edge. - - Rust is the most performant; JavaScript (via Quick.js) is the easiest path porting from the Cloudflare reference. + - Rust is the most performant; JavaScript (SpiderMonkey compiled to WebAssembly) is the easiest path porting from the Cloudflare reference. - Status: community/proposed — documented at ory.com/docs/integrates-with/edge-token-validation/fastly-compute. oryMechanism: - standards @@ -1694,13 +1694,13 @@ integrations: LaunchDarkly's multi-kind context shape so feature-flag evaluation rules can target on Ory traits, metadata_public, and authenticator_assurance_level without ad-hoc mapping in every call site. useCase: >- - A product wants feature flags that target on identity attributes (subscription plan, role, MFA status) sourced - from Ory. Rather than writing the Ory → LaunchDarkly context mapping in every request handler, the helper + A product wants feature flags that target on identity attributes (subscription tier, groups, organization, AAL) + sourced from Ory. Rather than writing the Ory → LaunchDarkly context mapping in every request handler, the helper centralizes the mapping into one class and exposes typed boolVariation / stringVariation / numberVariation / jsonVariation / allFlagsState methods that accept an Ory session directly. coreFunctionality: |- - OryLaunchDarkly class wraps an LDClient, takes Ory session shape (id, identity, traits, metadata_public, authenticator_assurance_level). - - Builds a LaunchDarkly multi-kind context: { user: {key=identity.id, email, firstName, lastName, plan, ...metadata}, session: {key=session.id, aal} }. + - Builds a LaunchDarkly multi-kind context: { user: {key=identity.id, email, firstName, lastName, tier=metadata_public.subscription_tier, groups, ...metadata}, organization: {key=metadata_public.org_id} when present, session: {key=session.id, aal} }. - boolVariation / stringVariation / numberVariation / jsonVariation pass through to LDClient.variation with the constructed context. - allFlagsState lets you bootstrap client-side flag state for the current Ory session in one call. - Ships as an importable library (peer-depends on @launchdarkly/node-server-sdk ^9) — not a deployed HTTP service. @@ -1728,7 +1728,7 @@ integrations: - sdk-client integration: not a webhook — application code resolves the Ory session, extracts attributes, and passes them to the Split SDK at evaluation time. - Stable key: use identity.id (not email) so bucketing is stable across identity changes. - Common attribute mapping: identity.traits.email → email; metadata_public.org_id → org_id; metadata_public.subscription_tier → tier; metadata_public.groups → groups. - - Server-side: Split server SDK; client-side: Split JavaScript SDK with Track API for attributes. + - Server-side: Split server SDK; client-side: Split JavaScript SDK. Pass attributes to getTreatment (or setAttributes in the JavaScript SDK) after whenReady(); track() records metric events, not attributes. - Impact measurement features need event-stream integration too — separate concern; see cdp-analytics/segment. - Status: community/proposed — convention guide, not turnkey code. oryMechanism: @@ -2103,7 +2103,7 @@ integrations: codebase and deployment pipeline as the rest of their infrastructure. coreFunctionality: |- - NO official Ory Pulumi provider exists today. - - Recommended path: bridge the official ory/ory Terraform provider via pulumi-terraform-bridge to generate a Pulumi-callable SDK. + - Recommended path: generate a local SDK from the official ory/ory Terraform provider with `pulumi package add terraform-provider ory/ory ` (pulumi-terraform-bridge); import it as @pulumi/ory in TypeScript. - Alternative: write a Pulumi `dynamic` provider that calls the Ory Network REST API using @ory/client (TypeScript) or ory-client (Go/Python). State diffing and CRUD logic is the implementer's responsibility. - Escape hatch: keep Ory resources in Terraform via the official provider (iac-devops/terraform) and only use Pulumi for the rest. oryMechanism: @@ -2120,19 +2120,19 @@ integrations: status: active description: >- Officially maintained Terraform provider for managing Ory Network resources as infrastructure-as-code — identity - schemas, OAuth2 clients, project configuration, identities, organizations, permissions, and webhooks. Source at - github.com/ory/terraform-provider-ory; published on the Terraform Registry as `ory/ory` and on the OpenTofu - registry (search.opentofu.org/provider/ory/ory) under the same source — works with `tofu` as well as `terraform`. - Targets Ory Network only — does not manage self-hosted Ory deployments. + schemas, OAuth2 clients, project configuration, identities, organizations, permissions, and actions (webhooks). + Source at github.com/ory/terraform-provider-ory; published on the Terraform Registry as `ory/ory` and on the + OpenTofu registry (search.opentofu.org/provider/ory/ory) under the same source — works with `tofu` as well as + `terraform`. Targets Ory Network only — does not manage self-hosted Ory deployments. useCase: >- A platform team running Ory Network wants their identity and OAuth2 configuration in Git (not the Console) for code review, drift detection, and per-environment (dev/staging/prod) reproducibility. coreFunctionality: |- - - Registry source: ory/ory. Authentication via the `api_key` provider attribute or ORY_API_KEY env var; project via `project` or ORY_PROJECT/ORY_SDK_URL. - - Resources cover identity schemas (ory_identity_schema), OAuth2 clients (ory_oauth2_client), project config (ory_project_config), identities, organizations, permissions, and webhooks (ory_webhook). + - Registry source: ory/ory (version ~> 26.3). Authentication via the workspace_api_key / project_api_key provider arguments or ORY_WORKSPACE_API_KEY / ORY_PROJECT_API_KEY env vars; project via project_id / project_slug or ORY_PROJECT_ID / ORY_PROJECT_SLUG. + - Resources cover identity schemas (ory_identity_schema), OAuth2 clients (ory_oauth2_client), project config (ory_project_config), identities, organizations, permissions, and actions/webhooks (ory_action). - Many ory_project_config attributes were renamed to match OpenAPI spec; old names still work but emit deprecation warnings. The provider repo ships a migrate-deprecated-attrs.sh script for bulk rewrites. - - Two API key types in Ory — Workspace keys (manage projects) vs Project keys (manage in-project resources); pick the right scope. - - Existing resources can be adopted via `terraform import ory_. `. + - Two API key types: Workspace keys (ory_wak_) manage projects, project configuration, identity schemas, actions, social/SAML providers, SCIM clients, and email templates; Project keys (ory_pat_) manage identities, OAuth2 clients, and relationships. + - Existing resources can be adopted with `terraform import` using each resource's import ID format (after-hook ory_action: project_id:flow:after:auth_method:method:url; before-hooks omit auth_method); identity schemas can't be imported. - Provider is for Ory Network only — does NOT manage self-hosted Kratos/Hydra/Keto/Oathkeeper. oryMechanism: - standards @@ -2483,7 +2483,7 @@ integrations: coreFunctionality: |- - There is no `datadog` tracing provider: TRACING_PROVIDER accepts otel, jaeger, or zipkin. Reach Datadog with TRACING_PROVIDER=otel over OTLP. - Ory's span helper sets Datadog-convention error attributes (error.message, error.stack, error.type), so OTLP traces render correctly in Datadog APM. - - Ingest either via the Datadog Agent's OTLP receiver (otlp_config.receiver, ports 4317/4318) or directly to Datadog's OTLP intake (DD-API-KEY header, region-specific endpoints). + - Ory's exporter is OTLP over HTTP only. Ingest either via the Datadog Agent's OTLP HTTP receiver (port 4318) or directly to Datadog's OTLP intake on port 443 (dd-api-key header via OTEL_EXPORTER_OTLP_HEADERS, region-specific hosts). Configure with TRACING_PROVIDERS_OTLP_SERVER_URL (host:port, no scheme). - Prometheus metrics on /metrics/prometheus (each Ory service, admin port); ingest via the Datadog Agent's OpenMetrics check. - Ory Network: not available — managed project config excludes operational settings (tracing, logging, port). oryMechanism: @@ -2509,9 +2509,9 @@ integrations: observability data — to debug latency in login/registration/MFA, alert on auth-error rates, and correlate identity events with downstream service calls. coreFunctionality: |- - - OTLP/HTTP endpoints: otlp.nr-data.net:4318 (US) or otlp.eu01.nr-data.net:4318 (EU). OTLP/gRPC also supported. + - OTLP/HTTP endpoints: otlp.nr-data.net:4318 (US) or otlp.eu01.nr-data.net:4318 (EU). Ory's exporter uses OTLP/HTTP only. - Auth via header `api-key: ` — use Account/Ingest-scope license keys, NEVER full user keys. - - Self-hosted Ory: standard OTel env vars (TRACING_PROVIDER=otel, OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS, OTEL_SERVICE_NAME). + - Self-hosted Ory: TRACING_PROVIDER=otel, TRACING_PROVIDERS_OTLP_SERVER_URL=otlp.nr-data.net:4318 (host:port, no scheme), OTEL_EXPORTER_OTLP_HEADERS=api-key=${NEW_RELIC_LICENSE_KEY}, TRACING_SERVICE_NAME. Ory ignores OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_SERVICE_NAME. - Ory Network: not available — managed project config excludes operational settings (tracing, logging, port). - For production, prefer an OpenTelemetry Collector in front for batching/retries/sampling. - New Relic enforces attribute count/size limits — identity.id fine, identity.traits as flat blob not. Sample at SDK/collector level, not backend side, to preserve head-of-trace context. @@ -2529,13 +2529,13 @@ integrations: status: active description: >- Ory natively supports OpenTelemetry across all self-hosted products — listed as the first/recommended tracing - backend in the Ory observability docs. Self-hosted products ship OTLP via standard OTEL_EXPORTER_OTLP_* env vars. - Self-hosted only: Ory Network does not export OTLP to customer collectors. + backend in the Ory observability docs. Self-hosted products ship OTLP/HTTP configured through the tracing config + (TRACING_* env vars). Self-hosted only: Ory Network does not export OTLP to customer collectors. useCase: >- An Ory customer wants distributed traces in their own observability backend without being locked into a single vendor — using the OTLP standard so they can swap backends or run multiple in parallel. coreFunctionality: |- - - Self-hosted: standard OTel env vars (TRACING_PROVIDER=otel, OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_SERVICE_NAME, OTEL_RESOURCE_ATTRIBUTES) on each Ory product. + - Self-hosted: TRACING_PROVIDER=otel, TRACING_PROVIDERS_OTLP_SERVER_URL (host:port, no scheme, OTLP/HTTP port 4318), TRACING_PROVIDERS_OTLP_INSECURE for plain-HTTP collectors, TRACING_SERVICE_NAME. TRACING_DEPLOYMENT_ENVIRONMENT sets deployment.environment.name. OTEL_RESOURCE_ATTRIBUTES merges in other resource attributes; OTEL_EXPORTER_OTLP_HEADERS applies unless the OTLP authorization_header is set; OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_SERVICE_NAME are ignored. - TRACING_PROVIDER accepts otel, jaeger, or zipkin; use otel for OTLP. - OTLP carries traces only; metrics come from the Prometheus endpoint and logs from stdout. - Ory Network: not available — managed project config excludes operational settings (tracing, logging, port), so there is no customer-facing OTLP export.