Skip to content

Migrate CLI OAuth flow to pkg/auth/dcr resolver#5250

Draft
tgrunnagle wants to merge 4 commits into
dcr-4a_issue_5145from
dcr-4b_issue_5219
Draft

Migrate CLI OAuth flow to pkg/auth/dcr resolver#5250
tgrunnagle wants to merge 4 commits into
dcr-4a_issue_5145from
dcr-4b_issue_5219

Conversation

@tgrunnagle
Copy link
Copy Markdown
Contributor

@tgrunnagle tgrunnagle commented May 11, 2026

DRAFT - not ready for review

Summary

Sub-issue 4b of #5145. The CLI OAuth flow in pkg/auth/discovery::PerformOAuthFlow used to call oauthproto.RegisterClientDynamically directly, so it bypassed the review-property behaviours added during #5042 (S256 PKCE gating, RFC 7591 §3.2.1 expiry-driven refetch, bearer-token transport with redirect refusal, panic recovery, singleflight deduplication). This PR routes that call site through the shared pkg/auth/dcr resolver introduced in sub-issue 4a (PR #5198) so both consumers — the embedded authserver and the CLI — share the same hardened DCR path.

To make the resolver genuinely profile-agnostic, pkg/auth/dcr now exposes a Request struct carrying exactly the fields the resolver reads. The embedded-authserver adapter helpers move into pkg/authserver/runner/dcr_adapter.go where they belong by ownership, and ResolveCredentials no longer imports authserver or upstream domain types.

Closes #5145.

Type of change

  • Refactoring (no behavior change)

Test plan

  • Unit tests (task test)
  • Linting (task lint-fix)

New tests in pkg/auth/discovery/dcr_resolver_test.go cover the CLI's inherited properties:

  • S256 PKCE gating — CLI surfaces a clear resolver error when upstream advertises only plain.
  • Bearer-token transport redirect refusal — registration POSTs do not follow redirects.
  • Singleflight deduplication — concurrent invocations with the same (issuer, scopes, redirectURI) produce one /register call.
  • Fallback error preservation — TestHandleDynamicRegistration_MissingRegistrationEndpoint pins the verbatim "configure with --remote-auth-client-id / --remote-auth-client-secret" message.

API Compatibility

  • This PR does not break the v1beta1 API, OR the api-break-allowed label is applied and the migration guidance is described above.

No operator API surface is touched. The pkg/auth/dcr package was added in 4a (#5198) and has no external consumers; the ResolveCredentials signature change is internal.

Changes

File Change
pkg/auth/dcr/request.go New profile-neutral Request input type for ResolveCredentials.
pkg/auth/dcr/resolver.go ResolveCredentials now takes *dcr.Request; no longer imports authserver / upstream.
pkg/auth/dcr/store.go New CloseableCredentialStore interface; NewInMemoryStore returns it so defer Close() is compile-time safe.
pkg/auth/discovery/discovery.go handleDynamicRegistration builds a dcr.Request and calls dcr.ResolveCredentials instead of oauthproto.RegisterClientDynamically. Loopback redirect URI (RFC 8252 §7.3) and CLI-flag fallback error preserved.
pkg/auth/discovery/dcr_request.go Deleted — superseded by pkg/auth/dcr.Request.
pkg/auth/discovery/dcr_resolver_test.go New tests pinning the CLI's inherited review properties.
pkg/authserver/runner/dcr_adapter.go Embedded-authserver-specific adapter helpers (needsDCR, newDCRRequest, consumeResolution, applyResolutionToOAuth2Config) moved here from pkg/auth/dcr.
pkg/authserver/runner/embeddedauthserver.go Call sites updated to use the adapter and pass *dcr.Request into dcr.ResolveCredentials.
pkg/authserver/runner/dcr_store.go Deleted — folded into the shared pkg/auth/dcr/store.go (4a follow-through).

Does this introduce a user-facing change?

No. The CLI OAuth flow's observable behaviour is unchanged on the happy path. The new error paths (S256 gating, redirect refusal) are inherited security improvements that surface as clearer error messages when an upstream provider misbehaves.

Special notes for reviewers

Persistence model — option (b) from the issue. The resolver runs against an in-memory dcr.CredentialStore scoped to one PerformOAuthFlow invocation. Cross-invocation persistence is handled outside the resolver by pkg/auth/remote/handler.go's existing CachedClientID / CachedClientSecretRef fields, which already preserved cross-invocation reuse and continue to do so unchanged. Option (a) — wrapping the secret provider as a CredentialStore adapter — was rejected as out-of-scope churn; the trade-off is documented at the wiring site in handleDynamicRegistration. A consequence worth flagging: cross-invocation expiry refetch lives in the remote handler, not in the resolver, on the CLI path. Closing that loop is a natural follow-up once option (a) is wired.

PublicClient flag. A new bool on dcr.Request tells the resolver to register as a public PKCE client (token_endpoint_auth_method=none). The S256 gate still fires — the CLI surfaces a clear resolver error rather than silently downgrading when the upstream advertises only plain PKCE.

Resolver invariant — code review only. The issue's acceptance criterion for a CI grep guard against direct oauthproto.RegisterClientDynamically calls outside pkg/auth/dcr is intentionally out of scope for this PR. A task check-dcr-isolation Taskfile target and matching workflow step were prototyped and then removed (commit fd66a1f) per reviewer preference. The resolver boundary is enforced by code review alone going forward.

Sub-issue 4b of #5145. The CLI OAuth flow at
pkg/auth/discovery::PerformOAuthFlow used to call
oauthproto.RegisterClientDynamically directly, so it did not inherit the
review-property behaviours added during #5042 (S256 PKCE gating, RFC 7591
§3.2.1 expiry-driven refetch, bearer-token transport with redirect
refusal, panic recovery, singleflight deduplication). This commit routes
that call site through the shared pkg/auth/dcr resolver introduced in
sub-issue 4a (PR #5198) and pins the invariant with a CI grep guard.

Profile-neutral resolver input: pkg/auth/dcr now exposes a Request struct
that carries exactly the fields the resolver reads (issuer, redirect
URI, scopes, discovery URL or registration endpoint, optional explicit
endpoint overrides, initial access token, client name, public-client
flag). ResolveCredentials takes a Request and no longer imports
authserver / upstream domain types. The embedded-authserver adapter
helpers (needsDCR, consumeResolution, applyResolutionToOAuth2Config)
move to pkg/authserver/runner/dcr_adapter.go where they belong by
ownership.

CLI persistence model: option (b) from the issue. The resolver runs
against an in-memory dcr.CredentialStore scoped to one PerformOAuthFlow
invocation. Cross-invocation persistence is handled outside the resolver
by pkg/auth/remote/handler.go's existing CachedClientID /
CachedClientSecretRef fields, which already preserved cross-invocation
reuse and continue to do so unchanged. Wrapping the secretProvider into
a CredentialStore adapter (option (a)) was rejected as out-of-scope
churn — the existing remote-handler caching is sufficient.

PublicClient flag: a new bool on dcr.Request tells the resolver to
register as a public PKCE client (token_endpoint_auth_method=none).
The S256 gate still fires — the CLI surfaces a clear resolver error
rather than silently downgrading when upstream advertises only "plain".

Invariant guard: Taskfile target check-dcr-isolation (wired into task
lint) and a matching CI step in .github/workflows/lint.yml fail if
oauthproto.RegisterClientDynamically is referenced anywhere outside
pkg/auth/dcr or pkg/oauthproto.

Tests added for the CLI's inherited properties (S256 gating, redirect
refusal, singleflight deduplication) in
pkg/auth/discovery/dcr_resolver_test.go. The fallback error message for
upstreams that omit registration_endpoint is preserved verbatim and
pinned by TestHandleDynamicRegistration_MissingRegistrationEndpoint.

Closes #5145.
Fixed issues from code review:
- MEDIUM: Rewrote TestResolveSecret / TestResolveSecretWithEnvVar doc
  comments so they no longer point at the deleted dcr-package twin; the
  runner-side resolveSecret is now described as the single authoritative
  implementation.
- MEDIUM: Documented the redundant discovery fetch in resolveDCRCredentials
  as an acknowledged trade-off (the S256 PKCE gate needs
  code_challenge_methods_supported, which AuthServerInfo does not carry).
  Threading that field through AuthServerInfo is the natural follow-up.
- MEDIUM: Rewrote the dcr.Request.Issuer field doc to spell out what each
  consumer puts there and why the cache key cannot collide between the
  embedded authserver and CLI consumers. Added a matching call-site
  comment in resolveDCRCredentials.
- MEDIUM: Introduced dcr.CloseableCredentialStore (embeds CredentialStore
  + io.Closer). NewInMemoryStore now returns it, so the CLI's
  defer store.Close() is compile-time safe — no more anonymous-interface
  type-assertion that would silently no-op on a future refactor.
- MEDIUM: Reconciled the CLI flow's "expiry refetch inheritance" wording
  with what option (b) actually delivers. The handleDynamicRegistration
  doc and the dcr_resolver_test.go file-level comment now spell out that
  cross-invocation expiry handling lives in the remote handler, not the
  resolver, and that option (a) would close the loop as a follow-up.
- LOW: The unreachable "fall back to RegistrationEndpoint-direct path"
  comment is replaced by an accurate description of when each branch
  fires.
Fixed issues from second-iteration review:
- MEDIUM: Made dcr.inMemoryStore.Close() idempotent via sync.Once.
  storage.MemoryStorage.Close() closes a channel and is NOT itself
  idempotent — the previous wrapper inherited that defect through an
  incorrect doc comment ("Safe to call multiple times"). The wrapper now
  delivers what the comment claims: a second Close returns the captured
  error from the first call rather than panicking on
  "close of closed channel". Pinned by TestInMemoryStore_CloseIsIdempotent
  and TestInMemoryStore_CloseIsIdempotentUnderRace (8 concurrent callers).
- MEDIUM: Dropped the embedded storageBackedStore from inMemoryStore so
  the type holds a single *storage.MemoryStorage handle instead of two
  parallel handles (one via embedded interface, one concrete) that
  required a manual "keep these two in sync" invariant. Get and Put are
  implemented directly on inMemoryStore, delegating to s.mem — three
  lines each, structurally guaranteed to share a backend with Close.
  Pinned by TestInMemoryStore_PutGetCloseShareBackend,
  TestInMemoryStore_PutRejectsNilResolution, and
  TestInMemoryStore_GetMissingKeyReturnsMissTuple.
The Taskfile check-dcr-isolation grep guard and its workflow
counterpart added extra surface area to enforce an architectural
invariant. Removing both per reviewer preference; the resolver
boundary remains enforced by code review alone.
@github-actions github-actions Bot added the size/XL Extra large PR: 1000+ lines changed label May 11, 2026
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Large PR Detected

This PR exceeds 1000 lines of changes and requires justification before it can be reviewed.

How to unblock this PR:

Add a section to your PR description with the following format:

## Large PR Justification

[Explain why this PR must be large, such as:]
- Generated code that cannot be split
- Large refactoring that must be atomic
- Multiple related changes that would break if separated
- Migration or data transformation

Alternative:

Consider splitting this PR into smaller, focused changes (< 1000 lines each) for easier review and reduced risk.

See our Contributing Guidelines for more details.


This review will be automatically dismissed once you add the justification section.

@tgrunnagle tgrunnagle changed the base branch from main to dcr-4a_issue_5145 May 11, 2026 19:52
@github-actions github-actions Bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels May 11, 2026
Copy link
Copy Markdown
Contributor Author

@tgrunnagle tgrunnagle left a comment

Choose a reason for hiding this comment

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

Multi-Agent Consensus Review

Agents consulted: oauth-security, go-correctness, architecture, concurrency, test-coverage, general-code-quality (codex cross-review skipped — CLI not installed)

Consensus Summary

# Finding Consensus Severity Action
1 PublicClient absent from singleflight + cache keys 9/10 MEDIUM Discuss
2 applyExplicitEndpointOverrides bypasses URL validation 7/10 MEDIUM Fix
3 Two-call consumeResolution/applyResolutionToOAuth2Config invariant not type-enforced 8/10 MEDIUM Discuss
4 SanitizeErrorForLog is http(s)-only — redis-go error chain 7/10 MEDIUM Discuss
5 DCRKey.Issuer doc contradicts new CLI usage 8/10 MEDIUM Fix
6 _ = store.Close() swallows error without comment/log 7/10 LOW Fix
7 TestNewDCRRequest missing env-var IAT branch 7/10 LOW Fix
8 TestConsumeResolution does not pin nil-input no-op 6/10 LOW Optional
9 LogStepError "upstream" attribute shape varies per consumer 7/10 LOW Discuss
10 Direct-registration + PublicClient=true error msg is misleading 6/10 LOW Optional
11 Wall-clock ClientSecretExpiresAt comparison lacks skew tolerance 6/10 LOW Optional
12 PR body still reads "DRAFT - not ready for review" 7/10 LOW Fix
13 Option-(b) rationale duplicated across two function docs 6/10 LOW Optional

Overall

This is a well-executed migration of the CLI OAuth flow onto the shared pkg/auth/dcr resolver. The package boundary is structurally clean (resolver no longer imports any authserver/discovery types — verified), the new dcr.Request envelope is genuinely profile-neutral, and the new inherited-property tests (S256 gate, redirect refusal, singleflight dedup) pin exactly the security-relevant behaviours that motivated the migration. The PR description, doc comments, and orientation prose are unusually good — a cold reviewer can pick up the architectural picture in a single pass.

The consensus findings cluster around one theme: correctness invariants encoded in comments rather than types. The singleflight + cache keys do not include PublicClient, the two-call resolution invariant relies on review, the sanitiser is scheme-specific despite the generic name, and DCRKey.Issuer's field doc still claims "local issuer ... NOT the upstream's" even though the CLI now puts the upstream issuer there. Each one is a small refactor away from being structural. Current call sites do not exhibit the failure modes by construction; the risk is forward-looking, not active.

None of the findings rise to a hard blocker. Recommended priorities are #1 (PublicClient in keys), #3 (two-call consolidation), and #5 (DCRKey doc fix) — all cheap, all close defense-in-depth gaps the author already calls out in inline comments.

For consistency, please note: the authenticated reviewer and PR author are the same user, so this review uses COMMENT rather than REQUEST_CHANGES — this is not a draft-blocker assessment, it's an artefact of GitHub's policy.

Additional findings (file not in this PR's diff, or line outside diff hunks)

  • [MEDIUM] DCRKey.Issuer doc driftpkg/authserver/storage/types.go:122-130 documents Issuer as "the local issuer of the embedded authorization server that performed the registration, NOT the upstream's". The new CLI call site at pkg/auth/discovery/discovery.go:712-713 puts the upstream's issuer URL there. The new dcr.Request.Issuer doc accommodates both consumers, but the underlying storage type's field doc still claims the old single-consumer semantic. Update DCRKey.Issuer to reflect the dual semantic, e.g. "For the embedded authserver this is its own (local) issuer; for the CLI flow this is the upstream's issuer because the CLI has no separate logical issuer of its own. The cache key (Issuer, RedirectURI, ScopesHash) keeps the two consumers' entries apart via the RedirectURI component."
  • [MEDIUM] SanitizeErrorForLog is http(s)-onlypkg/auth/dcr/resolver.go:481-538 (outside this PR's diff hunks). The function name reads generic, but the implementation only strips userinfo/query/fragment from http(s):// URLs. On the embedded-authserver path, cache.Get/Put errors from storage/redis.go flow into LogStepError. If a future operator (or redis-go release) puts credentials in a sentinel/cluster URL, those credentials hit the slog.Error attribute. Either rename to sanitizeHTTPURLs so the scope is unambiguous, or extend the regex to also match redis(s)?://. Add a small test asserting a redis-style error string with credentials is sanitised.
  • [LOW] LogStepError "upstream" attribute is inconsistent per consumerpkg/auth/dcr/resolver.go:452-479 (outside this PR's diff hunks). Embedded-authserver path passes rc.Name (operator-defined short name); CLI path passes issuer (upstream issuer URL). Operators reading mixed logs see two different shapes under the same key. Either rename the parameter to upstreamID and document it as opaque, or accept slog.Attr pairs so callers can label ("upstream_name" vs "upstream_issuer").
  • [LOW] ClientSecretExpiresAt wall-clock comparison lacks skew tolerancepkg/auth/dcr/resolver.go:641 (outside this PR's diff hunks). time.Now().After(cached.ClientSecretExpiresAt) is exact. NTP step on resume or AS-clock-ahead skew leads to either serving an expired secret (already broken at the AS) or refetching too early. Consider time.Now().Add(30 * time.Second).After(cached.ClientSecretExpiresAt) as defense-in-depth.
  • [LOW] PR body still reads "DRAFT - not ready for review" — The PR is in draft state and the body header says "DRAFT - not ready for review". Since you are explicitly requesting a review now, either flip out of draft or remove the header so other reviewers landing on this PR aren't confused about its status.

Dropped findings appendix (consensus < 7)

The following observations were raised by individual agents but did not meet the consensus threshold. Listed for auditability only — no action requested:

  • Regex (?i)https?://[^\s"']+ can match across a comma (security LOW, score 5). Failure mode is "did not sanitise", not corrupted output.
  • selectTokenEndpointAuthMethod with empty serverSupported + publicClient=true trusts upstream to accept "none" (security LOW, score 5).
  • CloseableCredentialStore loses Close capability when widened to base interface (architecture LOW, score 6). No occurrence in this PR.
  • Hardcoded CallbackPort: 8765 in CLI tests (test INFO, score 5). Tests don't bind the port — acceptable today.
  • dcr.Key is generically named (quality INFO, score 5).
  • flightKeyOf(key) is a free function where a method would read more idiomatically (quality LOW, score 5).
  • Long doc comments on Request/Resolution partially restate code (quality LOW, score 5).
  • handleDynamicRegistration doc preamble is 26 lines for a 45-line body (quality LOW, score 5).
  • ResolveCredentials takes *Request by pointer; could take by value (architecture LOW, score 5).
  • PublicClient bool tri-state suggestion to fail-loud on unset (security INFO, score 5).
  • CLI cross-invocation expiry refetch not in resolver's loop (architecture INFO, score 6) — explicitly documented as option-(b) trade-off.

Generated with Claude Code

Comment thread pkg/auth/dcr/resolver.go
// either of those spaces would silently coalesce; if a third profile is
// added the flight key MUST gain a consumer-identifier component so a
// collision is impossible rather than improbable.
var dcrFlight singleflight.Group
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.

[MEDIUM] PublicClient absent from singleflight and cache keys (Consensus: 9/10)

The flight key (and storage.DCRKey) is (Issuer, RedirectURI, ScopesHash). req.PublicClient is not part of either. The singleflight closure captures the leader's req and returns the leader's *Resolution to every follower — so two callers with different PublicClient values whose keys happen to match would receive the same registration. Today's two consumers don't collide by construction (CLI loopback redirect vs AS-origin redirect), but the comment two lines down already calls this out: "if a third profile is added the flight key MUST gain a consumer-identifier component so a collision is impossible rather than improbable."

The same gap also affects the persisted Redis-backed cache for the embedded authserver: if option-(a) (wrap the CLI's secret provider as a CredentialStore) ever lands, a CLI invocation could read a confidential-client resolution the AS persisted.

Recommendation: include PublicClient (or a typed ConsumerKind enum tag on dcr.Request) in both storage.DCRKey and flightKeyOf. Cost: one extra field + one extra concat. Benefit: the safety invariant becomes structural rather than review-only.

Raised by: oauth-security (HIGH), concurrency (MEDIUM), architecture (MEDIUM), go-correctness (LOW), code-quality (INFO)

Comment thread pkg/auth/dcr/resolver.go
endpoints.authorizationEndpoint = rc.AuthorizationEndpoint
// from req when req specifies them. Explicit caller configuration always
// wins over discovery.
func applyExplicitEndpointOverrides(endpoints *dcrEndpoints, req *Request) {
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.

[MEDIUM] applyExplicitEndpointOverrides bypasses validateUpstreamEndpointURL (Consensus: 7/10)

endpointsFromMetadata runs validateUpstreamEndpointURL against discovered authorization_endpoint / token_endpoint. This function then overwrites them with req.AuthorizationEndpoint / req.TokenEndpoint without re-validating. The validation contract documented on validateUpstreamEndpointURL claims "every point where an endpoint URL enters the resolver from outside" — the override path violates that promise.

Today's two callers (newDCRRequest, resolveDCRCredentials) populate these from already-validated sources, so this is defense-in-depth, not an active bug. But the cost to close the gap is trivial.

Suggested change
func applyExplicitEndpointOverrides(endpoints *dcrEndpoints, req *Request) {
func applyExplicitEndpointOverrides(endpoints *dcrEndpoints, req *Request) error {
if req.AuthorizationEndpoint != "" {
if err := validateUpstreamEndpointURL(req.AuthorizationEndpoint, "authorization_endpoint"); err != nil {
return fmt.Errorf("dcr: explicit %w", err)
}
endpoints.authorizationEndpoint = req.AuthorizationEndpoint
}
if req.TokenEndpoint != "" {
if err := validateUpstreamEndpointURL(req.TokenEndpoint, "token_endpoint"); err != nil {
return fmt.Errorf("dcr: explicit %w", err)
}
endpoints.tokenEndpoint = req.TokenEndpoint
}
return nil
}

(Signature change requires updating the one caller at registerAndCache to propagate the error via newDCRStepError(dcrStepMetadata, …).)

Raised by: oauth-security

Comment thread pkg/auth/dcr/resolver.go

if publicClient {
if !pkceS256Advertised {
return "", fmt.Errorf(
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.

[LOW] Misleading error for direct-registration + PublicClient=true (Consensus: 6/10)

When a caller passes RegistrationEndpoint (no DiscoveryURL) and PublicClient=true, the resolver correctly refuses — but via this branch with the message "public client requested but upstream does not advertise S256 in code_challenge_methods_supported (got [])". Operators who never asked for discovery will look for a metadata document that doesn't exist in their config.

Recommendation: detect the PublicClient=true && RegistrationEndpoint != "" && DiscoveryURL == "" case earlier (in validateResolveInputs) and return: "PublicClient=true requires DiscoveryURL so the S256 PKCE gate can be evaluated; got RegistrationEndpoint only". The behaviour is correct today; only the diagnostic needs sharpening.

Raised by: oauth-security

// fully-resolved DCR client. Forgetting the second call leaves
// ClientSecret empty and produces silent auth failures at request time —
// the type system does not enforce the pair, so the invariant lives here.
func applyResolutionToOAuth2Config(cfg *upstream.OAuth2Config, res *dcr.Resolution) {
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.

[MEDIUM] Two-call invariant not type-enforced (Consensus: 8/10)

The doc on this function acknowledges the gap: "Forgetting the second call leaves ClientSecret empty and produces silent auth failures at request time — the type system does not enforce the pair, so the invariant lives here." The only call site (buildUpstreamConfigs) is correct, but a future call site that copies the consumeResolution pattern would silently produce a no-secret config — exactly the "silent no-op that breaks callers" pattern .claude/rules/go-style.md warns against.

Recommendations (pick one):

  1. Collapse into a single applyDCRResolution(rc *OAuth2UpstreamRunConfig, cfg *upstream.OAuth2Config, res *dcr.Resolution) that writes everything atomically.
  2. Keep the split but add a regression test that exercises only consumeResolution and asserts cfg.ClientSecret == "" — a tripwire so any future code-shaped refactor has an explicit failure mode.

Raised by: architecture (MEDIUM), go-correctness (LOW)

}

// getDiscoveryDocument retrieves the OIDC discovery document
// resolveDCRCredentials routes the CLI-flow DCR registration through the
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.

[LOW] Option-(b) rationale duplicated across handleDynamicRegistration and resolveDCRCredentials (Consensus: 6/10)

Both function docs carry multi-paragraph explanations of the option-(b) persistence trade-off in overlapping vocabulary. They are not duplicates (one is about persistence, the other about the redundant discovery fetch), but they share substantial conceptual ground and reference the same sub-issue numbering. A future maintainer modifying option-(b) behaviour must update both.

Recommendation: pick this function (where the trade-off actually manifests) as the canonical site. In handleDynamicRegistration, summarise in 2-3 lines and link here.

Raised by: code-quality

}

store := dcr.NewInMemoryStore()
defer func() { _ = store.Close() }()
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.

[LOW] _ = store.Close() silently drops the error (Consensus: 7/10)

.claude/rules/go-style.md: "Comment ignored errors — explain why and typically log them." inMemoryStore.Close returns nil today, but a future change to storage.MemoryStorage.Close (e.g. timeout on cleanup goroutine teardown) would surface a real error and the CLI would drop it without a trace.

Suggested change
defer func() { _ = store.Close() }()
defer func() {
if err := store.Close(); err != nil {
slog.Debug("dcr: in-memory store close failed", "error", err)
}
}()

Raised by: concurrency

}
}

func TestConsumeResolution_RespectsExplicitEndpoints(t *testing.T) {
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.

[LOW] TestConsumeResolution does not pin the nil-input no-op contract (Consensus: 6/10)

consumeResolution returns early on rc == nil || res == nil, but no test exercises that early-return. TestApplyResolutionToOAuth2Config does cover nil cfg and nil res (lines 145-147), so the asymmetry is conspicuous. A regression that dereferenced rc before the nil check would crash the embedded-authserver boot in a hard-to-diagnose way.

Recommendation: add a one-line subtest that calls consumeResolution(nil, &dcr.Resolution{}) and consumeResolution(&authserver.OAuth2UpstreamRunConfig{}, nil) and asserts no panic. Cheap, high-signal.

Raised by: test-coverage

// TestNewDCRRequest covers the OAuth2UpstreamRunConfig → dcr.Request
// translation, including the file-based InitialAccessToken resolution that
// previously lived inside the resolver.
func TestNewDCRRequest(t *testing.T) {
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.

[LOW] TestNewDCRRequest missing env-var InitialAccessToken branch (Consensus: 7/10)

The four cases cover file-based IAT, no IAT, nil run-config, and missing dcr_config. The env-var sibling (InitialAccessTokenEnvVar) is not exercised end-to-end through newDCRRequestresolveSecret. The underlying resolveSecret is independently covered in embeddedauthserver_test.go::TestResolveSecretWithEnvVar, but the adapter-level wiring boundary is not pinned — a refactor that swapped the argument order or accidentally read the wrong field on DCRConfig would not be caught.

Recommendation: add a fifth table case that sets DCRConfig.InitialAccessTokenEnvVar via t.Setenv on a unique env-var name (drop t.Parallel() for that subcase, matching the TestResolveSecretWithEnvVar pattern) and asserts the resulting req.InitialAccessToken.

Raised by: test-coverage

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/XL Extra large PR: 1000+ lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Consolidate the two RFC 7591 DCR client implementations into pkg/auth/dcr

1 participant