Skip to content

feat(proxy): add optional OIDC access token audience validation - #3466

Merged
rhafer merged 6 commits into
opencloud-eu:mainfrom
zerox80:codex/oidc-audiences
Sep 16, 2026
Merged

rhafer merged 6 commits into
opencloud-eu:mainfrom
zerox80:codex/oidc-audiences

Conversation

@zerox80

@zerox80 zerox80 commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Description

Add optional OIDC access token audience validation through PROXY_OIDC_AUDIENCES or oidc.audiences in proxy.yaml. The list defaults to empty for compatibility. When configured, a token must contain at least one exact, case-sensitive match in its aud claim. Missing, empty, malformed and nonmatching audiences are rejected during token verification.

Token verification, including the audience check, runs on Userinfo cache misses. Cache hits reuse unexpired cached claims without verifying the token again or requesting Userinfo. Existing entries in a shared or persistent cache can therefore remain accepted under the previous audience configuration until they expire. The documentation explains clearing the Userinfo cache after updating all proxy instances when a policy change must take effect immediately.

Audience configuration is validated when creating the OIDC client, so invalid settings fail during startup. The proxy logs an audience warning only when OIDC and JWT verification are enabled and the audience list is empty. The README includes ENV and YAML examples, the built-in IDP's client-ID audience behavior, Keycloak setup guidance and a recommendation to enable the check in production.

The token parsing optimization is in #3530 and can be reviewed independently.

Related Issue

Fixes #3456

Motivation and Context

When an IdP serves several applications, OpenCloud can currently accept an otherwise valid access token issued for another application. This gives administrators an explicit way to restrict tokens to OpenCloud without breaking existing IdP configurations by default.

How Has This Been Tested?

Tested on Ubuntu WSL with Go 1.25.9 and GCC, using the vendored dependencies.

  • go test -mod=vendor -race -count=1 ./pkg/oidc/... ./services/proxy/... passed after the constructor validation changes and rebase onto main.
  • The local changed-lines golangci-lint fast check reached its 60-second timeout while loading packages; no lint result was produced. Full lint validation is left to CI.
  • git diff --check passed.

The tests use locally generated keys and an HTTP test IdP with discovery, JWKS and Userinfo endpoints. They cover both skip_user_info settings, audience matching and rejection, cached Userinfo reuse, expired or corrupt cache entries, logout invalidation, ENV/YAML precedence, rejection of invalid configuration during client setup and the startup warning, including its suppression with verification set to none. A middleware test verifies that cache misses call token verification once and cache hits skip it. Signature, issuer, nbf, token expiry and AD FS issuer checks are also covered. No external IdP is needed.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Technical debt
  • Tests only (no source changes)

Checklist

  • Code changes
  • Unit tests added
  • Acceptance tests added
  • Documentation added

HTTP integration tests are included in the Go suite. The Behat acceptance suite was not extended.

GPT 6 Astra was used as a supporting tool for the implementation and tests.

@codacy-production

codacy-production Bot commented Sep 5, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 89 complexity

Metric Results
Complexity 89

View in Codacy

🟢 Coverage 74.60% diff coverage · +0.12% coverage variation

Metric Results
Coverage variation +0.12% coverage variation (-1.00%)
Diff coverage 74.60% diff coverage

View coverage diff in Codacy

Coverage variation details
Coverable lines Covered lines Coverage
Common ancestor commit (92560db) 88625 21142 23.86%
Head commit (72d2047) 88660 (+35) 21258 (+116) 23.98% (+0.12%)

Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch: <coverage of head commit> - <coverage of common ancestor commit>

Diff coverage details
Coverable lines Covered lines Diff coverage
Pull request (#3466) 63 47 74.60%

Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified: <covered lines added or modified>/<coverable lines added or modified> * 100%

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@zerox80

zerox80 commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

Do you think a small performance benchmark for the cache-hit path would be useful here, given that the access token now needs to be verified even on cache hits?

@zerox80

zerox80 commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

I did a small performance investigation of the audience-enabled cache-hit path.

The additional cost is dominated by the required JWT signature verification. Audience-enabled cache hits were around 99 µs/op compared to around 8 µs/op without per-hit verification.

I looked into avoiding repeated verification through policy-scoped caching, but that would weaken the current behavior around JWKS key removal/replacement and would also require broader cache/logout invalidation changes, so I kept that out of this PR.

I did find one small safe optimization in the existing verification path: avoiding the second full JWT parse after successful verification. In controlled serial benchmarks this reduced audience-enabled cache-hit time by about 5–7% and saves 1,088 B / 22 allocations per hit, while leaving the audience-disabled path unchanged.

Full affected package tests and race tests pass.

@zerox80
zerox80 force-pushed the codex/oidc-audiences branch from 5847b9e to bf738a1 Compare September 5, 2026 16:17

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

@zerox80 Thanks a lot for you efforts!

Would you mind submitting the fix(config): correct pending version annotations commit as a separate PR and remove it from this one?

We could fast-track that one, as it is straight-forward. The rest needs a more thorough review.

@zerox80

zerox80 commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

@zerox80 Thanks a lot for you efforts!

Would you mind submitting the fix(config): correct pending version annotations commit as a separate PR and remove it from this one?

We could fast-track that one, as it is straight-forward. The rest needs a more thorough review.

done

@github-project-automation github-project-automation Bot moved this to Qualification in OpenCloud Team Board Sep 14, 2026
@rhafer rhafer moved this from Qualification to In Progress in OpenCloud Team Board Sep 14, 2026
Comment thread changelog/unreleased/feature-oidc-audiences.md Outdated

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

Thanks again. Here's round of feedback after a quick look through the code.

Comment thread pkg/oidc/client.go Outdated
Comment thread services/proxy/pkg/middleware/oidc_auth.go Outdated

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

Just a few smaller comments after some testing.

Also: Please rebase this once more on the latest main branch to get the latest CI fixes.

Comment thread services/proxy/pkg/command/oidc.go Outdated

func newOIDCAuthenticator(logger log.Logger, cfg *config.Config, userInfoCache store.Store, httpClient *http.Client) *middleware.OIDCAuthenticator {
if cfg.OIDC.Issuer != "" && len(cfg.OIDC.Audiences) == 0 {
logger.Warn().Msg("OIDC access token audience validation is disabled. Configure PROXY_OIDC_AUDIENCES to enable it; this is recommended for production.")

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.

We should only log this if AccessTokenVerifyMethod != "none'. Otherwise this is misleading.

Comment thread pkg/oidc/client.go Outdated
if strings.TrimSpace(audience) == "" {
return RegClaimsWithSID{}, jwt.MapClaims{}, errors.New("access token audiences must not contain empty or whitespace-only entries")
}
}

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 config validation should IMO happen in NewOIDCClient, so that we have a chance to fail early (NewOIDCClient would obviously need a separate error return value for that), instead of failing every single request.

Comment thread services/proxy/README.md
[audience validation requirement in RFC 9068, Section 4](https://www.rfc-editor.org/rfc/rfc9068.html#name-validating-jwt-access-token):
a resource server following that JWT access token profile must reject tokens
whose audience does not identify the resource server.

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.

I think we should add a few words/links about:

Comment thread services/proxy/pkg/command/oidc.go Outdated
"go-micro.dev/v4/store"
)

func newOIDCAuthenticator(logger log.Logger, cfg *config.Config, userInfoCache store.Store, httpClient *http.Client) *middleware.OIDCAuthenticator {

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.

Nitpick: please move this function to server.go. I think it not needed to have it in a separate file.

@zerox80
zerox80 force-pushed the codex/oidc-audiences branch from 5707906 to 05a8e79 Compare September 15, 2026 11:23
Comment thread services/proxy/README.md Outdated
Co-authored-by: Ralf Haferkamp <ralf@h4kamp.de>
@rhafer
rhafer merged commit e6699f3 into opencloud-eu:main Sep 16, 2026
67 checks passed
@github-project-automation github-project-automation Bot moved this from In Progress to Done in OpenCloud Team Board Sep 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Allow configuring valid audiences

2 participants