Validate CIMD scope, grant_types and response_types against AS policy#5385
Conversation
1e060ed to
90db2a6
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #5385 +/- ##
==========================================
+ Coverage 68.76% 68.77% +0.01%
==========================================
Files 634 633 -1
Lines 64183 64229 +46
==========================================
+ Hits 44136 44175 +39
- Misses 16778 16784 +6
- Partials 3269 3270 +1 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
90db2a6 to
070de2d
Compare
070de2d to
8e68d45
Compare
C3 - Thread ScopesSupported into NewCIMDStorageDecorator so CIMD scope
handling is consistent with DCR. Uses registration.ValidateScopes
(same function as the DCR handler) to validate declared scopes
against the AS allowlist and compute the effective scope list.
When ScopesSupported is unset, the document's declared scopes are
used directly; omitted scopes default to DefaultScopes.
C4 - Reject CIMD documents that declare grant_types or response_types
the embedded AS does not support for public clients
(authorization_code + refresh_token; code). Consistent with DCR
which returns invalid_client_metadata for the same cases.
buildFositeClient now receives pre-computed scopes from fetch() rather
than re-parsing doc.Scope, matching the DCR handler pattern where scope
computation and validation happen before client construction.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
8e68d45 to
36b5e97
Compare
tgrunnagle
left a comment
There was a problem hiding this comment.
Multi-agent review
Four specialist agents (oauth/security, Go correctness/style, test quality, general/API) reviewed this PR. Codex cross-review was skipped (codex CLI not installed locally). 0 HIGH, 5 MEDIUM, 7 LOW findings survived consensus scoring. None are blocking; this is posted as COMMENT.
Executive summary
A focused follow-up to stacked PR #5348 closing deferred review comments C3/C4. It threads ScopesSupported and BaselineClientScopes into the CIMD storage decorator so CIMD documents are validated against the same policy as DCR (via registration.ValidateScopes), and rejects unsupported grant_types / response_types at fetch time. As supporting refactor, unionScopes is promoted to registration.UnionScopes. Approach is correct in shape: reuse a single validator, reject earlier in the pipeline, surface a startup warning for the CIMD+baseline interaction. Most surviving findings are correctness-leaning details and test-quality nits.
Pros
- Reuses
registration.ValidateScopesinstead of duplicating policy. - Rejects invalid CIMD documents at fetch time (not at
/authorize), giving operators a clear, early error. - Error class change
ErrNotFound→ErrInvalidClientfor "fetched but policy-violating" is semantically correct per RFC 6749 §5.2. - Public/PKCE invariant preserved (
Public: trueunconditional; allowlists still excludeclient_credentials). slog.Warnat startup surfaces the non-obvious CIMD+baseline interaction.- Doc comments explain the design intent and the test-only nil escape hatch.
- PR title, body, and size comply with
.claude/rules/pr-creation.md.
Cons
- The "same scope policy as DCR" claim has one silent divergence: when
doc.Scope == ""andScopesSupported⊃DefaultScopes, the CIMD client receives the fullScopesSupportedwhile a DCR client in the same position receives onlyDefaultScopes. See F5. NewCIMDStorageDecoratornow takes six positional args with two adjacent same-typed[]stringslices — easy to swap silently.TestUnionScopeswas not relocated alongside the moved function and still lives inpkg/authserver/server/handlers/, wherescopes.gois now empty.- New rejection tests assert
require.Error(...)only — they would still pass if the code regressed tofosite.ErrNotFound, defeating the PR's main intent.
Consensus findings table
| # | Severity | File | Title |
|---|---|---|---|
| F1 | MEDIUM | handlers/scopes_test.go | TestUnionScopes lives in wrong package after move; pair with deleting now-empty scopes.go |
| F2 | MEDIUM | storage/cimd_decorator_test.go | Rejection tests don't assert errors.Is(err, fosite.ErrInvalidClient) |
| F3 | MEDIUM | storage/cimd_decorator_test.go | Consolidate 11 new top-level tests into ~3 table-driven tests |
| F4 | MEDIUM | storage/cimd_decorator.go | NewCIMDStorageDecorator signature: 6 positional args, two adjacent same-typed slices |
| F5 | MEDIUM | storage/cimd_decorator.go | Omitted-scope branch on CIMD diverges from DCR when ScopesSupported ⊃ DefaultScopes |
| F6 | LOW | storage/cimd_decorator.go | WithHint(dcrErr.Error) passes OAuth error code where descriptive hint belongs |
| F7 | LOW | storage/cimd_decorator.go | Constructor stores caller-supplied scope slices without defensive slices.Clone |
| F8 | LOW | storage/cimd_decorator.go | Doc comments document cross-file invariants not enforced in code |
| F9 | LOW | storage/cimd_decorator.go | Missing DCR-parity defense-in-depth: response_types must include 'code'; grant-type validation order reversed vs DCR |
| F10 | LOW | storage/cimd_decorator_test.go | Test names don't match assertions |
| F11 | LOW | storage/cimd_decorator_test.go | Redundant unit-level test; missing baseline-exceeds-supported coverage |
| F12 | LOW | authserver/server_impl.go | slog.Warn level/clarity for CIMD+baseline interaction |
Detailed comments are inline on the relevant changed lines.
How to test
git fetch origin pull/5385/head:pr-5385 && git checkout pr-5385
task lint-fix
task test
Plus targeted checks against an embedded AS:
- C3 — declared scope outside
ScopesSupported: serve a CIMD doc declaringscope: "openid profile"withscopes_supported: ["openid"]. Expectfosite.ErrInvalidClient. - C3 — omitted scope: same setup but doc omits
scope. Confirm the client carries exactly["openid"], notDefaultScopes(and consider what should happen whenScopesSupported⊃DefaultScopes— see F5). - C4 — unsupported grant_type: serve a doc with
grant_types: ["client_credentials"]. Expect rejection. - C4 — missing
authorization_code: serve a doc withgrant_types: ["refresh_token"]only. Expect rejection. - C4 — unsupported response_type: serve a doc with
response_types: ["token"]. Expect rejection. - slog.Warn: start the AS with
cimd.enabled: trueandbaseline_client_scopes: ["offline_access"]. Expect one WARN line at startup. - Load-bearing-test check: temporarily revert
if !allowedCIMDGrantTypes[gt]to always-false, re-rungo test ./pkg/authserver/storage/....TestFetch_RejectsUnsupportedGrantTypeshould fail; revert when done.
Generated with Claude Code (multi-agent /dev:pr-review).
F1 Move TestUnionScopes to registration package where UnionScopes lives;
delete now-empty handlers/scopes.go and handlers/scopes_test.go
F2 Add assert.ErrorIs(ErrInvalidClient)/NotErrorIs(ErrNotFound) to
all CIMD policy rejection tests to pin the error type change
F4 Replace 6 positional NewCIMDStorageDecorator args with
CIMDDecoratorConfig struct — prevents silent swap of adjacent []string
F5 Omitted-scope now calls ValidateScopes(nil, scopesSupported) matching
DCR: returns DefaultScopes when DefaultScopes ⊆ ScopesSupported,
error otherwise (document must declare scope explicitly)
F6 Fix dcrErr.Error → dcrErr.ErrorDescription in scope validation hint
so the human-readable description reaches the fosite hint field
F7 slices.Clone scope slices in CIMDDecoratorConfig constructor
F8 Fix buildFositeClient doc: "when empty" not "when nil"
F9 Export ValidatePublicGrantTypes/ValidatePublicResponseTypes from
registration package; replace hand-rolled loops in cimd_decorator.go
with calls to these shared validators — grant_type/response_type
validation is now identical on both DCR and CIMD paths
F10 Rename AcceptsSupportedGrantTypes→AcceptsOmittedGrantTypes and
RejectsRefreshTokenOnly→RejectsGrantTypesMissingAuthorizationCode
F11 Remove redundant TestBuildFositeClient_ScopeDefaultsToScopesSupported
(real decision lives in fetch(), not buildFositeClient)
F12 Update slog.Warn message to name the security consequence when
CIMD+BaselineClientScopes are both configured
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
1dd4677 to
7c14ee2
Compare
tgrunnagle
left a comment
There was a problem hiding this comment.
Re-review LGTM — all 12 findings from the prior multi-agent review are resolved. F3 went from 11 separate tests to 3 table-driven tests (plus a serveCIMDDocWithFields helper), F5 chose the fail-closed option (require explicit scope when DefaultScopes ⊄ ScopesSupported), and F9 extracted ValidatePublicGrantTypes / ValidatePublicResponseTypes so DCR and CIMD now share a single validation source. The errors.Is(err, fosite.ErrInvalidClient) assertions pin the error-type change so a regression to ErrNotFound would fail tests.
One nice-to-have follow-up (not blocking)
defaultCIMDGrantTypes and defaultCIMDResponseTypes in cimd_decorator.go are still parallel to defaultGrantTypes / defaultResponseTypes in the registration package. After F9 the validation is shared, but the default application is still duplicated — if someone later widens registration.defaultGrantTypes for DCR (e.g., adds a new public-client grant), they'd need to remember to update the CIMD copy too, and task lint won't catch the mismatch.
Since ValidatePublicGrantTypes / ValidatePublicResponseTypes already return the defaulted slice, buildFositeClient could use those return values and the CIMD-side constants could be deleted. Closes the last DCR/CIMD parity gap. Not blocking for this PR — could be a small follow-up.
Approving.
Summary
C3 — CIMD scope handling now consistent with DCR:
ScopesSupportedis threaded intoNewCIMDStorageDecorator. Scope validation infetch()now usesregistration.ValidateScopes— the same function as the DCR handler — so both paths enforce the same scope policy. A CIMD document declaring scopes outsideScopesSupportedis rejected atGetClienttime. The fallback whenscopeis omitted also respectsScopesSupported.C4 — grant_types / response_types validated at fetch time:
fetch()now rejects documents declaringgrant_typesoutside[authorization_code, refresh_token]orresponse_typesoutside[code]. Consistent with DCR which returnsinvalid_client_metadatafor the same cases, and matches the existingtoken_endpoint_auth_methodvalidation pattern already in the decorator.Type of change
Test plan
go test ./pkg/authserver/...passestask lint-fixcleanScopesSupported,grant_typerejection for unsupported values,response_typerejection fortokenGenerated with Claude Code