mirror of
https://github.com/open-webui/open-webui.git
synced 2026-11-10 08:47:14 +00:00
feat: add opt-in OAUTH_MERGE_REFUSE_UNVERIFIED_EMAIL to refuse merging on provider-reported unverified emails
When OAUTH_MERGE_ACCOUNTS_BY_EMAIL is enabled, the callback binds a new (provider, sub) identity to an existing account looked up by the email claim alone. If the identity provider explicitly reports that email as unverified (OIDC email_verified=false, or verified=false on the GitHub /user/emails entry the code already fetches), an IdP that lets users assert arbitrary addresses would hand over the matching existing account. The new flag (default off - zero behavior change) refuses the merge with a 400 in exactly that case. Providers that send no verification signal at all are unaffected either way, so deployments relying on merge for IdPs without the claim concept keep working. Signed-off-by: SashaMIT <sash@ela.city> Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
8faaf2cd1e
commit
b8472c1b80
2 changed files with 45 additions and 2 deletions
|
|
@ -2464,6 +2464,12 @@ OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE = os.getenv('OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE
|
|||
|
||||
OAUTH_MERGE_ACCOUNTS_BY_EMAIL = os.getenv('OAUTH_MERGE_ACCOUNTS_BY_EMAIL', 'False').lower() == 'true'
|
||||
|
||||
# When merging OAuth accounts by email is enabled, refuse the merge if the
|
||||
# identity provider explicitly reports the email address as unverified
|
||||
# (email_verified=false claim, or GitHub's /user/emails verified flag).
|
||||
# Providers that send no verification signal are unaffected. Off by default.
|
||||
OAUTH_MERGE_REFUSE_UNVERIFIED_EMAIL = os.getenv('OAUTH_MERGE_REFUSE_UNVERIFIED_EMAIL', 'False').lower() == 'true'
|
||||
|
||||
OAUTH_PROVIDERS = {}
|
||||
|
||||
GOOGLE_CLIENT_ID = os.getenv('GOOGLE_CLIENT_ID', '')
|
||||
|
|
@ -3125,6 +3131,7 @@ DEFAULT_CONFIG = {
|
|||
'oauth.auto_redirect': OAUTH_AUTO_REDIRECT,
|
||||
'oauth.refresh_token.include_scope': OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE,
|
||||
'oauth.merge_accounts_by_email': OAUTH_MERGE_ACCOUNTS_BY_EMAIL,
|
||||
'oauth.merge_refuse_unverified_email': OAUTH_MERGE_REFUSE_UNVERIFIED_EMAIL,
|
||||
'oauth.google.client_id': GOOGLE_CLIENT_ID,
|
||||
'oauth.google.client_secret': GOOGLE_CLIENT_SECRET,
|
||||
'oauth.google.scope': GOOGLE_OAUTH_SCOPE,
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ from open_webui.config import (
|
|||
OAUTH_GROUPS_CLAIM,
|
||||
OAUTH_GROUPS_SEPARATOR,
|
||||
OAUTH_MERGE_ACCOUNTS_BY_EMAIL,
|
||||
OAUTH_MERGE_REFUSE_UNVERIFIED_EMAIL,
|
||||
OAUTH_PICTURE_CLAIM,
|
||||
OAUTH_PROVIDERS,
|
||||
OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE,
|
||||
|
|
@ -131,6 +132,11 @@ OAUTH_RUNTIME_CONFIG = {
|
|||
'OAUTH_MERGE_ACCOUNTS_BY_EMAIL': (
|
||||
'oauth.merge_accounts_by_email',
|
||||
OAUTH_MERGE_ACCOUNTS_BY_EMAIL,
|
||||
OAUTH_MERGE_REFUSE_UNVERIFIED_EMAIL,
|
||||
),
|
||||
'OAUTH_MERGE_REFUSE_UNVERIFIED_EMAIL': (
|
||||
'oauth.merge_refuse_unverified_email',
|
||||
OAUTH_MERGE_REFUSE_UNVERIFIED_EMAIL,
|
||||
),
|
||||
'ENABLE_OAUTH_ROLE_MANAGEMENT': (
|
||||
'oauth.enable_role_mapping',
|
||||
|
|
@ -1848,6 +1854,10 @@ class OAuthManager:
|
|||
# Email extraction
|
||||
email_claim = auth_config.OAUTH_EMAIL_CLAIM
|
||||
email = user_data.get(email_claim, '')
|
||||
# Whether the provider vouches for the email address. None means
|
||||
# the provider gave no verification signal; set from the GitHub
|
||||
# /user/emails entry below or from the email_verified claim.
|
||||
email_verified = None
|
||||
# We currently mandate that email addresses are provided
|
||||
if not email:
|
||||
# If the provider is GitHub,and public email is not provided, we can use the access token to fetch the user's email
|
||||
|
|
@ -1864,12 +1874,16 @@ class OAuthManager:
|
|||
if resp.ok:
|
||||
emails = await resp.json()
|
||||
# use the primary email as the user's email
|
||||
primary_email = next(
|
||||
(e['email'] for e in emails if e.get('primary')),
|
||||
primary_entry = next(
|
||||
(e for e in emails if e.get('primary')),
|
||||
None,
|
||||
)
|
||||
primary_email = (
|
||||
primary_entry.get('email') if primary_entry else None
|
||||
)
|
||||
if primary_email:
|
||||
email = primary_email
|
||||
email_verified = bool(primary_entry.get('verified'))
|
||||
else:
|
||||
log.warning('No primary email found in GitHub response')
|
||||
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
|
||||
|
|
@ -1885,6 +1899,15 @@ class OAuthManager:
|
|||
log.warning(f'OAuth callback failed, email is missing: {user_data}')
|
||||
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
|
||||
|
||||
# OIDC providers send an email_verified claim when they track
|
||||
# verification; the GitHub fallback above already set it.
|
||||
if email_verified is None:
|
||||
verified_claim = user_data.get('email_verified')
|
||||
if isinstance(verified_claim, bool):
|
||||
email_verified = verified_claim
|
||||
elif isinstance(verified_claim, str):
|
||||
email_verified = verified_claim.lower() == 'true'
|
||||
|
||||
email = email.lower()
|
||||
# If allowed domains are configured, check if the email domain is in the list
|
||||
if (
|
||||
|
|
@ -1899,6 +1922,19 @@ class OAuthManager:
|
|||
if not user:
|
||||
# If the user does not exist, check if merging is enabled
|
||||
if auth_config.OAUTH_MERGE_ACCOUNTS_BY_EMAIL:
|
||||
# Opt-in hardening: never merge on an email the provider
|
||||
# explicitly reports as unverified. An IdP that lets users
|
||||
# assert arbitrary addresses would otherwise hand over the
|
||||
# matching existing account (account takeover). Providers
|
||||
# that send no verification signal are unaffected.
|
||||
if (
|
||||
auth_config.OAUTH_MERGE_REFUSE_UNVERIFIED_EMAIL
|
||||
and email_verified is False
|
||||
):
|
||||
log.warning(
|
||||
f'OAuth callback failed, refusing to merge account on unverified email: {email}'
|
||||
)
|
||||
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
|
||||
# Check if the user exists by email
|
||||
user = await Users.get_user_by_email(email, db=db)
|
||||
if user:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue