
# Require User Claims Policy

Authorize requests by validating claims on the authenticated user against a
configurable rule — allowlists (`in`), exact values (`eq`), and prefixes
(`startsWith`), combined with `and`/`or`.

Run this policy after any authentication policy (for example Open ID JWT Auth
or one of its provider-specific variants): the authentication policy proves
_who_ the caller is, and this policy decides _what_ they may call — for
example, pinning a route to specific service accounts, OAuth clients, tenants,
or groups without writing custom code.

## Configuration

The configuration shows how to configure the policy in the 'policies.json' document.

```json title="config/policies.json"
{
  "name": "my-require-user-claims-inbound-policy",
  "policyType": "require-user-claims-inbound",
  "handler": {
    "export": "RequireUserClaimsInboundPolicy",
    "module": "$import(@zuplo/runtime)",
    "options": {
      "rule": {
        "or": [
          {
            "claim": "sub",
            "in": [
              "ci-deployer@my-project.iam.gserviceaccount.com",
              "112233445566778899000"
            ]
          },
          {
            "claim": "email",
            "eq": "ci-deployer@my-project.iam.gserviceaccount.com"
          }
        ]
      }
    }
  }
}
```

### Policy Configuration

- `name` <code className="text-green-600">&lt;string&gt;</code> - The name of your policy instance. This is used as a reference in your routes.
- `policyType` <code className="text-green-600">&lt;string&gt;</code> - The identifier of the policy. This is used by the Zuplo UI. Value should be `require-user-claims-inbound`.
- `handler.export` <code className="text-green-600">&lt;string&gt;</code> - The name of the exported type. Value should be `RequireUserClaimsInboundPolicy`.
- `handler.module` <code className="text-green-600">&lt;string&gt;</code> - The module containing the policy. Value should be `$import(@zuplo/runtime)`.
- `handler.options` <code className="text-green-600">&lt;object&gt;</code> - The options for this policy. [See Policy Options](#policy-options) below.

### Policy Options

The options for this policy are specified below. All properties are optional unless specifically marked as required.

- `rule` **(required)** <code className="text-green-600">&lt;undefined&gt;</code> - The authorization rule evaluated against the authenticated user's claims (request.user.data, populated by the authentication policy that runs before this one — a JWT policy, API key auth, mTLS, etc.). A rule is either a single claim check (eq, in, or startsWith) or an and/or combination of nested rules. Combinators may nest up to three levels deep. Requests that fail the rule receive a 403 Forbidden response.

## Using the Policy

## Overview

The `require-user-claims-inbound` policy authorizes requests by evaluating a
rule against the claims of the authenticated user (`request.user`). It
separates authentication from authorization: an authentication policy — a JWT
policy such as `open-id-jwt-auth-inbound`, API key auth, mTLS, or any other —
proves _who_ the caller is; this policy then decides _what_ they may call.

Typical uses:

- Pin a route to specific caller identities — service accounts, OAuth clients,
  or workload identities — by allowlisting the `sub` and/or `email` claims.
- Require an exact value on any claim, including nested claims (for example an
  AWS STS token's `lambda_source_function_arn` under the
  `https://sts.amazonaws.com/` namespace).
- Match prefixes with `startsWith`, for example AWS assumed-role subjects like
  `arn:aws:sts::123456789012:assumed-role/my-role/<session>` where the session
  name varies.
- Require membership in a group or role carried in an array-valued claim.

## Configuration

```json
{
  "name": "require-known-agents",
  "policyType": "require-user-claims-inbound",
  "handler": {
    "module": "$import(@zuplo/runtime)",
    "export": "RequireUserClaimsInboundPolicy",
    "options": {
      "rule": {
        "or": [
          {
            "claim": "sub",
            "in": [
              "ci-deployer@my-project.iam.gserviceaccount.com",
              "112233445566778899000"
            ]
          },
          {
            "claim": "email",
            "eq": "ci-deployer@my-project.iam.gserviceaccount.com"
          }
        ]
      }
    }
  }
}
```

This policy must run **after** an authentication policy that populates
`request.user`. If no authenticated user is present on the request, the policy
returns `401 Unauthorized`. If the rule evaluates to false, it returns
`403 Forbidden`.

## Rules

A rule is either a single claim check or an `and`/`or` combination of nested
rules (`and`/`or` may nest up to three levels deep; a claim check may appear at
any level):

- `{ "and": [rule, ...] }` — every nested rule must pass.
- `{ "or": [rule, ...] }` — at least one nested rule must pass.
- `{ "claim": <claim>, "eq": value }` — the claim equals the value.
- `{ "claim": <claim>, "in": [value, ...] }` — the claim equals any listed
  value.
- `{ "claim": <claim>, "startsWith": "prefix" }` — the claim is a string
  starting with the prefix.

Each claim check uses exactly one operator. To combine operators on the same
claim, use `and`/`or` with one check per operator.

### Selecting claims

Claims are read from the authenticated user's data (`request.user.data`), which
is set by whichever authentication policy ran before this one. For a JWT auth
policy this is the full decoded token payload; for API key auth it is the key's
metadata.

`claim` is a light JSON-path selector:

- **Dot notation** addresses top-level and nested claims — `email`,
  `address.country`.
- **Bracket notation** with quotes addresses claim names that contain dots or
  slashes (namespaced claims), which dot notation cannot express —
  `['https://example.com/roles']`, or nested:
  `['https://sts.amazonaws.com/']['lambda_source_function_arn']`. Single or
  double quotes both work; single quotes avoid backslash-escaping when the
  selector is written in a JSON `policies.json`. The two notations may be
  mixed, e.g. `realm_access['some.key']`.
- `sub` falls back to `request.user.sub` when the user data has no `sub`
  property, so subject checks also work with authentication policies that only
  set the subject (for example API key authentication).

### Evaluation semantics

Every check fails closed:

- A missing claim never matches.
- Object-valued claims never match.
- Comparisons are strict and type-sensitive: the string `"42"` does not equal
  the number `42`. Note that Google-issued tokens carry `sub` as a string of
  digits, and `$env(...)` substitution always produces strings — quote numeric
  values accordingly.
- `startsWith` only matches string claim values, and an empty prefix is
  rejected as a configuration error.
- When a claim value is an **array** (for example `groups` or `roles`), a check
  passes if **any element** satisfies the operator.

Denied responses never echo claim values or expected values. The failing claim
checks and the caller's `sub` are written to the request log instead.

## Related policies

- [Open ID JWT Auth](https://zuplo.com/docs/policies/open-id-jwt-auth-inbound) —
  verifies token signature, issuer, and audience, and populates
  `request.user`.
- [JWT Scope Validation](https://zuplo.com/docs/policies/jwt-scopes-inbound) —
  requires that the space-delimited `scope` claim contains **all** configured
  scopes; use it for OAuth scope enforcement.

Read more about [how policies work](/articles/policies)
