JWT Auth Policy - Claim & Scope Matching Refactor #2745
renuka-fernando
started this conversation in
Ideas
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
1. Background
Two problems in the current
jwt-authpolicy:Array claims silently fail.
requiredClaimsonly handles string-valued claims. A token with an array claim such as"group": ["foo","bar"]never matches — even when the expected value is present in the array — so the request is wrongly rejected. (Confirmed by direct test.)requiredScopesuses the wrong logic.We also want richer matching (any-of, all-of, exclusion) for both claims and scopes, expressed consistently.
2. New field set (overview)
anyOfClaimsrequiredClaimsallOfClaimsrequiredClaimsnoneOfClaimsclaimsExpressionanyOfScopesrequiredScopesallOfScopesrequiredScopesnoneOfScopesscopesExpressionrequiredClaimsrequiredScopesNaming decisions:
noneOfClaims/noneOfScopes;claimsExpression/scopesExpressionfor the advanced form.3. Simple matchers —
anyOfClaimsstructureRecommended: Option 2 (array of objects). Both forms are shown below; they are functionally equivalent for the simple case, but Option 2 is the recommendation — it is the proven UI-rendering shape and natively supports the "contains all" case (see the pros/cons for each option below).
Option 1 — Map (
claim → values)requiredClaims); duplicate claim keys are impossible by construction (prevents a config mistake).matchType: regex); cannot express the array "contains all" case (see below). UI-rendering risk: every map param in shipped policies resolves toclaim → string— no shipped policy uses a map whose values are lists, so this shape is un-exercised in the workspace UI (a front-end we don't own).Option 2 — Array of objects (Recommended)
claimsExpressionleaves. Allows the same claim more than once, which unlocks the array "contains all" case without the advanced form (see below). Proven UI-rendering shape: repeatable structured rows are the dominant pattern across shipped policies (set-headers,request-rewrite,mcp-authz,token-based-ratelimit, …);mcp-authzis direct precedent — an array of objects where each item has anameplus a nested list, i.e. the same{claim, values}structure.claim:/values:boilerplate on every entry.Array "contains all" — only possible with Option 2. Because entries can repeat a claim,
allOfClaims(AND across entries) can require an array claim to contain multiple values:Option 1 (map) cannot express this — keys are unique, so
groupcan't be listed twice. With Option 1, this case requiresclaimsExpression(§6).allOfClaimsandnoneOfClaimsuse the same structure as whichever option is chosen foranyOfClaims.UI rendering — side-by-side comparison
4. Two-level any/all semantics (documenting for clarity)
Each simple field has two levels of logic:
anyOfClaims→ OR (at least one claim matches)allOfClaims→ AND (every claim matches)noneOfClaims→ NONE (no claim matches)Examples
allOfClaims— every claim must match:Passes when:
(department ∈ {platform, engineering}) AND (team == devops).anyOfClaims— at least one claim must match:Passes when:
(department ∈ {platform, engineering}) OR (team == devops).noneOfClaims— no claim may match (exclusion):Passes when:
(department ∉ {finance}) AND (team ∉ {hr, legal}).Array claims: if the token claim is itself an array (e.g.
"group": ["a","b"]), "matches" means the array contains any of the listed values (membership).Known limitation (Option 1 only): with the map form, the simple fields cannot express "an array claim must contain all of these values" (within-claim is always OR, and keys are unique). That case then requires
claimsExpression(§6). With Option 2 (array form), it is expressible by repeating the claim underallOfClaims— see §3.5. Array-claim handling (bug fix, no config needed)
Value matching auto-detects the claim shape at runtime:
"role": "admin") → exact equality."group": ["foo","bar"]) → membership check.Example — scalar claim
Token:
"role": "admin"Example — array claim (membership)
Token:
"group": ["foo","bar"]The same policy works whether the identity provider sends
groupas a single string or as an array — no config change needed.This fixes the array bug from §1 transparently for all fields. No
type: string|array|autoparam is needed — auto-detection is always correct, and its cost is negligible next to signature verification / JWKS fetch.6. Advanced conditions — CEL expression
For arbitrary boolean logic for example:
A user must belong to both the
adminandsecurity-auditorgroups, wheregroupsis a JSON array claim:The simple form can't express "contains all of [admin, security-auditor]" — this needs the advanced form.
Option A — Recursive expression tree
and/ortake a list of sub-expressions;nottakes one; leaves are{claim, anyOf|allOf}. Sibling entries underand/orcombine per the parent operator.Rule: siblings under
andare AND-ed; siblings underorare OR-ed.and/or/notare the only operators.Array "contains all" via
allOfat the leaf:anyOf/allOfcovers the array cases; claim names are statically inspectable.and/or/notrecursively, and it renders poorly as a form (deeply nested group widgets).Option B — CEL expression (Recommended)
A single CEL string:
Array "contains all":
cel-gois a direct dependency,advanced-ratelimitships a reusable CEL evaluator, and itspolicy-definition.yamlalready surfaces a CELexpressionfield in the workspace UI. Nothing new to build or render.advanced-ratelimit).Comparison
cel-go, reused fromadvanced-ratelimit)expressionfield already ships)allOfleafinoperator7. Field interaction
When multiple fields are set, all present fields are AND-ed together. The request passes only if every specified block passes.
Example — a policy combining four fields:
The request passes only when all four hold:
If any one block fails, the request is rejected.
8. Deprecations
requiredClaims→ useallOfClaims. Kept for backward compatibility; still string-only + AND.requiredScopes→ useanyOfScopes(OR) orallOfScopes(AND) depending on intent. Note the AND→OR shift in default expectation — needs a migration note.Precedence rule (recommended): new fields override the deprecated one — they do not stack.
anyOfClaims/allOfClaims/noneOfClaims/claimsExpression) is set,requiredClaimsis ignored entirely. Same for scopes: any new scope field supersedesrequiredScopes.requiredClaims→allOfClaimsjust works. Stacking would silently make a policy stricter if a user forgot to delete the old field — a subtle over-restriction that's hard to debug.Examples
Only the deprecated field set → it still applies (backward compatible):
Both set → new field wins, deprecated one is ignored + warning logged:
Effective check:
role ∈ {admin, superuser}. TherequiredClaims: {role: admin}block has no effect.Precedence is per-concept — claims and scopes are independent:
Here the new claim field supersedes
requiredClaims, butrequiredScopesis untouched because no new scope field was provided.9. Open questions for the team
Map vs array for the simple claim fields (§3)? — Recommendation: Option 2 (array of objects). Ratify?
Advanced form (§6) — Recommendation: adopt CEL (Option B) now, reusing the
advanced-ratelimitevaluator; not deferred. Ratify?Deprecated + new field both set (§8) — Recommendation: precedence (new overrides old, per-concept); do not stack. Ratify?
Deprecation warnings — log at startup, per-request, or silent (§8)?
Namespacing — worth grouping under
claims:/scopes:objects instead of the...OfClaims/...OfScopessuffixes? Cleaner, but a bigger structural change and must not collide with existing param names.Flat (current proposal):
Namespaced (alternative):
Beta Was this translation helpful? Give feedback.
All reactions