Skip to content

Skip upstream identity capture when sub_id is off - #6956

Merged
rdimitrov merged 1 commit into
mainfrom
authserver-skip-verified-upstream-when-off
Oct 27, 2026
Merged

rdimitrov merged 1 commit into
mainfrom
authserver-skip-verified-upstream-when-off

Conversation

@jhrozek

@jhrozek jhrozek commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Since #6908 (v0.57.0) the auth server stores the verified upstream identity (issuer, subject, stable claim and value) at every login, even when SubjectIdentifierAudiences is empty, which is the default. The only reader turns it into the sub_id and upstream_subject claims, and only for listed audiences. With the list empty, every one of those writes is stored data nobody reads. It is a stable identifier for a user at their upstream IdP, and it stays at rest for the lifetime of a refresh token. Deployments that seal upstream identity at rest had to strip it in their own storage layer across several store methods.

  • Record the identity at login only when SubjectIdentifierAudiences is not empty. Otherwise VerifiedUpstream stays zero, which downstream code already treats as "no claim".
  • Both login paths (the authorization-code callback and the device verification flow) ask one helper, Handler.recordedUpstreamIdentity, so a new login path cannot forget the check. carryFirstUpstreamIdentity now takes the value the helper returns.
  • Update the SubjectIdentifierAudiences docs and docs/arch/11-auth-server-storage.md to describe when the claims start to appear.

Behaviour with disclosure on is unchanged.

Fixes #6955

Type of change

  • Bug fix
  • New feature
  • Refactoring (no behavior change)
  • Dependency update
  • Documentation
  • Other (describe):

Test plan

  • Unit tests (task test)
  • E2E tests (task test-e2e)
  • Linting (task lint-fix)
  • Manual testing (describe below)

task test and task lint-fix both pass on this commit. I did not run the e2e tests or test against a kind cluster. The change does not touch RunConfig, CRDs or the operator.

Changes

File Change
pkg/authserver/server/handlers/callback.go Add recordedUpstreamIdentity; carryFirstUpstreamIdentity takes the identity it returns
pkg/authserver/server/handlers/device_verification.go Use recordedUpstreamIdentity in completeDeviceLogin
pkg/authserver/server/handlers/subject_identifier_test.go New tests: authorization-code session, next chain leg, device confirmation and device request, with the list empty and non-empty
pkg/authserver/integration_test.go Rename the refresh-policy test, change its "enabled after login" case to the new behaviour, and add a second login as a positive control
pkg/authserver/config.go, runner/embeddedauthserver.go, server/session/session.go, storage/types.go Doc comments updated
docs/arch/11-auth-server-storage.md Describe when the claims appear, rollout, and consumer guidance

Does this introduce a user-facing change?

Only for Go callers that set SubjectIdentifierAudiences. The setting is not reachable from config files, CRDs or the CLI, so thv, vMCP and operator users are not affected.

  • Before: the upstream identity was stored at every login. Enabling disclosure later on a running deployment added sub_id and upstream_subject to the next refresh of existing sessions.
  • After: nothing is recorded while the list is empty.
    • Going from an empty list to a non-empty one adds the claims only to sessions whose login happened while the list was non-empty. Other users get them from their next login.
    • Adding an audience to a list that is already non-empty still applies at the next refresh.
    • Emptying the list still stops the claims at the next refresh.

A user whose session predates the change can lack sub_id until they log in again, and nothing reports why. Consumers should not switch the key they use for a user depending on whether sub_id is present. If disclosure is enabled with a rolling restart, logins served by a replica that still has the empty list record nothing. The arch doc says all of this.

Special notes for reviewers

  • This reverses a choice made in Add opt-in upstream subject identifiers #6908. That PR stored the identity even with disclosure off so that a policy change would apply at the next refresh with no re-login. I think data minimisation is the better default, and the issue proposes it. The cost is the re-login described above. The integration test case "disclosure enabled after login" changed to match, and now also checks that a second login does get the claims.
  • The check is "list not empty", not per audience. token.go still requires every audience of a token to be listed before it adds the claims. The login-side check is looser on purpose: it only decides whether to record the value at all. I did not share a predicate between the two, because the conditions are different.
  • Nothing is cleaned up. Sessions created on v0.57.0 already hold the stored identity and keep it until their refresh token expires. Only new logins skip recording.
  • Why the check lives in a helper. Putting an if in CallbackHandler took it over the gocyclo limit, and two separate gates could drift apart.
  • Motivation outside this repo. Downstream deployments that seal upstream identity at rest can drop their workaround once they are on a release with this change.

Since #6908 the auth server stored the verified upstream identity
(issuer, subject, stable claim and value) at every login, even when
SubjectIdentifierAudiences was empty. Its only reader turns it into the
sub_id and upstream_subject claims, and only for listed audiences. With
the default empty list, every write was stored data nobody reads, and
deployments that keep upstream identity out of storage had to strip it
in their own storage layer.

Record the identity at login only when SubjectIdentifierAudiences is
not empty. Both login paths, the authorization-code callback and the
device verification flow, now ask one helper,
Handler.recordedUpstreamIdentity, so a future login path cannot forget
the check. Nothing downstream changes: a zero value already means "no
claim". Behaviour with disclosure on is unchanged.

This reverses a choice made in #6908, which stored the identity so that
a policy change applied at the next refresh. Now, going from an empty
list to a non-empty one adds the claims only to sessions whose login
happened while the list was non-empty. Other users get them from their
next login. Adding an audience to a list that is already non-empty
still applies at the next refresh, and emptying the list still stops
the claims at the next refresh.

Sessions created on v0.57.0 already hold the stored identity and keep
it until their refresh token expires. There is no cleanup step.

Update the SubjectIdentifierAudiences docs and the auth server storage
architecture page to describe both cases, tell consumers not to switch
a user's key based on whether sub_id is present, and note that a
rolling restart leaves some logins unrecorded.

Fixes #6955

Co-authored-by: Mecatl <noreply@mecatl.dev>
@github-actions github-actions Bot added the size/S Small PR: 100-299 lines changed label Oct 27, 2026

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

LGTM. Verified that the only two places that turn an upstream login result into VerifiedUpstream (carryFirstUpstreamIdentity in the callback and completeDeviceLogin) now both go through recordedUpstreamIdentity; every other write just copies an existing value through storage, and the sole reader (applySubjectIdentifier in token.go) is unchanged and still requires every audience to be listed. The looser "list non-empty" gate at login vs. per-audience gate at issuance is the right split, and the new tests cover auth-code, chain leg and device paths in both states.

@codecov

codecov Bot commented Oct 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 80.20%. Comparing base (1d6b531) to head (db153c5).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6956      +/-   ##
==========================================
- Coverage   80.21%   80.20%   -0.02%     
==========================================
  Files         844      844              
  Lines       85647    85649       +2     
==========================================
- Hits        68702    68695       -7     
- Misses      16940    16949       +9     
  Partials        5        5              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@rdimitrov
rdimitrov merged commit c80a999 into main Oct 27, 2026
47 checks passed
@rdimitrov
rdimitrov deleted the authserver-skip-verified-upstream-when-off branch October 27, 2026 11:23
@github-actions github-actions Bot mentioned this pull request Oct 27, 2026
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/S Small PR: 100-299 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

authserver: don't record VerifiedUpstream when sub_id disclosure is off

2 participants