fix!: reject OIDC login when email_verified claim is non-bool or absent#25713
Conversation
…or absent The email_verified claim check used a Go type assertion that silently passed when the claim was a string, number, or absent. This allowed unverified emails to bypass rejection. Add coerceEmailVerified() to handle string, numeric, and json.Number variants. Default to unverified (fail-closed) when the claim is missing or an unrecognized type. The existing IgnoreEmailVerified config option remains as an escape hatch for IdPs that never send the claim.
…tibility The FakeIDP encodeClaims now defaults email_verified to true (like exp, aud, iss) so enterprise and other OIDC tests that do not care about the verification flow continue to pass. Tests that need a specific value can set it explicitly.
|
/coder-agents-review |
|
Review posted | Chat Review history
deep-review v0.5.0 | Round 1 | Last posted: Round 1, 4 findings (1 P2, 1 P3, 1 Nit, 1 Note), COMMENT. Review Finding inventoryFindings
Contested and acknowledged(none) Round logRound 1Netero-only. 1 P2, 1 P3, 1 Nit, 1 Note. Reviewed against 20b50dd..384b786. About deep-reviewCRF = Coder Review Finding (P0-P4, Nit, Note)
|
There was a problem hiding this comment.
Solid security fix. The fail-closed rewrite is the right call, coerceEmailVerified covers the realistic IdP variants cleanly, and the unit test matrix is thorough (22 cases). One structural issue worth addressing before full panel review: the FakeIDP now defaults email_verified to true, which makes the absent-claim rejection path, the core behavioral change in this PR, unreachable in integration tests.
This is a first-pass review (Netero only). The full review panel has not yet reviewed this PR. The panel will review after these findings are addressed.
1 P2, 1 P3, 1 Nit, 1 Note.
“The encodeClaims default is a convenience that hides a coverage gap. The one scenario that changed from implicit-allow to explicit-deny is the one scenario no integration test can reach.” — Netero
🤖 This review was automatically generated with Coder Agents.
- Add WithOmitEmailVerifiedDefault() FakeIDP option so integration tests can exercise the handler's absent-claim rejection branch (CRF-1). - Add EmailVerifiedMissing and EmailVerifiedMissingIgnored standalone subtests that use the opt-out to cover the absent-claim path end-to-end (CRF-1, CRF-2). - Remove duplicate EmailVerifiedMissingIgnored table entry that was functionally identical to EmailNotVerifiedIgnored (CRF-2). - Remove unnecessary tc := tc loop variable capture (CRF-3).
Emyrk
left a comment
There was a problem hiding this comment.
Mark this as a breaking change.
Problem
The OIDC callback checks
email_verifiedvia a Go type assertion (verifiedRaw.(bool)). When an IdP returns the claim as a string ("false"), a number, or omits it entirely, the assertion fails silently and the email is implicitly treated as verified. Several real IdPs (SAML-to-OIDC bridges, certain Azure AD B2C configurations) emit string-typed booleans, making this reachable in practice.Fix
Add
coerceEmailVerified()to handlebool,string("true"/"false"/"1"/"0"viastrconv.ParseBool),float64,json.Number, andint/int64variants. Rewrite the check to be fail-closed: an absent claim, an unrecognized type, or any non-truthy value is treated as unverified and rejected. The existingIgnoreEmailVerifiedconfig option remains as an escape hatch.Fixes https://linear.app/codercom/issue/PLAT-228
Implementation plan
Production code (
coderd/userauth.go)encoding/jsonimportcoerceEmailVerified(v interface{}) (verified bool, ok bool)helper near EOFcoerceEmailVerifiedUnit tests (
coderd/userauth_internal_test.go, new file)bool,string("true","false","1","0","TRUE","t","f","invalid",""),json.Number,float64,int,int64,nil,[]string{},map[string]string{}Integration tests (
coderd/userauth_test.go,coderd/users_test.go)EmailVerifiedMissingIgnored(200),EmailVerifiedAsStringTrue(200),EmailVerifiedAsStringFalse(403)email_verifiedand expected success to include"email_verified": trueFakeIDP (
coderd/coderdtest/oidctest/idp.go)encodeClaimsnow defaultsemail_verifiedtotrue(likeexp,aud,iss) so tests that don't care about the verification flow are unaffected