mirror of
https://github.com/m1k1o/neko.git
synced 2026-11-03 00:11:48 -06:00
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:
@@ -168,6 +168,12 @@ func (service *Service) Complete(ctx context.Context, stateToken, code string) (
|
|||||||
email = claim(idClaims, "email")
|
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) {
|
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")
|
log.Warn().Msg("OAuth admin_emails/user_emails are configured but the provider did not return an 'email' claim; request the 'email' scope")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,6 +70,21 @@ func boolClaim(claims map[string]any, field string) bool {
|
|||||||
return false
|
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 {
|
func administrator(email string, isAdmin bool, emails []string) bool {
|
||||||
if isAdmin {
|
if isAdmin {
|
||||||
return true
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -234,6 +234,10 @@ The sign-in endpoint is `GET /api/oauth/login`; the callback endpoint is `GET /a
|
|||||||
`admin_emails` and `user_emails` are matched against the `email` claim. Most providers only include that claim if the `email` scope is requested, so `scopes` must include `"email"` (in addition to `"openid"`) or the claim will be empty and these lists will never match.
|
`admin_emails` and `user_emails` are matched against the `email` claim. Most providers only include that claim if the `email` scope is requested, so `scopes` must include `"email"` (in addition to `"openid"`) or the claim will be empty and these lists will never match.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
:::warning
|
||||||
|
If the provider returns `email_verified: false`, Neko ignores the email, so it cannot match `admin_emails` or `user_emails`. If the claim is missing, the email is trusted, because many providers (including generic OAuth2 providers such as GitHub) do not return it at all. In that case `admin_emails` and `user_emails` rely on the identity provider having verified the address. If users can register or edit their email without verification on a provider that does not send `email_verified`, anyone could claim an address listed in `admin_emails` and obtain an administrator session. Require email verification at the provider or do not use these lists.
|
||||||
|
:::
|
||||||
|
|
||||||
For OAuth sessions, `GET /api/whoami` includes `extra_data`: the merged userinfo and ID-token claims, with userinfo values taking precedence. It is returned only to the authenticated session owner and is intended for profile-field troubleshooting. Do not place secrets in provider profile claims.
|
For OAuth sessions, `GET /api/whoami` includes `extra_data`: the merged userinfo and ID-token claims, with userinfo values taking precedence. It is returned only to the authenticated session owner and is intended for profile-field troubleshooting. Do not place secrets in provider profile claims.
|
||||||
|
|
||||||
For non-OIDC providers, set `authorization_url`, `token_url`, `userinfo_url`, and optionally `redirect_url` directly. For GitHub, for example, use `read:user` as a scope and set `subject_field: id`, `username_field: login`, and `avatar_field: avatar_url`.
|
For non-OIDC providers, set `authorization_url`, `token_url`, `userinfo_url`, and optionally `redirect_url` directly. For GitHub, for example, use `read:user` as a scope and set `subject_field: id`, `username_field: login`, and `avatar_field: avatar_url`.
|
||||||
|
|||||||
Reference in New Issue
Block a user