Login
This commit is contained in:
+34
-21
@@ -1,5 +1,6 @@
|
||||
import hashlib
|
||||
import logging
|
||||
import re
|
||||
import secrets
|
||||
import uuid
|
||||
from datetime import timedelta
|
||||
@@ -9,6 +10,7 @@ from urllib.parse import urlsplit
|
||||
import stripe
|
||||
from authlib.integrations.django_client import OAuth
|
||||
from django.conf import settings
|
||||
from django.core.validators import validate_email
|
||||
from django.core.mail import send_mail
|
||||
from django.db import transaction
|
||||
from django.http import HttpResponse
|
||||
@@ -129,6 +131,9 @@ def finish_registration(request):
|
||||
remember_groups(account)
|
||||
Registration.objects.filter(uid=registration.uid).delete()
|
||||
request.session.pop("registration_hash", None)
|
||||
# Server-side, short-lived proof permits first binding of ONLY the
|
||||
# account just provisioned in this browser, never an email match.
|
||||
request.session["registration_identity"] = {"account_id": str(account.pk), "expires_at": (timezone.now() + timedelta(hours=1)).timestamp()}
|
||||
return page(request, title="Account created", message="Your free eom.dev account is ready. Sign in to explore the community and its services, or choose an optional paid membership for additional benefits.")
|
||||
except ValueError:
|
||||
form.add_error(None, "An account with those details may already exist. Try signing in or contact us for help.")
|
||||
@@ -163,32 +168,37 @@ def login(request):
|
||||
return page(request, title="Sign-in unavailable", message="Please try again later.")
|
||||
|
||||
|
||||
def link_identity(claims):
|
||||
def link_identity(claims, *, registration_account_id=None):
|
||||
if claims.get("iss") != settings.DEX_ISSUER or claims.get("email_verified") is not True or not isinstance(claims.get("sub"), str) or not claims["sub"] or len(claims["sub"]) > 512:
|
||||
raise ValueError("A verified Dex identity is required.")
|
||||
identity = directory.identity_for_login(claims.get("preferred_username"))
|
||||
uid, email = claims.get("preferred_username"), claims.get("email")
|
||||
if not isinstance(uid, str) or not re.fullmatch(r"[a-z][a-z0-9._-]{2,47}", uid):
|
||||
raise ValueError("Dex must return a valid uid as preferred_username.")
|
||||
if not isinstance(email, str) or len(email) > 254:
|
||||
raise ValueError("A verified Dex email is required.")
|
||||
validate_email(email)
|
||||
federated = claims.get("federated_claims") or {}
|
||||
if federated.get("connector_id") != settings.DEX_CONNECTOR_ID or federated.get("user_id") != identity["uid"]:
|
||||
if not isinstance(federated, dict) or federated.get("connector_id") != settings.DEX_CONNECTOR_ID or federated.get("user_id") != uid:
|
||||
raise ValueError("Dex must authenticate the configured LDAP connector with uid as idAttr.")
|
||||
if identity["email"].casefold() != str(claims.get("email", "")).casefold():
|
||||
raise ValueError("Dex and LDAP email disagree.")
|
||||
# Resolve by immutable LDAP entryUUID, never by email. Dex's federated user
|
||||
# ID remains uid so the unified connector preserves existing OIDC subjects.
|
||||
# Authentication is entirely OIDC. Never contact LDAP or silently rebind an
|
||||
# existing account to a different subject based on username/email matches.
|
||||
with transaction.atomic():
|
||||
uid_owner = Account.objects.filter(uid=identity["uid"]).first()
|
||||
if uid_owner and uid_owner.ldap_uuid != identity["ldap_uuid"]:
|
||||
raise ValueError("Username is already linked to another directory entry.")
|
||||
account, _ = Account.objects.get_or_create(ldap_uuid=identity["ldap_uuid"], defaults=identity)
|
||||
account = Account.objects.select_for_update().get(pk=account.pk)
|
||||
subject_owner = Account.objects.filter(oidc_sub=claims["sub"]).exclude(pk=account.pk).exists()
|
||||
if subject_owner:
|
||||
raise ValueError("Dex subject is already linked to another directory entry.")
|
||||
# A previous login may have used the retired www-ldap connector, whose
|
||||
# subjects used entryUUID. Rebind after validating the same LDAP entry.
|
||||
account = Account.objects.select_for_update().filter(oidc_issuer=claims["iss"], oidc_sub=claims["sub"]).first()
|
||||
if account:
|
||||
if account.uid != uid:
|
||||
raise ValueError("Dex identity username changed; an explicit migration is required.")
|
||||
else:
|
||||
account, created = Account.objects.select_for_update().get_or_create(uid=uid, defaults={"email": email, "oidc_issuer": claims["iss"], "oidc_sub": claims["sub"]})
|
||||
if not created:
|
||||
if account.oidc_sub:
|
||||
if account.oidc_issuer != claims["iss"] or account.oidc_sub != claims["sub"]:
|
||||
raise ValueError("Existing account requires an explicit Dex identity mapping.")
|
||||
elif str(account.pk) != registration_account_id:
|
||||
raise ValueError("Existing account requires an explicit Dex identity mapping.")
|
||||
account.oidc_issuer = claims["iss"]
|
||||
account.oidc_sub = claims["sub"]
|
||||
account.email = identity["email"]
|
||||
account.ldap_dn = identity["ldap_dn"]
|
||||
account.save(update_fields=["oidc_sub", "email", "ldap_dn"])
|
||||
account.email = email
|
||||
account.save(update_fields=["oidc_issuer", "oidc_sub", "email"])
|
||||
remember_groups(account)
|
||||
return account
|
||||
|
||||
@@ -199,7 +209,10 @@ def callback(request):
|
||||
token = dex().authorize_access_token(request, claims_options={"iss": {"essential": True, "value": settings.DEX_ISSUER}}, leeway=30)
|
||||
if not token.get("id_token"):
|
||||
raise ValueError("ID token is required.")
|
||||
account = link_identity(token.get("userinfo") or {})
|
||||
proof = request.session.get("registration_identity") or {}
|
||||
expires_at = proof.get("expires_at") if isinstance(proof, dict) else None
|
||||
registration_account_id = proof.get("account_id") if isinstance(expires_at, (int, float)) and expires_at > timezone.now().timestamp() else None
|
||||
account = link_identity(token.get("userinfo") or {}, registration_account_id=registration_account_id)
|
||||
except Exception as exc:
|
||||
logger.error("Dex callback rejected kind=%s", type(exc).__name__)
|
||||
return page(request, title="Sign-in could not be verified", message="Try signing in again, or contact the administrator.")
|
||||
|
||||
Reference in New Issue
Block a user