Skip to content

feat: make id_token mutator cache configurable#1177

Open
David-Wobrock wants to merge 1 commit into
ory:masterfrom
David-Wobrock:feat/id-token-mutator-cache-configurable
Open

feat: make id_token mutator cache configurable#1177
David-Wobrock wants to merge 1 commit into
ory:masterfrom
David-Wobrock:feat/id-token-mutator-cache-configurable

Conversation

@David-Wobrock

@David-Wobrock David-Wobrock commented Aug 9, 2024

Copy link
Copy Markdown
Contributor

Make the id_token mutator cache configurable:

  • can be enabled/disabled
  • can set the max_cost

Changes to default configuration:
Previous:

  • NumCounters: 10000
  • MaxCost: 1 << 25

New:

  • NumCounters: maxCost * 10
  • MaxCost: 1 << 25
  • Cost function: JWT length

Related issue(s)

Follow up of #1171 and #1209 (and #1210 too a bit).

Related docs PR: ory/docs#1820

Checklist

  • I have read the contributing guidelines.
  • I have referenced an issue containing the design document if my change
    introduces a new feature.
  • I am following the
    contributing code guidelines.
  • I have read the security policy.
  • I confirm that this pull request does not address a security
    vulnerability. If this pull request addresses a security vulnerability, I
    confirm that I got the approval (please contact
    security@ory.sh) from the maintainers to push
    the changes.
  • I have added tests that prove my fix is effective or that my feature
    works.
  • I have added or changed the documentation.

Further Comments

Could probably be subject to a minor version bump, since there's a behaviour change.

Summary by CodeRabbit

  • New Features
    • Per-rule configurable caching for ID token generation.
    • Cache options include an enable/disable toggle (default: enabled) and a max_cost parameter (default: 33554432) to control memory usage.
  • Tests
    • Tests and benchmarks updated to cover cache enabled/disabled behavior and max_cost eviction scenarios.

Review Change Stack

@David-Wobrock
David-Wobrock force-pushed the feat/id-token-mutator-cache-configurable branch from 6146085 to 6ad3106 Compare August 9, 2024 15:01
Comment thread pipeline/mutate/mutator_id_token.go Outdated

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

To be discussed if this is really the best strategy for id_token.

I'm less familiar with other use cases of generated id_token and also how ristretto computes the "cost".

For now, this is copied from the AuthN OAuth2 introspection handler.
But perhaps saying that each id_token = 1 cost would cache too many id_tokens.

I'd like @aeneasr's opinion here :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

In the current state it does not work, but we could at least do

                        NumCounters: 10000,
			BufferItems: 64,
                        MaxCost: cost,

to keep the same behaviour as previously.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

cost = 1 << 25

as the default is definitely too many now that the cost function is 1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Since JWTs can be quite long it may make more sense to calculate the cost based on the string length to have an approximation of storage use

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That could make sense indeed 👍
Then the default limit of 1 << 25 (~33.5 million), if we estimate an average length of JWT of a few thousands char, we should be able to store some tens of thousands of keys - which sounds reasonable.

@David-Wobrock
David-Wobrock marked this pull request as ready for review August 9, 2024 15:11
@David-Wobrock
David-Wobrock requested a review from aeneasr as a code owner August 9, 2024 15:11
@David-Wobrock
David-Wobrock force-pushed the feat/id-token-mutator-cache-configurable branch from 6ad3106 to 7a46fd0 Compare August 22, 2024 11:57
@David-Wobrock
David-Wobrock force-pushed the feat/id-token-mutator-cache-configurable branch from 7a46fd0 to 2b89d1e Compare August 29, 2024 12:56
Comment thread pipeline/mutate/mutator_id_token.go Outdated
@David-Wobrock
David-Wobrock force-pushed the feat/id-token-mutator-cache-configurable branch from 2f91f5e to e78b048 Compare September 13, 2024 07:44
Comment thread spec/config.schema.json Outdated

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.

We have a bunch of other caches in the config schema already. Can you make your change so that it is more similar (identical) to those other cache configurations? The max_cost parameter in particular is really opaque and its impossible to come up with a reasonable value without knowing the implementation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I agree with the remark on max_cost 👍
A value with similar semantics is used today for the OAuth2 introspection authenticator, so I mainly mimicked this one.
Else, there is also max_tokens, used by the OAuth2 client credentials authenticator.

I think the reason for the cost instead comes from the fact that the cached objects have variable lengths, so storing a certain number of objects will result in a different cache memory usage depending on your config.

However, one can also make the decision to let the user make this decision anyway :)
Let me know what makes most sense to you, and Ory's strategy around these questions (should the product have the same defaults for everyone, or is the user trusted to configure this accordingly).


For the enabled/disabled value, I didn't re-use the existing

    "handlerSwitch": {
      "title": "Enabled",
      "type": "boolean",
      "default": false,
      "examples": [true],
      "description": "En-/disables this component."
    },

to avoid introducing a breaking change.

Currently, the id_token cache is enabled by default, and didn't wanna change the default value to false - so I couldn't re-use this configuration.


And finally, this cache config has no TTL, because the id_token mutator already has a TTL config value for the JWT expiration date.
It make sense to me to re-use the same value => cache for 15 min if the JWT is valid 15min.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ideally, we would probably have one namespaced cache for all of oathkeeper and then use that everywhere, but this is good for now!

Comment thread pipeline/mutate/mutator_id_token.go Outdated

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.

In other caches, I believe we set the TTL to min(TTL, time.Until(expiresAt). That would make sense here too IMO.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

They should be same normally. Since calling this method does:

	now := time.Now().UTC()
	exp := now.Add(ttl)
	[...]
	a.tokenToCache(c, session, templateClaims, ttl, exp, signed)

Or what am I missing? :)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

Comment thread pipeline/mutate/mutator_id_token.go Outdated

@aeneasr aeneasr left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This only changes cache sizes and not the actual caching function itself, right? If so I think we’re very close!

@David-Wobrock

Copy link
Copy Markdown
Contributor Author

This only changes cache sizes and not the actual caching function itself, right? If so I think we’re very close!

I'm glad to read this 😁

At the time of writing this patch does:

  • behaviour changes:
    • set a TTL on keys in id_token mutator cache
    • set a cost function in the id_token mutator cache
  • new configuration:
    • allow disabling id_token mutator cache
    • allow changing the cost of id_okten mutator cache

@David-Wobrock
David-Wobrock force-pushed the feat/id-token-mutator-cache-configurable branch from 8c5806e to d48aa10 Compare October 8, 2024 07:44
@David-Wobrock
David-Wobrock force-pushed the feat/id-token-mutator-cache-configurable branch from d48aa10 to 4229ee6 Compare October 28, 2024 09:21
@David-Wobrock

Copy link
Copy Markdown
Contributor Author

Hey @aeneasr I hope you're well :)

Did you get a chance to have a look again? 😇
We are still running our forked and patched Oathkeeper, but we would obviously prefer running the Ory upstream version.

Perhaps this can make it into the next version?

@David-Wobrock
David-Wobrock force-pushed the feat/id-token-mutator-cache-configurable branch 2 times, most recently from 9c749a7 to 5ffc3e7 Compare November 11, 2024 12:30
@David-Wobrock
David-Wobrock force-pushed the feat/id-token-mutator-cache-configurable branch from 5ffc3e7 to a24b0a2 Compare December 13, 2024 15:02
@David-Wobrock

Copy link
Copy Markdown
Contributor Author

Hello @aeneasr @alnr,

A pity to see that we didn't make it into the latest Oathkeeper release 😞

Feel free to leave additional review to push this over the finish line 💪 I think we are not far 😄

@David-Wobrock
David-Wobrock force-pushed the feat/id-token-mutator-cache-configurable branch 3 times, most recently from 95d5a75 to d2b9074 Compare December 28, 2024 11:43
Comment thread pipeline/mutate/mutator_id_token.go Outdated

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The a.tokenCache.MaxCost() != cost condition is mainly for unit tests, in order to be able to test other max_cost values on the cache config.

On prod envs, this should not happen, and we'll keep skipping this branch.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So basically this does hot reloading if the cost changes?

Comment thread pipeline/mutate/mutator_id_token.go Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So counters should actually be 10 * number of items. Since the cost no longer == number of items, it's not trivial to set this. But this is probably too high?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah, being able to estimate the number of JWTs that we can fit in the given max size relates to the average size of JWTs. But it's gonna be tough to get an "average JWT size" when the payload is configurable 😅

What would be a better guess in your opinion?
I pushed this for now, which would leave a bit less extra space through the number of counters.

Suggested change
NumCounters: cost * 10,
NumCounters: cost * 4,

Comment thread .schema/config.schema.json Outdated

@aeneasr aeneasr left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Generally LGTM, a few comments

@David-Wobrock
David-Wobrock force-pushed the feat/id-token-mutator-cache-configurable branch from d2b9074 to e390ea5 Compare January 2, 2025 11:04
@David-Wobrock
David-Wobrock requested a review from a team as a code owner January 2, 2025 11:04
@David-Wobrock
David-Wobrock force-pushed the feat/id-token-mutator-cache-configurable branch 2 times, most recently from 124120c to 30c02fa Compare January 2, 2025 21:01
@aeneasr

aeneasr commented Jan 3, 2025

Copy link
Copy Markdown
Member

Looks like we're now failing some cache tests: https://github.com/ory/oathkeeper/actions/runs/12588707987/job/35087289005?pr=1177

@David-Wobrock
David-Wobrock force-pushed the feat/id-token-mutator-cache-configurable branch from 30c02fa to d1a6359 Compare January 3, 2025 10:30
@David-Wobrock

Copy link
Copy Markdown
Contributor Author

Looks like we're now failing some cache tests: https://github.com/ory/oathkeeper/actions/runs/12588707987/job/35087289005?pr=1177

I think we're good again 🙂

@David-Wobrock
David-Wobrock force-pushed the feat/id-token-mutator-cache-configurable branch from d1a6359 to 9075a49 Compare January 22, 2025 10:24
@David-Wobrock
David-Wobrock force-pushed the feat/id-token-mutator-cache-configurable branch from 9075a49 to 461e6a0 Compare February 7, 2025 09:46
@David-Wobrock
David-Wobrock force-pushed the feat/id-token-mutator-cache-configurable branch from 461e6a0 to 786fd8e Compare May 8, 2026 09:02
@coderabbitai

coderabbitai Bot commented May 8, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ebfc3c14-e290-4b76-a33d-d60ec3f03e28

📥 Commits

Reviewing files that changed from the base of the PR and between 786fd8e and 5327566.

📒 Files selected for processing (5)
  • .schema/config.schema.json
  • .schemas/mutators.id_token.schema.json
  • pipeline/mutate/mutator_id_token.go
  • pipeline/mutate/mutator_id_token_test.go
  • spec/config.schema.json
🚧 Files skipped from review as they are similar to previous changes (5)
  • .schemas/mutators.id_token.schema.json
  • spec/config.schema.json
  • pipeline/mutate/mutator_id_token_test.go
  • .schema/config.schema.json
  • pipeline/mutate/mutator_id_token.go

📝 Walkthrough

Walkthrough

ID token mutator caching transitions from a global SetCaching toggle to per-rule configuration. JSON schemas define the new cache object with enabled and max_cost. Go types introduce IdTokenCacheConfig, cache reads/writes check config.Cache.Enabled, and tokenCache is lazily initialized with configurable cost limits. Tests and benchmarks reflect the configuration-driven approach.

Changes

Per-Rule ID Token Caching Configuration

Layer / File(s) Summary
Schema Definitions
.schema/config.schema.json, .schemas/mutators.id_token.schema.json, spec/config.schema.json
JSON schemas for ID token mutator extended with cache object containing enabled (default true) and max_cost (default 33554432).
Configuration Types
pipeline/mutate/mutator_id_token.go
New IdTokenCacheConfig struct with Enabled and MaxCost fields added to CredentialsIDTokenConfig; MutatorIDToken.tokenCache field added; constructor no longer pre-creates cache.
Cache Access Logic
pipeline/mutate/mutator_id_token.go
tokenFromCache and tokenToCache now gate cache operations on config.Cache.Enabled; exported SetCaching(token bool) method removed.
Lazy Cache Initialization
pipeline/mutate/mutator_id_token.go
Config() method defaults Cache.Enabled to true, derives cache cost from Cache.MaxCost (with fallback), and lazily creates/updates tokenCache when needed or when max-cost changes.
Tests and Benchmarks
pipeline/mutate/mutator_id_token_test.go
Cache tests extended with subtests for disabled caching and cost limits; benchmark toggles caching via configuration flag instead of calling SetCaching.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 title clearly and concisely describes the main change: making the id_token mutator cache configurable, which aligns with the primary objective across all modified files.
Description check ✅ Passed The description covers the main feature (making cache configurable), explains configuration changes, links related issues, and includes a completed checklist with all required items checked.
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.

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

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

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

♻️ Duplicate comments (3)
.schemas/mutators.id_token.schema.json (1)

47-51: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Spell out what cache.max_cost measures in this schema copy.

The other schema copies now explain that the cost is the JWT string length, but this one still says only Max cost to cache.. If docs or tooling read this file, users still get the opaque version.

Suggested diff
         "max_cost": {
           "type": "integer",
           "default": 33554432,
           "title": "Maximum Cached Cost",
-          "description": "Max cost to cache."
+          "description": "The cost of one cached JSON Web Token is the length of its string form."
         }
🤖 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 @.schemas/mutators.id_token.schema.json around lines 47 - 51, Update the
"max_cost" JSON schema property description so it explicitly states what is
being measured (the JWT string length) and how the value is applied; locate the
"max_cost" property in the schema (the integer property titled "Maximum Cached
Cost") and replace the opaque description "Max cost to cache." with a clear
sentence such as "Maximum cost to cache, measured as the length (in characters)
of the JWT string." to match the other schema copies.
pipeline/mutate/mutator_id_token.go (2)

215-223: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

NumCounters is being sized from bytes instead of expected entry count.

With the default max_cost, this sets NumCounters to 134217728. Since cost is now JWT length, the cache can hold only on the order of thousands-to-tens-of-thousands of tokens, so the frequency-tracking metadata becomes vastly oversized relative to the actual cache contents.

🤖 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 `@pipeline/mutate/mutator_id_token.go` around lines 215 - 223, The NumCounters
is currently computed from byte-based `cost` (MaxCost) which makes it far too
large; change the logic in the ristretto.NewCache config in mutator_id_token.go
so NumCounters is sized from an estimated number of entries rather than bytes:
compute an estimatedEntries value (e.g., estimatedEntries := max(1, cost /
expectedTokenSize) or a sensible default like 1024) and set NumCounters =
estimatedEntries * 4 (or another small multiplier). Keep MaxCost as `cost` and
keep the Cost func on `idTokenCacheContainer` returning token byte length; only
adjust how NumCounters is derived to use estimated entry count instead of raw
bytes.

41-42: ⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

A single shared tokenCache can't honor per-rule cache settings safely.

MutatorIDToken is shared across requests, but Config() mutates one global cache from the effective rule config. That means a rule with cache.enabled=false still pays the cache allocation cost, and two rules with different cache.max_cost values will keep replacing the cache and dropping each other’s entries. Because this swap happens on the request path without synchronization, it also introduces a data race under concurrent traffic.

Also applies to: 195-231

🤖 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 `@pipeline/mutate/mutator_id_token.go` around lines 41 - 42, MutatorIDToken
currently uses a single shared tokenCache field which is mutated in Config(),
causing cross-rule interference and races; change the design so cache state is
per-rule instead of a single global: in Config() allocate a per-rule cache
(e.g., store a *ristretto.Cache in the rule-specific config or in a map keyed by
rule ID) rather than replacing MutatorIDToken.tokenCache, and guard the map with
a sync.RWMutex (or keep cache pointer on the rule config object returned by
Config()); ensure when cache.enabled is false you leave the per-rule entry nil
(no allocation) and when updating the cache you only set the per-rule entry
under a Lock to avoid races, and update all uses to look up the per-rule cache
(or nil) instead of using the single tokenCache field referenced elsewhere
(references: MutatorIDToken, tokenCache, Config(), idTokenCacheContainer).
🤖 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.

Duplicate comments:
In @.schemas/mutators.id_token.schema.json:
- Around line 47-51: Update the "max_cost" JSON schema property description so
it explicitly states what is being measured (the JWT string length) and how the
value is applied; locate the "max_cost" property in the schema (the integer
property titled "Maximum Cached Cost") and replace the opaque description "Max
cost to cache." with a clear sentence such as "Maximum cost to cache, measured
as the length (in characters) of the JWT string." to match the other schema
copies.

In `@pipeline/mutate/mutator_id_token.go`:
- Around line 215-223: The NumCounters is currently computed from byte-based
`cost` (MaxCost) which makes it far too large; change the logic in the
ristretto.NewCache config in mutator_id_token.go so NumCounters is sized from an
estimated number of entries rather than bytes: compute an estimatedEntries value
(e.g., estimatedEntries := max(1, cost / expectedTokenSize) or a sensible
default like 1024) and set NumCounters = estimatedEntries * 4 (or another small
multiplier). Keep MaxCost as `cost` and keep the Cost func on
`idTokenCacheContainer` returning token byte length; only adjust how NumCounters
is derived to use estimated entry count instead of raw bytes.
- Around line 41-42: MutatorIDToken currently uses a single shared tokenCache
field which is mutated in Config(), causing cross-rule interference and races;
change the design so cache state is per-rule instead of a single global: in
Config() allocate a per-rule cache (e.g., store a *ristretto.Cache in the
rule-specific config or in a map keyed by rule ID) rather than replacing
MutatorIDToken.tokenCache, and guard the map with a sync.RWMutex (or keep cache
pointer on the rule config object returned by Config()); ensure when
cache.enabled is false you leave the per-rule entry nil (no allocation) and when
updating the cache you only set the per-rule entry under a Lock to avoid races,
and update all uses to look up the per-rule cache (or nil) instead of using the
single tokenCache field referenced elsewhere (references: MutatorIDToken,
tokenCache, Config(), idTokenCacheContainer).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f9000af2-0fd5-486f-a3bf-7decc90d1c7b

📥 Commits

Reviewing files that changed from the base of the PR and between 00fa2ab and 786fd8e.

📒 Files selected for processing (5)
  • .schema/config.schema.json
  • .schemas/mutators.id_token.schema.json
  • pipeline/mutate/mutator_id_token.go
  • pipeline/mutate/mutator_id_token_test.go
  • spec/config.schema.json

@David-Wobrock
David-Wobrock force-pushed the feat/id-token-mutator-cache-configurable branch from 786fd8e to 5327566 Compare May 12, 2026 07:09
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.

3 participants