OAuth2: bound state map with TTL, add PKCE S256 (M-7) #110
No reviewers
Labels
No labels
Compat/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
qwc-open/asiakirjat!110
Loading…
Reference in a new issue
No description provided.
Delete branch "feature/oauth2-state-ttl-pkce"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Workstream item #15. Audit finding M-7 (Medium): two OAuth2 flow issues.
states map[string]boolgrew on every/auth/oauth2hit and was only pruned when the corresponding callback consumed the state. An unauthenticated attacker spamming the endpoint kept entries forever.Changes
map[string]stateEntrywherestateEntry { expiresAt time.Time; codeVerifier string }.oauth2StateTTL = 10 * time.Minute.sweepExpiredLockeddrops aged entries on everyGenerateAuthURLcall. No janitor goroutine — the map is bounded by the number of in-flight issuances within any 10-minute window.GenerateAuthURLnow generates anoauth2.GenerateVerifier()per state and appendsS256ChallengeOption(verifier)to the auth URL. The verifier is stored in thestateEntry.ValidateState(s) bool→ConsumeState(s) (verifier string, ok bool). Handler passes the verifier through toHandleCallback.HandleCallback(ctx, code, codeVerifier)— verifier sent to the token endpoint viaoauth2.VerifierOption(RFC 7636 §4.5).Tests
New
oauth2_csrf_test.go:code_challenge=andcode_challenge_method=S256ConsumeStatereturns the verifier and removes the entry; second call failsConsumeState)Existing tests updated for the new
HandleCallback/ConsumeStatesignatures.Test plan
go test ./...passes/auth/oauth2, observecode_challenge=...&code_challenge_method=S256in the redirect URL🤖 Generated with Claude Code
Audit finding M-7. Two issues in the OAuth2 flow: 1. states map[string]bool grew on every /auth/oauth2 hit and was only pruned when the corresponding callback consumed the state. An unauthenticated attacker spamming the endpoint kept entries forever. 2. No PKCE (RFC 7636). A public-client or sloppy-redirect deployment was more exposed than necessary to authorization code interception. Changes: - stateEntry { expiresAt time.Time; codeVerifier string }; the map is map[string]stateEntry now. - oauth2StateTTL = 10 minutes. - sweepExpiredLocked drops aged entries on every GenerateAuthURL call — no janitor goroutine, map is bounded by in-flight issuances in any one TTL window. - GenerateAuthURL also calls oauth2.GenerateVerifier + S256ChallengeOption so the auth URL carries code_challenge + code_challenge_method=S256. - ValidateState becomes ConsumeState(state) (verifier string, ok bool). The handler passes verifier to HandleCallback. - HandleCallback signature gains codeVerifier; passes it to Exchange via oauth2.VerifierOption (RFC 7636 §4.5). New tests (oauth2_csrf_test.go) cover: - AuthCodeURL includes code_challenge + S256 - ConsumeState returns the verifier and deletes the entry - Expired state entries are rejected by ConsumeState - 100 issuances + age-out + 1 more issuance → sweep leaves only 1 Existing tests updated for the new HandleCallback / ConsumeState signatures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>