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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions api-gateways/apigee/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,40 @@

## Pattern

Apigee intercepts the API request → SharedFlow runs VerifyJWT against `https://<project>.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://<project>.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 <project-id> --replace '/strategies/access_token="jwt"'

# 2. Create a client that may request your API's audience.
ory create oauth2-client --project <project-id> --name "<client-name>" \
--grant-type client_credentials --audience https://api.example.com

# 3. Request the audience when you get a token.
curl https://<project>.projects.oryapis.com/oauth2/token -u "<client-id>:<client-secret>" \
-d grant_type=client_credentials -d audience=https://api.example.com
```

The token's `iss` claim is `https://<project>.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. Create an Apigee SharedFlow with a **VerifyJWT** policy:
- Algorithm: RS256.
- PublicKey JWKS URL: `https://<project>.projects.oryapis.com/.well-known/jwks.json`.
- Issuer: `https://<project>.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.

Expand Down
4 changes: 2 additions & 2 deletions api-gateways/apigee/registry.entry.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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://<project>.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://<project>.projects.oryapis.com/.well-known/jwks.json; Issuer = https://<project>.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.
Expand Down
22 changes: 21 additions & 1 deletion api-gateways/aws-api-gateway/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <project-id> --replace '/strategies/access_token="jwt"'

# 2. Create a client that may request your API's audience.
ory create oauth2-client --project <project-id> --name "<client-name>" \
--grant-type client_credentials --audience https://api.example.com

# 3. Request the audience when you get a token.
curl https://<project>.projects.oryapis.com/oauth2/token -u "<client-id>:<client-secret>" \
-d grant_type=client_credentials -d audience=https://api.example.com
```

The token's `iss` claim is `https://<project>.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 <token>` header.
- Verifies against JWKS at `https://<project>.projects.oryapis.com/.well-known/jwks.json` (cache in module scope, refresh on `kid` miss).
- Checks that `iss` is `https://<project>.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://<project>.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`.

Expand Down
2 changes: 1 addition & 1 deletion api-gateways/aws-api-gateway/registry.entry.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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://<project>.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://<project>.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.
Expand Down
26 changes: 23 additions & 3 deletions api-gateways/envoy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <project-id> --replace '/strategies/access_token="jwt"'

# 2. Create a client that may request your API's audience.
ory create oauth2-client --project <project-id> --name "<client-name>" \
--grant-type client_credentials --audience https://api.example.com

# 3. Request the audience when you get a token.
curl https://<project>.projects.oryapis.com/oauth2/token -u "<client-id>:<client-secret>" \
-d grant_type=client_credentials -d audience=https://api.example.com
```

The token's `iss` claim is `https://<project>.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)
Expand All @@ -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://<project>.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://<project>.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.

Expand All @@ -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://<project>.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

Expand Down
4 changes: 2 additions & 2 deletions api-gateways/envoy/registry.entry.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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://<project>.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://<project>.projects.oryapis.com/.well-known/jwks.json; set issuer to https://<project>.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.
Expand Down
62 changes: 46 additions & 16 deletions api-gateways/kong/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <project-id> --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 <project-id> --name "<client-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://<project>.projects.oryapis.com/oauth2/token -u "<client-id>:<client-secret>" \
-d grant_type=client_credentials -d audience=https://api.example.com
```

The token's `iss` claim is `https://<project>.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=<access-token>
curl -s https://<project>.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://<project>.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://<project>.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://<project>.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

Expand Down
Loading
Loading