oauth: ignore email when provider reports email_verified=false.

An unverified email could match admin_emails/user_emails on providers that
let users set their own email. Only an explicit false is rejected; providers
that omit the claim are still trusted. Document the behavior.

Reported-by: Zachariah Mithani <zachariah@aplosai.com> (AploSec)
This commit is contained in:
Miroslav Šedivý
2026-10-28 17:05:56 +01:00
parent 92f99168b8
commit 23d8a2718e
4 changed files with 47 additions and 0 deletions
+6
View File
@@ -168,6 +168,12 @@ func (service *Service) Complete(ctx context.Context, stateToken, code string) (
email = claim(idClaims, "email")
}
// Providers that omit email_verified are trusted; only an explicit false is rejected.
if email != "" && (claimFalse(claims, "email_verified") || claimFalse(idClaims, "email_verified")) {
log.Warn().Msg("OAuth provider reported the email as unverified; ignoring it for admin_emails/user_emails")
email = ""
}
if email == "" && (len(config.AdminEmails) > 0 || len(config.UserEmails) > 0) {
log.Warn().Msg("OAuth admin_emails/user_emails are configured but the provider did not return an 'email' claim; request the 'email' scope")
}
+15
View File
@@ -70,6 +70,21 @@ func boolClaim(claims map[string]any, field string) bool {
return false
}
// claimFalse reports whether the claim is present and explicitly false.
func claimFalse(claims map[string]any, field string) bool {
value, ok := claims[field]
if !ok {
return false
}
switch value := value.(type) {
case bool:
return !value
case string:
return strings.EqualFold(value, "false")
}
return false
}
func administrator(email string, isAdmin bool, emails []string) bool {
if isAdmin {
return true
@@ -0,0 +1,22 @@
package oauth
import "testing"
func TestClaimFalse(t *testing.T) {
cases := []struct {
name string
claims map[string]any
want bool
}{
{"absent", map[string]any{}, false},
{"true", map[string]any{"email_verified": true}, false},
{"false", map[string]any{"email_verified": false}, true},
{"string true", map[string]any{"email_verified": "true"}, false},
{"string false", map[string]any{"email_verified": "False"}, true},
}
for _, tc := range cases {
if got := claimFalse(tc.claims, "email_verified"); got != tc.want {
t.Errorf("%s: got %v, want %v", tc.name, got, tc.want)
}
}
}