Skip to content

feat(auth): cache client credentials tokens in POST /token - #570

Merged
mfiedorowicz merged 5 commits into
developfrom
feat/auth-token-cache
Jul 31, 2026
Merged

feat(auth): cache client credentials tokens in POST /token#570
mfiedorowicz merged 5 commits into
developfrom
feat/auth-token-cache

Conversation

@mfiedorowicz

@mfiedorowicz mfiedorowicz commented Jul 30, 2026

Copy link
Copy Markdown
Member

What

POST /token is a 1:1 proxy to the oauth2 server, so caller token rate is upstream token rate. Adds an optional per-credential cache with single-flight. Disabled by default.

Safety

The presented secret is part of every cache key, so a valid client_id with the wrong secret derives a different key and always goes upstream. Only client_credentials and only 200 responses are cached. expires_in is recomputed on every hit. Reuse is bounded by min(OAUTH2_TOKEN_CACHE_MAX_TTL, 80% of token lifetime).

Deployments running extra checks at issuance can install a TokenIssuanceGate, consulted on every hit. A denial or gate error falls through to the upstream rather than serving the entry, so failing closed costs a cache hit and not availability.

Config

var default
OAUTH2_TOKEN_CACHE_ENABLED false
OAUTH2_TOKEN_CACHE_MAX_ENTRIES 4096
OAUTH2_TOKEN_CACHE_MAX_TTL 15m
OAUTH2_TOKEN_CACHE_NEGATIVE_TTL 5s

Memory measured locally at roughly 7 KiB per entry, so the default bound is about 27 MiB when full, and flat past capacity.

Tests

14 new tests. Four are the ones that must never regress: a wrong secret is never served from cache, a narrower scope is never served from a broader entry, non-client_credentials grants are never cached, and a gate denial never serves the entry.

The token endpoint proxies every request upstream, so caller token rate is
upstream token rate. Cache successful client_credentials responses and
collapse concurrent misses per key into a single upstream call. Off by
default.

The cache key is an HMAC over grant_type, client_id, the presented secret
and the sorted scope and audience sets, so a caller with a wrong secret can
never be served another caller's token. expires_in is recomputed from
remaining lifetime on every hit, and reuse is capped at the shorter of a
configured maximum and 80% of the token's own lifetime.

Deployments that apply additional checks during issuance can install a
TokenIssuanceGate, consulted on every hit so a cached response cannot
bypass them.

The upstream call is also detached from the caller's context: a caller that
gives up must not abort an in-flight upstream transaction, and under
single-flight must not abort the request other waiters depend on.
@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown

Vulnerability Scan: Passed — diode-auth

Image: diode-auth:scan

Source Library CVE Severity Installed Fixed Title
usr/bin/hydra github.com/docker/docker CVE-2026-34040 🟠 HIGH v28.3.3+incompatible 29.3.1 Moby: Moby: Authorization bypass vulnerability
usr/bin/hydra github.com/docker/docker CVE-2026-33997 🟡 MEDIUM v28.3.3+incompatible 29.3.1 moby: docker: github.com/moby/moby: Moby: Privilege validation bypass during plu
usr/bin/hydra github.com/go-jose/go-jose/v3 CVE-2026-34986 🟠 HIGH v3.0.4 3.0.5 github.com/go-jose/go-jose/v3: github.com/go-jose/go-jose/v4: Go JOSE: Denial of
usr/bin/hydra github.com/jackc/pgx/v5 CVE-2026-33815 🔴 CRITICAL v5.7.5 5.9.0 github.com/jackc/pgx/v5: github.com/jackc/pgx: Memory-safety vulnerability
usr/bin/hydra github.com/jackc/pgx/v5 CVE-2026-33816 🔴 CRITICAL v5.7.5 5.9.0 github.com/jackc/pgx/v5: github.com/jackc/pgx: Memory-safety vulnerability
usr/bin/hydra github.com/jackc/pgx/v5 CVE-2026-41889 ⚪ LOW v5.7.5 5.9.2 github.com/jackc/pgx: golang: pgx: SQL injection via specific SQL query conditio
usr/bin/hydra go.opentelemetry.io/otel CVE-2026-29181 🟠 HIGH v1.40.0 1.41.0 github.com/open-telemetry/opentelemetry-go: OpenTelemetry-Go: Denial of Service
usr/bin/hydra go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp CVE-2026-39882 🟡 MEDIUM v1.37.0 1.43.0 github.com/open-telemetry/opentelemetry-go: golang: OpenTelemetry-Go: Memory exh
usr/bin/hydra go.opentelemetry.io/otel/sdk CVE-2026-39883 🟠 HIGH v1.40.0 1.43.0 github.com/open-telemetry/opentelemetry-go: OpenTelemetry-Go: Arbitrary code exe
usr/bin/hydra golang.org/x/crypto CVE-2026-39828 🟠 HIGH v0.46.0 0.52.0 golang.org/x/crypto/ssh: golang.org/x/crypto/ssh: Unauthorized command execution
usr/bin/hydra golang.org/x/crypto CVE-2026-39829 🟠 HIGH v0.46.0 0.52.0 golang.org/x/crypto/ssh: golang.org/x/crypto/ssh: Denial of Service via crafted
usr/bin/hydra golang.org/x/crypto CVE-2026-39830 🟠 HIGH v0.46.0 0.52.0 golang.org/x/crypto/ssh: golang.org/x/crypto/ssh: Denial of Service via resource
usr/bin/hydra golang.org/x/crypto CVE-2026-39831 🟠 HIGH v0.46.0 0.52.0 golang.org/x/crypto/ssh: golang.org/x/crypto/ssh: Security key bypass due to mis
usr/bin/hydra golang.org/x/crypto CVE-2026-39832 🟠 HIGH v0.46.0 0.52.0 golang.org/x/crypto/ssh/agent: golang.org/x/crypto/ssh/agent: Security bypass du
usr/bin/hydra golang.org/x/crypto CVE-2026-39835 🟠 HIGH v0.46.0 0.52.0 golang.org/x/crypto/ssh: golang: golang.org/x/crypto/ssh: Denial of Service via
usr/bin/hydra golang.org/x/crypto CVE-2026-42508 🟠 HIGH v0.46.0 0.52.0 golang.org/x/crypto/ssh/knownhosts: golang: golang.org/x/crypto/ssh/knownhosts:
usr/bin/hydra golang.org/x/crypto CVE-2026-46595 🟠 HIGH v0.46.0 0.52.0 golang.org/x/crypto/ssh: golang.org/x/crypto/ssh: Authorization bypass due to sk
usr/bin/hydra golang.org/x/crypto CVE-2026-46597 🟠 HIGH v0.46.0 0.52.0 golang.org/x/crypto/ssh: golang.org/x/crypto/ssh: Denial of Service via crafted
usr/bin/hydra golang.org/x/crypto CVE-2026-39827 🟡 MEDIUM v0.46.0 0.52.0 golang.org/x/crypto/ssh: golang: golang.org/x/crypto/ssh: Denial of Service via
usr/bin/hydra golang.org/x/crypto CVE-2026-39833 🟡 MEDIUM v0.46.0 0.52.0 golang.org/x/crypto/ssh/agent: golang.org/x/crypto/ssh/agent: Security bypass du
usr/bin/hydra golang.org/x/crypto CVE-2026-39834 🟡 MEDIUM v0.46.0 0.52.0 golang.org/x/crypto/ssh: golang: golang.org/x/crypto/ssh: Denial of Service due
usr/bin/hydra golang.org/x/crypto CVE-2026-46598 🟡 MEDIUM v0.46.0 0.52.0 golang.org/x/crypto/ssh/agent: golang: golang.org/x/crypto/ssh/agent: Denial of
usr/bin/hydra golang.org/x/net CVE-2026-25681 🟠 HIGH v0.48.0 0.55.0 golang.org/x/net/html: golang.org/x/net/html: Arbitrary code execution via Cross
usr/bin/hydra golang.org/x/net CVE-2026-27136 🟠 HIGH v0.48.0 0.55.0 golang.org/x/net/html: golang: golang.org/x/net/html: Cross-Site Scripting via H
usr/bin/hydra golang.org/x/net CVE-2026-33814 🟠 HIGH v0.48.0 0.53.0 net/http/internal/http2: golang: golang.org/x/net: Go HTTP/2: Denial of Service
usr/bin/hydra golang.org/x/net CVE-2026-39821 🟠 HIGH v0.48.0 0.55.0 golang.org/x/net/idna: golang: net/http: golang.org/x/net/idna: Privilege escala
usr/bin/hydra golang.org/x/net CVE-2026-25680 🟡 MEDIUM v0.48.0 0.55.0 golang.org/x/net/html: golang.org/x/net/html: Denial of Service due to excessive
usr/bin/hydra golang.org/x/net CVE-2026-42502 🟡 MEDIUM v0.48.0 0.55.0 golang.org/x/net/html: golang: golang.org/x/net/html: Cross-Site Scripting via u
usr/bin/hydra golang.org/x/net CVE-2026-42506 🟡 MEDIUM v0.48.0 0.55.0 golang.org/x/net/html: golang.org/x/net/html: Cross-Site Scripting (XSS) via arb
usr/bin/hydra golang.org/x/text CVE-2026-56852 🟠 HIGH v0.32.0 0.39.0 A norm.Iter can enter an infinite loop when handling input containing ...
usr/bin/hydra google.golang.org/grpc GHSA-hrxh-6v49-42gf 🟠 HIGH v1.79.3 1.82.1 gRPC-Go: xDS RBAC and HTTP/2 Vulnerabilities
usr/bin/hydra stdlib CVE-2026-25679 🟠 HIGH v1.26.0 1.25.8, 1.26.1 net/url: Incorrect parsing of IPv6 host literals in net/url
usr/bin/hydra stdlib CVE-2026-27137 🟠 HIGH v1.26.0 1.26.1 crypto/x509: Incorrect enforcement of email constraints in crypto/x509
usr/bin/hydra stdlib CVE-2026-27145 🟠 HIGH v1.26.0 1.25.11, 1.26.4 crypto/x509: golang: golang crypto/x509: Denial of Service via excessive process
usr/bin/hydra stdlib CVE-2026-32280 🟠 HIGH v1.26.0 1.25.9, 1.26.2 crypto/x509: crypto/tls: golang: Go: Denial of Service vulnerability in certific
usr/bin/hydra stdlib CVE-2026-32281 🟠 HIGH v1.26.0 1.25.9, 1.26.2 crypto/x509: golang: Go crypto/x509: Denial of Service via inefficient certifica
usr/bin/hydra stdlib CVE-2026-32283 🟠 HIGH v1.26.0 1.25.9, 1.26.2 crypto/tls: golang: Go crypto/tls: Denial of Service via multiple TLS 1.3 key up
usr/bin/hydra stdlib CVE-2026-33810 🟠 HIGH v1.26.0 1.26.2 crypto/x509: golang: Go crypto/x509: Certificate validation bypass due to incorr
usr/bin/hydra stdlib CVE-2026-33811 🟠 HIGH v1.26.0 1.25.10, 1.26.3 net: golang: Go net package: Denial of Service via long CNAME response in Lookup
usr/bin/hydra stdlib CVE-2026-33814 🟠 HIGH v1.26.0 1.25.10, 1.26.3 net/http/internal/http2: golang: golang.org/x/net: Go HTTP/2: Denial of Service
usr/bin/hydra stdlib CVE-2026-39820 🟠 HIGH v1.26.0 1.25.10, 1.26.3 net/mail: golang: Go net/mail: Denial of Service via crafted email inputs
usr/bin/hydra stdlib CVE-2026-39822 🟠 HIGH v1.26.0 1.25.12, 1.26.5, 1.27.0-rc.2 os: golang: Go os.Root: Symlink following vulnerability allows directory travers
usr/bin/hydra stdlib CVE-2026-39836 🟠 HIGH v1.26.0 1.25.10, 1.26.3 ELSA-2026-22121: golang security update (IMPORTANT)
usr/bin/hydra stdlib CVE-2026-42499 🟠 HIGH v1.26.0 1.25.10, 1.26.3 net/mail: golang: net/mail: Denial of Service via pathological email address par
usr/bin/hydra stdlib CVE-2026-42504 🟠 HIGH v1.26.0 1.25.11, 1.26.4 mime: golang: Golang MIME: Denial of Service via maliciously-crafted MIME header
usr/bin/hydra stdlib CVE-2026-27142 🟡 MEDIUM v1.26.0 1.25.8, 1.26.1 html/template: URLs in meta content attribute actions are not escaped in html/te
usr/bin/hydra stdlib CVE-2026-32282 🟡 MEDIUM v1.26.0 1.25.9, 1.26.2 golang: internal/syscall/unix: Root.Chmod can follow symlinks out of the root
usr/bin/hydra stdlib CVE-2026-32288 🟡 MEDIUM v1.26.0 1.25.9, 1.26.2 archive/tar: golang: Go's archive/tar package: Denial of Service via maliciously
usr/bin/hydra stdlib CVE-2026-32289 🟡 MEDIUM v1.26.0 1.25.9, 1.26.2 html/template: golang: html/template: Cross-Site Scripting (XSS) via improper co
usr/bin/hydra stdlib CVE-2026-39823 🟡 MEDIUM v1.26.0 1.25.10, 1.26.3 html/template: golang: Go html/template: Cross-Site Scripting via improper URL e
usr/bin/hydra stdlib CVE-2026-39825 🟡 MEDIUM v1.26.0 1.25.10, 1.26.3 net/http/httputil: golang: net/http/httputil: ReverseProxy forwards hidden query
usr/bin/hydra stdlib CVE-2026-39826 🟡 MEDIUM v1.26.0 1.25.10, 1.26.3 html/template: golang: html/template: Cross-site scripting due to incorrect scri
usr/bin/hydra stdlib CVE-2026-42505 🟡 MEDIUM v1.26.0 1.25.12, 1.26.5, 1.27.0-rc.2 crypto/tls: golang: Go crypto/tls: Information disclosure in Encrypted Client He
usr/bin/hydra stdlib CVE-2026-42507 🟡 MEDIUM v1.26.0 1.25.11, 1.26.4 net/textproto: golang: Golang net/textproto: Misleading error messages via input
usr/bin/hydra stdlib CVE-2026-27138 ⚪ LOW v1.26.0 1.26.1 crypto/x509: Panic in name constraint checking for malformed certificates in cry
usr/bin/hydra stdlib CVE-2026-27139 ⚪ LOW v1.26.0 1.25.8, 1.26.1 os: FileInfo can escape from a Root in golang os module

Commit: 817efbf

@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown

Vulnerability Scan: Passed — diode-ingester

Image: diode-ingester:scan

No vulnerabilities found.

Commit: 817efbf

@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown

Vulnerability Scan: Passed — diode-reconciler

Image: diode-reconciler:scan

No vulnerabilities found.

Commit: 817efbf

@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown

Go test coverage

STATUS ELAPSED PACKAGE COVER PASS FAIL SKIP
🟢 PASS 1.49s github.com/netboxlabs/diode/diode-server/auth 57.4% 78 0 0
🟢 PASS 1.11s github.com/netboxlabs/diode/diode-server/auth/cli 0.0% 0 0 0
🟢 PASS 1.08s github.com/netboxlabs/diode/diode-server/authutil 82.8% 5 0 0
🟢 PASS 0.20s github.com/netboxlabs/diode/diode-server/dbstore/postgres 0.0% 0 0 0
🟢 PASS 1.14s github.com/netboxlabs/diode/diode-server/entityhash 79.2% 13 0 0
🟢 PASS 1.16s github.com/netboxlabs/diode/diode-server/entitymatcher 82.8% 97 0 0
🟢 PASS 0.09s github.com/netboxlabs/diode/diode-server/errors 0.0% 0 0 0
🟢 PASS 1.19s github.com/netboxlabs/diode/diode-server/graph 52.0% 81 0 0
🟢 PASS 1.03s github.com/netboxlabs/diode/diode-server/grpckeepalive 100.0% 1 0 0
🟢 PASS 1.46s github.com/netboxlabs/diode/diode-server/ingester 85.4% 66 0 0
🟢 PASS 1.09s github.com/netboxlabs/diode/diode-server/matching 94.1% 66 0 0
🟢 PASS 1.08s github.com/netboxlabs/diode/diode-server/migrator 70.4% 4 0 0
🟢 PASS 3.15s github.com/netboxlabs/diode/diode-server/netboxdiodeplugin 46.8% 23 0 0
🟢 PASS 0.17s github.com/netboxlabs/diode/diode-server/pprof 0.0% 0 0 0
🟢 PASS 5.10s github.com/netboxlabs/diode/diode-server/reconciler 74.8% 102 0 0
🟢 PASS 0.11s github.com/netboxlabs/diode/diode-server/reconciler/changeset 0.0% 0 0 0
🟢 PASS 1.06s github.com/netboxlabs/diode/diode-server/reconciler/differ 49.3% 23 0 0
🟢 PASS 1.02s github.com/netboxlabs/diode/diode-server/server 85.7% 14 0 0
🟢 PASS 1.01s github.com/netboxlabs/diode/diode-server/strcase 100.0% 24 0 0
🟢 PASS 1.02s github.com/netboxlabs/diode/diode-server/telemetry 26.2% 26 0 0
🟢 PASS 1.02s github.com/netboxlabs/diode/diode-server/telemetry/otel 90.2% 25 0 0
🟢 PASS 1.01s github.com/netboxlabs/diode/diode-server/tls 65.2% 6 0 0
🟢 PASS 1.01s github.com/netboxlabs/diode/diode-server/version 100.0% 2 0 0

Total coverage: 56.6%

@davidlanouette davidlanouette 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.

Some questions and some suggestions.

But, I wouldn't complain too much about merging it as-is.

Comment thread diode-server/auth/tokencache.go
Comment thread diode-server/auth/tokencache.go
Comment thread diode-server/auth/tokencache.go
Comment thread diode-server/auth/tokencache.go
Comment thread diode-server/auth/tokencache.go Outdated
Every client this service registers uses client_secret_post and the
authorization server rejects any other method, so credentials carried in an
Authorization header could only ever produce a rejection. Replace the
extraction with a refusal to cache those requests.

Keeping the detection still matters even though the extraction does not:
without it we would key on the form credentials while the upstream may
authenticate the header, which could cache one client's token under
another client's key.

Also records why an expired entry is dropped on read, and the invariant that
a cached rejection can never overwrite a live token.
CVE-2026-56852 (HIGH) in x/text 0.37.0 fails the container scan for all
three images. It is present on develop and unrelated to this branch, but the
scan only runs on pull requests touching diode-server, so nothing has
re-scanned develop since the advisory was published.

Bumped here rather than separately to unblock this PR. x/sync moves to
0.21.0 as a side effect of the resolution.
The log message fires when hydra starts binding, not when the mapped port
answers, so both hydra integration tests raced ahead and failed their first
admin call with connection refused. Latent where port forwarding is
instant, reproducible on Docker Desktop and Rancher Desktop.

Locally the suite also needs the docker socket path, an IPv4 host override,
and the reaper disabled, none of which are code issues.
A cached response is served without consulting the authorization server, so
it must grant exactly what that server would have granted. Valid is not
enough, it has to be the right token.

Mints twice through a cache-enabled server and once directly against hydra,
then compares every claim except the per-issuance ones. Identical token
strings across the first two prove the second was a cache hit, since two
issuances never share a jti; without that check the comparison would pass
just as happily with the cache switched off.

The comparison ignores a fixed set of claims rather than checking a fixed
set, so a claim nobody anticipated is compared by default. That matters for
claims injected by a token hook, where the wrong value would hand a caller
another tenant's authorization. A control asserts the comparison actually
distinguishes tokens issued for different clients.

@davidlanouette davidlanouette 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.

Looks great!

@mfiedorowicz
mfiedorowicz merged commit 5616b5b into develop Jul 31, 2026
12 checks passed
@mfiedorowicz
mfiedorowicz deleted the feat/auth-token-cache branch July 31, 2026 12:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants