Skip to content

[APIP] Add Backend JWT Policy#200

Open
DDH13 wants to merge 22 commits into
wso2:mainfrom
DDH13:main.backend-jwt
Open

[APIP] Add Backend JWT Policy#200
DDH13 wants to merge 22 commits into
wso2:mainfrom
DDH13:main.backend-jwt

Conversation

@DDH13

@DDH13 DDH13 commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Related to wso2/api-platform#2131

This pull request introduces a new "Backend JWT" policy, which enables the gateway to generate and inject a signed JWT containing authenticated user information into upstream request headers. This policy is designed to work after authentication policies, allowing backend services to verify caller identity using the gateway's public key, without needing access to the original client credentials. The PR includes comprehensive documentation, configuration metadata, and policy definition files for seamless integration.

Backend JWT Policy Introduction and Documentation:

  • Added a new policy, "Backend JWT", to the policy catalog in docs/README.md, describing its purpose and usage.
  • Created detailed documentation in docs/backend-jwt/v1.0/docs/backend-jwt.md, covering how the policy works, claim handling, configuration options, caching behavior, dynamic context claims, and usage examples.
  • Added policy metadata in docs/backend-jwt/v1.0/metadata.json with name, version, provider, categories, and a concise description.

Policy Definition and Configuration:

  • Introduced the formal policy definition in policies/backend-jwt/policy-definition.yaml, specifying user and system parameters (e.g., header name, claim mappings, custom claims, signing key, algorithm, issuer, expiry, caching), including validation rules and documentation for each field.
  • Added Go module definition for the policy implementation in policies/backend-jwt/go.mod, listing dependencies and a local SDK replacement for development.

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Implements a new Backend JWT gateway policy: metadata and README entry, full user documentation and policy-definition schema, Go module, policy factory and runtime handler, PEM private-key loading/parsing (RSA/EC/PKCS#8), signing (RSA/EC/NONE), deterministic token cache keys, in-memory token/key caches with eviction, $ctx: runtime claim resolution, requireAuthentication handling, and a comprehensive Go test suite.

Sequence Diagram

sequenceDiagram
  participant Client
  participant GatewayPolicy as Backend JWT Policy
  participant AuthContext
  participant KeyCache
  participant TokenCache
  participant Signer as JWT Signer
  participant Backend
  Client->>GatewayPolicy: incoming HTTP request
  GatewayPolicy->>AuthContext: read authentication context
  alt requireAuthentication=true && no auth
    GatewayPolicy->>Client: 401 Unauthorized
  else
    GatewayPolicy->>TokenCache: lookup token by identity+path+claims hash
    alt cached token exists & valid
      TokenCache->>GatewayPolicy: return cached token
    else
      GatewayPolicy->>KeyCache: load & parse private key
      GatewayPolicy->>GatewayPolicy: resolve $ctx: claims
      GatewayPolicy->>Signer: build and sign JWT
      Signer->>TokenCache: cache token with TTL (half expiry, min 30s)
      TokenCache->>GatewayPolicy: return signed token
    end
    GatewayPolicy->>Backend: forward request with token in configured header
  end
  Backend->>Backend: verify JWT using gateway public key
Loading

Suggested reviewers

  • pubudu538
  • malinthaprasan
  • Krishanx92
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title '[APIP] Add Backend JWT Policy' clearly and concisely summarizes the main change: introduction of a new Backend JWT policy. It is specific, relevant, and appropriately highlights the primary contribution.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description is largely complete and covers the purpose, implementation scope, and main deliverables. However, it omits several required template sections.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@DDH13 DDH13 changed the title [APIP[ Add Backend JWT Policy [APIP] Add Backend JWT Policy Jun 9, 2026
@DDH13 DDH13 force-pushed the main.backend-jwt branch from bcc35ce to 35d8cf1 Compare June 9, 2026 10:15

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
policies/backend-jwt/backendjwt_test.go (1)

232-292: ⚡ Quick win

Consider testing overlapping claim names to document precedence.

The test suite covers customClaims and claimMappings independently. Adding a test where both configurations produce a claim with the same name would explicitly document which takes precedence. Based on the implementation, custom claims are added after mapped claims, so custom claims override in case of collision. A test clarifying this behavior would aid future maintainability and make the design intent explicit.

Example test structure
func TestCustomClaimsOverrideClaimMappings(t *testing.T) {
	rsaKey, keyPEM := generateRSAKey(t)
	p := &BackendJWTPolicy{keyCache: make(map[[32]byte]crypto.PrivateKey), tokenCache: make(map[string]cachedToken)}
	params := baseParams(keyPEM)
	params["claimMappings"] = map[string]interface{}{
		"dept": "department", // Maps auth property "dept" to JWT claim "department"
	}
	params["customClaims"] = map[string]interface{}{
		"department": "override-value", // Custom claim with same name
	}

	reqCtx := newRequestContext(&policy.AuthContext{
		Authenticated: true,
		AuthType:      "jwt",
		Subject:       "test-user",
		Properties:    map[string]string{"dept": "engineering"},
	})

	result := p.OnRequestHeaders(context.Background(), reqCtx, params)
	mods := result.(policy.UpstreamRequestHeaderModifications)
	claims := decodeJWT(t, mods.HeadersToSet[defaultHeader], &rsaKey.PublicKey)

	if claims["department"] != "override-value" {
		t.Errorf("expected customClaims to override claimMappings, got %v", claims["department"])
	}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@policies/backend-jwt/backendjwt_test.go` around lines 232 - 292, Add a new
unit test that verifies customClaims override claimMappings by creating a
BackendJWTPolicy (keyCache/tokenCache), using baseParams with both
"claimMappings" mapping an auth property (e.g., "dept" -> "department") and
"customClaims" containing the same claim name ("department": "override-value"),
then call p.OnRequestHeaders with a request context whose Properties include
dept, decode the JWT (using decodeJWT and rsa key from generateRSAKey) and
assert the "department" claim equals the customClaims value; place this
alongside TestCustomClaims/TestClaimMappings to document precedence.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@policies/backend-jwt/backendjwt.go`:
- Around line 156-165: The cache key built by buildTokenCacheKey omits
authCtx.Audience so different audiences can share one cached token; update
buildTokenCacheKey to accept an audience parameter (e.g., audience string or
[]string normalized to a canonical string) and include it in the key/hash
computation, then update every callsite (including the call in backendjwt.go
that currently passes authCtx.Subject, authCtx.CredentialID, authCtx.AuthType,
authCtx.Issuer, reqCtx.OperationPath, issuer, algorithm, extras) to also pass
authCtx.Audience, and ensure getCachedToken and any cache-set logic uses the new
key so tokens with different aud claims are cached separately.

---

Nitpick comments:
In `@policies/backend-jwt/backendjwt_test.go`:
- Around line 232-292: Add a new unit test that verifies customClaims override
claimMappings by creating a BackendJWTPolicy (keyCache/tokenCache), using
baseParams with both "claimMappings" mapping an auth property (e.g., "dept" ->
"department") and "customClaims" containing the same claim name ("department":
"override-value"), then call p.OnRequestHeaders with a request context whose
Properties include dept, decode the JWT (using decodeJWT and rsa key from
generateRSAKey) and assert the "department" claim equals the customClaims value;
place this alongside TestCustomClaims/TestClaimMappings to document precedence.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2dd323af-3ebb-4dce-9816-68105f578b45

📥 Commits

Reviewing files that changed from the base of the PR and between 352a7a9 and bcc35ce.

📒 Files selected for processing (6)
  • docs/README.md
  • docs/backend-jwt/v1.0/docs/backend-jwt.md
  • docs/backend-jwt/v1.0/metadata.json
  • policies/backend-jwt/backendjwt.go
  • policies/backend-jwt/backendjwt_test.go
  • policies/backend-jwt/policy-definition.yaml

Comment thread policies/backend-jwt/backendjwt.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@policies/backend-jwt/backendjwt.go`:
- Around line 185-188: The cache key currently built by buildTokenCacheKey omits
authCtx.AuthType, authCtx.Issuer and the policy configuration parameters
(issuer, algorithm, tokenExpiry), causing incorrect cache reuse; update
buildTokenCacheKey to accept authCtx.AuthType and authCtx.Issuer plus the policy
config values (issuer, algorithm, tokenExpiry) in its signature and include them
in the hash/input used to build the cache key (ensure the call site where
buildTokenCacheKey is invoked with authCtx.CredentialID, authCtx.Subject,
reqCtx.APIContext, reqCtx.APIVersion, authCtx.Audience, extras is updated to
pass the new fields as well).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 295e666f-558e-43bb-a87b-88511271bbe9

📥 Commits

Reviewing files that changed from the base of the PR and between bcc35ce and bff39dd.

📒 Files selected for processing (6)
  • docs/README.md
  • docs/backend-jwt/v1.0/docs/backend-jwt.md
  • docs/backend-jwt/v1.0/metadata.json
  • policies/backend-jwt/backendjwt.go
  • policies/backend-jwt/backendjwt_test.go
  • policies/backend-jwt/policy-definition.yaml
✅ Files skipped from review due to trivial changes (2)
  • docs/README.md
  • docs/backend-jwt/v1.0/docs/backend-jwt.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • docs/backend-jwt/v1.0/metadata.json
  • policies/backend-jwt/policy-definition.yaml
  • policies/backend-jwt/backendjwt_test.go

Comment thread policies/backend-jwt/backendjwt.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
policies/backend-jwt/backendjwt.go (2)

132-149: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject invalid or non-positive tokenExpiry values during validation.

parseDuration silently falls back to the default on parse errors and also accepts 0 or negative durations. That lets a bad policy config validate successfully but mint tokens with an unintended or already-expired lifetime. Please validate tokenExpiry in Validate() and require a positive duration.

Also applies to: 177-177, 502-510

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@policies/backend-jwt/backendjwt.go` around lines 132 - 149, In
BackendJWTPolicy.Validate, explicitly validate the "tokenExpiry" param: call
parseDuration on getString(params, "tokenExpiry", "") (or the same retrieval
used elsewhere), and if parsing fails or the resulting duration is <= 0 return
an error (e.g., "invalid tokenExpiry: must be a positive duration"); do this
before returning success so malformed, zero or negative expiries are rejected.
Apply the same check where tokenExpiry is validated elsewhere (the other
Validate/creation sites referenced around the other occurrences) to ensure all
policy paths enforce a positive token lifetime.

523-570: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Preserve auth.property key casing during $ctx: resolution.

The code lowercases the full $ctx: reference before lookup, so $ctx:auth.property.ClientName becomes clientname. Mixed-case property keys will be skipped even when present. Lowercase only the fixed prefix and keep the property suffix unchanged. Based on the documented $ctx:auth.property.<key> mapping to AuthContext.Properties.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@policies/backend-jwt/backendjwt.go` around lines 523 - 570, The issue is that
the code lowercases the entire ctx value (variable :=
strings.ToLower(strings.TrimPrefix(value, ctxPrefix))), so keys like
auth.property.ClientName are lowercased and not found; change the logic to only
do case-insensitive matching on the fixed prefix/field names while preserving
the suffix/key casing for auth.property lookups. Concretely: compute variable :=
strings.TrimPrefix(value, ctxPrefix) (preserve original), use a lowercased
helper (e.g., lower := strings.ToLower(variable)) for the switch/HasPrefix
checks (request.path, request.header., auth.property., etc.), and when handling
the "auth.property." case extract the key with strings.TrimPrefix(variable,
"auth.property.") (not from the lowercased string) so the original casing of the
property key is preserved when accessing
reqCtx.SharedContext.AuthContext.Properties.
🧹 Nitpick comments (1)
policies/backend-jwt/backendjwt_test.go (1)

997-1030: ⚡ Quick win

Cover the full reserved-claim set in this regression test.

The implementation also blocks aud, exp, and iat, but this case only asserts iss and sub. Adding the remaining assertions would make the new restricted-claim behavior fully regression-safe.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@policies/backend-jwt/backendjwt_test.go` around lines 997 - 1030,
TestCustomClaims_RestrictedClaimsIgnored only asserts iss and sub but the
implementation also treats aud, exp and iat as restricted; update the test
(TestCustomClaims_RestrictedClaimsIgnored) to assert that claims["aud"] remains
the original audience, and that claims["exp"] and claims["iat"] are not
overridden by the customClaims (validate their values/types from the decoded JWT
via decodeJWT and the token produced by p.OnRequestHeaders / defaultHeader), so
the test covers the full reserved-claim set (iss, sub, aud, exp, iat) and
ensures non-restricted claims like env are still set.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@policies/backend-jwt/backendjwt.go`:
- Around line 300-305: The runtime currently drops keys in restrictedClaims when
merging params["customClaims"], which conflicts with policy-definition.yaml that
states customClaims run last and may override derived claims; update the merge
logic in backendjwt.go (around the params["customClaims"] handling and the
restrictedClaims check) to allow custom claim keys to overwrite existing claims
instead of warning/continuing (i.e., remove or change the block that calls
slog.Warn and continue), ensuring customClaims are applied after derived claims
so they take precedence, or alternatively update the policy-definition.yaml to
document that reserved claims cannot be overridden—choose one approach in this
PR so runtime behavior and schema/docs stay consistent.

---

Outside diff comments:
In `@policies/backend-jwt/backendjwt.go`:
- Around line 132-149: In BackendJWTPolicy.Validate, explicitly validate the
"tokenExpiry" param: call parseDuration on getString(params, "tokenExpiry", "")
(or the same retrieval used elsewhere), and if parsing fails or the resulting
duration is <= 0 return an error (e.g., "invalid tokenExpiry: must be a positive
duration"); do this before returning success so malformed, zero or negative
expiries are rejected. Apply the same check where tokenExpiry is validated
elsewhere (the other Validate/creation sites referenced around the other
occurrences) to ensure all policy paths enforce a positive token lifetime.
- Around line 523-570: The issue is that the code lowercases the entire ctx
value (variable := strings.ToLower(strings.TrimPrefix(value, ctxPrefix))), so
keys like auth.property.ClientName are lowercased and not found; change the
logic to only do case-insensitive matching on the fixed prefix/field names while
preserving the suffix/key casing for auth.property lookups. Concretely: compute
variable := strings.TrimPrefix(value, ctxPrefix) (preserve original), use a
lowercased helper (e.g., lower := strings.ToLower(variable)) for the
switch/HasPrefix checks (request.path, request.header., auth.property., etc.),
and when handling the "auth.property." case extract the key with
strings.TrimPrefix(variable, "auth.property.") (not from the lowercased string)
so the original casing of the property key is preserved when accessing
reqCtx.SharedContext.AuthContext.Properties.

---

Nitpick comments:
In `@policies/backend-jwt/backendjwt_test.go`:
- Around line 997-1030: TestCustomClaims_RestrictedClaimsIgnored only asserts
iss and sub but the implementation also treats aud, exp and iat as restricted;
update the test (TestCustomClaims_RestrictedClaimsIgnored) to assert that
claims["aud"] remains the original audience, and that claims["exp"] and
claims["iat"] are not overridden by the customClaims (validate their
values/types from the decoded JWT via decodeJWT and the token produced by
p.OnRequestHeaders / defaultHeader), so the test covers the full reserved-claim
set (iss, sub, aud, exp, iat) and ensures non-restricted claims like env are
still set.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3fe58fb7-cb37-4450-9f46-10212825dd8c

📥 Commits

Reviewing files that changed from the base of the PR and between bff39dd and d8a4e3d.

⛔ Files ignored due to path filters (1)
  • policies/backend-jwt/go.sum is excluded by !**/*.sum
📒 Files selected for processing (5)
  • docs/backend-jwt/v1.0/docs/backend-jwt.md
  • policies/backend-jwt/backendjwt.go
  • policies/backend-jwt/backendjwt_test.go
  • policies/backend-jwt/go.mod
  • policies/backend-jwt/policy-definition.yaml
💤 Files with no reviewable changes (1)
  • policies/backend-jwt/policy-definition.yaml
✅ Files skipped from review due to trivial changes (2)
  • policies/backend-jwt/go.mod
  • docs/backend-jwt/v1.0/docs/backend-jwt.md

Comment thread policies/backend-jwt/backendjwt.go
@DDH13 DDH13 force-pushed the main.backend-jwt branch from d8a4e3d to 977c7fa Compare June 10, 2026 08:46

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
policies/backend-jwt/backendjwt_test.go (1)

1030-1059: 💤 Low value

Test name suggests warning verification but doesn't verify logs.

The test name TestCustomClaims_RestrictedClaimsOverrideWithWarn includes "WithWarn", suggesting it verifies that a warning is logged when restricted claims are overridden. However, the test only verifies the override behavior itself (that iss and sub can be overridden via customClaims) without checking for any warning logs.

Consider either removing "WithWarn" from the name if warnings aren't logged, or adding log verification if warnings are intended.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@policies/backend-jwt/backendjwt_test.go` around lines 1030 - 1059, The test
TestCustomClaims_RestrictedClaimsOverrideWithWarn either needs to assert the
expected warning is emitted or be renamed to remove "WithWarn"; to fix, decide
which is intended: if a warning should be logged when restricted claims ("iss",
"sub") are overridden, update the test to capture the policy logger (or inject a
test logger) around the call to p.OnRequestHeaders and assert the logger
received a warning message mentioning the restricted claim override after
calling p.OnRequestHeaders (referencing the BackendJWTPolicy instance p and the
OnRequestHeaders call); otherwise rename the test to
TestCustomClaims_RestrictedClaimsOverride (remove "WithWarn") and leave the
existing assertions unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@policies/backend-jwt/backendjwt_test.go`:
- Around line 1030-1059: The test
TestCustomClaims_RestrictedClaimsOverrideWithWarn either needs to assert the
expected warning is emitted or be renamed to remove "WithWarn"; to fix, decide
which is intended: if a warning should be logged when restricted claims ("iss",
"sub") are overridden, update the test to capture the policy logger (or inject a
test logger) around the call to p.OnRequestHeaders and assert the logger
received a warning message mentioning the restricted claim override after
calling p.OnRequestHeaders (referencing the BackendJWTPolicy instance p and the
OnRequestHeaders call); otherwise rename the test to
TestCustomClaims_RestrictedClaimsOverride (remove "WithWarn") and leave the
existing assertions unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6418e044-9af5-41b0-b40a-167a5d53c855

📥 Commits

Reviewing files that changed from the base of the PR and between d8a4e3d and dbc558f.

📒 Files selected for processing (4)
  • docs/backend-jwt/v1.0/docs/backend-jwt.md
  • policies/backend-jwt/backendjwt.go
  • policies/backend-jwt/backendjwt_test.go
  • policies/backend-jwt/policy-definition.yaml
🚧 Files skipped from review as they are similar to previous changes (2)
  • docs/backend-jwt/v1.0/docs/backend-jwt.md
  • policies/backend-jwt/backendjwt.go

@DDH13 DDH13 force-pushed the main.backend-jwt branch from 67f9165 to ecff618 Compare June 16, 2026 06:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant