Repository navigation
test(auth): cover remaining auth failure paths; fix invite re-send and sticky KMS signer errors - #1152
test(auth): cover remaining auth failure paths; fix invite re-send and sticky KMS signer errors#1152IAmJSD wants to merge 3 commits into
Conversation
…d sticky KMS signer errors Issue LeanerCloud#263 lists auth failure paths with no negative test. Most were covered by the fixes that landed since the audit; two still reproduced and are fixed here, fail-closed: - A03-009: resetRecentlyIssued derived every token's age from the 1h PasswordResetExpiry, so a 7-day invite token looked negative-aged for almost the whole week and Forgot Password, the documented recovery when an invite email fails, silently did nothing. A remaining window longer than PasswordResetExpiry can only be an invite, so its age is now derived from PasswordSetupExpiry; reset tokens are unchanged and an out-of-range window still rate-limits. - A03-014: the AWS, Azure and GCP KMS signers resolved the public key in sync.Once and stored the error with it, so one failed first lookup (a throttle, a blip, an already-canceled ctx) broke every later KeyID/PublicKey/Mint for the process lifetime. A shared publicKeyCache now memoizes only success, under a mutex so concurrent first callers never fetch in parallel. Tests: invited-user forgot-password (unit, boundary table, and a real Postgres run that races two requests through the expiry CAS and completes the re-sent link), and transient-then-success plus concurrent-first-caller cases for all three signers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
Comment |
|
VERDICT: APPROVE c0a4764 Reviewed head c0a4764 against origin/main ca1e8a8. Findings
Verified (file:line)
AttackedEach mutation probe was run against the merged tree, then the file was restored and diff-verified:
Local results (macOS, merged tree)
|
|
Independent review 2 (claude-fleet, cold): APPROVE c0a4764 Both remaining bugs are fixed at the root. Everything passes on head and on the tree merged with origin/main, the new tests fail on base, and the behaviour-changing mutation probes are caught except two test gaps. Findings (none blocking):
Root cause:
Attacked:
Verified locally, with no cloud credentials, under the heavy lock, using real Postgres via the repo's testcontainers harness:
Reviewed by an independent agent with no author context (second reviewer). Review only: nothing was pushed, and the GitHub review state is unchanged. |
Resolve the conflict in internal/oidc/azure_signer.go: main (LeanerCloud#1167) added a kty/crv check (validateES256KeyType) inside resolveOnce, and this branch replaced resolveOnce with fetchPublicKey behind publicKeyCache. The check now runs in fetchPublicKey at the same point, after the missing X/Y check and before the coordinate parse, and returns its error instead of memoizing it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…blicKey error Close the two test gaps independent review 2 found with surviving mutation probes: - TestResetRecentlyIssued gets an "invite token about to expire" row: an invite with just over PasswordResetExpiry left must still be aged as an invite (not limited). Raising the invite threshold by a minute (probe P7) now fails it. - TestKMSSignerPublicKeyErrorIsUntypedNil checks, for the AWS, Azure and GCP signers, that a failed lookup returns a nil crypto.PublicKey interface. Returning a typed-nil *ecdsa.PublicKey again (probe P4) now fails it. Refs LeanerCloud#263 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Merged origin/main (a14dc8f) to resolve the conflict, and closed the two test gaps from independent review 2. New head: Conflict:
|
Summary
Issue #263 lists auth failure paths that had no negative test. I checked each item against current
main(2d822f3). Most are now covered by tests that came in with the fixes merged since the audit. Two still reproduced. This PR adds their regression tests and fixes both, fail-closed:carveout_wildcard_test.go(from #2069)TestLogin_WithMFA_RecoveryCode_PersistenceFailureandTestIntegration_LoginRecoveryPersistenceFailure(#462)DeleteUserSessionsand a rejected session (#89)TestService_UpdateUser/update_active_status_successfully,TestService_ValidateSession/deactivated_user_session_rejectedTestMFASetupAndEnable_RefusedWhileEnabledservice_password_test.goTestResolveBastionProvider_LegacyFallbackinto an error assertion once the fallback is removed (A03-011)Root cause and fix
A03-009: forgot-password silently did nothing for invited users
resetRecentlyIssuedworked out a token's age asPasswordResetExpiry - time.Until(expiry), which treats every stored token as a 1h reset token. Invite tokens are issued withPasswordSetupExpiry(7d), so an invite's age came out negative for about 6d23h. During that timeRequestPasswordResetreturned nil without issuing a token or sending mail. That is the recovery pathCreateUserResultsends the admin to when the invite email fails.Fix (
internal/auth/service_password.go): a reset token never has more thanPasswordResetExpiryleft, so a longer remaining window can only belong to an invite. In that case the age is now derived fromPasswordSetupExpiry. Reset tokens are handled exactly as before. A remaining window longer than even the invite lifetime still gives a negative age, so it stays rate-limited (fail-closed). No schema change.Concurrency: an invited user's request now reaches the existing
SetPasswordResetTokencompare-and-swap onpassword_reset_expiry. The new integration test runs a second request between the first one's read and its write, on real Postgres. Only the request that commits first stores and mails a token. The losing request mails nothing.A03-014: KMS signers cached the first lookup error forever
All three KMS-backed signers (AWS, Azure Key Vault, GCP) resolved the public key inside
sync.Onceand stored the error alongside it. One failed firstGetPublicKey/GetKey(a throttle, a network blip, or a caller whose ctx was already canceled) made every laterKeyID/PublicKey/Mintfail until the instance recycled. That takes down every federated Azure/GCP credential exchange.Fix (
internal/oidc/signer.goplus the three signers): a sharedpublicKeyCachememoizes only a successful resolution. A failed fetch goes back to its caller, and the next call fetches again. A mutex serializes first callers, so concurrent callers never fetch in parallel and a success is fetched exactly once. As before, waiters block while the first fetch is in flight, the same as withsync.Once. On errorPublicKeynow returns an untyped nil instead of a typed-nil*ecdsa.PublicKey. Validation errors (wrong curve, not ECDSA, incomplete JWK) still fail every call. They are just no longer memoized.Verification
All of this is local unit, mock and fixture evidence, plus real local PostgreSQL 16 via testcontainers. Nothing calls real AWS KMS, Azure Key Vault or GCP KMS: the signer tests use in-process fakes that wrap the existing fake clients with a failure gate. Tests ran with
AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/nullbecause this machine's default AWS profile is invalid.Fail-before (new tests against the unfixed production code):
Pass-after (
c0a4764f):Mutation probes. For each probe I applied one production mutation, ran the named tests, and restored the file. Every probe was killed:
coversCarvedOutcompares exact pairs instead ofcheckPermissionMatch(reintroduces cli#1901)TestGrantCeiling_WildcardMoneyVerbNotGrantable/create_execute:*ConsumeMFARecoveryCodefailureTestLogin_WithMFA_RecoveryCode_PersistenceFailureDeleteUserSessionsTestService_UpdateUser/update_active_status_successfully(+2 more)ValidateSessiondrops the!user.ActivecheckTestService_ValidateSession/deactivated_user_session_rejectedMFASetupdrops theMFAEnabledrefusalTestMFASetupAndEnable_RefusedWhileEnabledRequestPasswordResetdrops theDeactivatedAtrefusalTestService_RequestPasswordReset/deactivated_account_gets_no_reset_link,...ConfirmPasswordResetdrops theDeactivatedAtrefusalTestService_ConfirmPasswordReset/admin-deactivated_account_cannot_self-reactivate_via_resetTestService_RequestPasswordReset_InvitedUser,TestResetRecentlyIssuedTestResetRecentlyIssued/reset_token_just_issued,TestService_RequestPasswordReset_RateLimitTestService_RequestPasswordReset_InvitedUser/invite_inside_the_rate_limit_window_is_not_reissuedTestKMSSignerRetriesFailedKeyLookup(all backends)TestKMSSignerRetriesFailedKeyLookup(lookup count)TestKMSSignerConcurrentFirstResolutionunder-race(all backends)SetPasswordResetTokenCAS onpassword_reset_expiryneutralizedTestIntegration_InvitedUserForgotPasswordReissuesLink(real Postgres)Out of scope
AWSResolveOptions.AccountLookup/STSClientFactory(scheduler, org discovery, purchase, marketplace, AWS probe, server). MakingresolveBastionProviderfail closed on its own would therefore break every bastion account on every path. The real fix needs those callers wired, and that belongs in its own change. I leftTestResolveBastionProvider_LegacyFallbackas it is rather than write a test that pins a fallback nobody has removed. I found no separate issue tracking A03-011. This PR only refers to test(auth): carve-out, recovery-code, deactivation and reset-token failures have no test #263 (it does not close it), so test(auth): carve-out, recovery-code, deactivation and reset-token failures have no test #263 stays open to track that item.Refs #263 (the bastion fallback test, A03-011, stays open until the fallback is removed)
Maintainers: please mirror the issue's triage labels onto this PR:
priority/p2,severity/medium,impact/internal,type/chore,effort/m,urgency/this-quarter,triaged.🤖 Generated with Claude Code