"""
Email magic link authentication provider.
Users enter their email, receive a magic link, click it and they're logged in.
No passwords needed. Domain restriction ensures only allowed users can access.
Email delivery modes:
1. SMTP relay (recommended) - configure SMTP_HOST, SMTP_PORT, etc. in .env
2. Console mode (development) - link printed to server log, shown in browser
"""
import logging
import smtplib
import time
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from flask import (
Blueprint,
current_app,
flash,
redirect,
render_template,
request,
session,
url_for,
)
from itsdangerous import BadSignature, SignatureExpired, URLSafeTimedSerializer
from auth import AuthProvider
from webapp.auth import validate_email_domain
from webapp.config import Config
logger = logging.getLogger(__name__)
email_bp = Blueprint("email_auth", __name__)
# SVG envelope icon for the login button
_EMAIL_ICON_HTML = (
'"
)
def _get_serializer() -> URLSafeTimedSerializer:
"""Create token serializer using the app secret key."""
return URLSafeTimedSerializer(Config.SECRET_KEY, salt="email-magic-link")
def _generate_magic_token(email: str) -> str:
"""Generate a signed, time-limited token containing the email."""
s = _get_serializer()
return s.dumps({"email": email.lower(), "t": int(time.time())})
def _verify_magic_token(token: str, max_age_seconds: int = 900) -> str | None:
"""Verify magic link token. Returns email if valid, None otherwise.
Args:
token: The signed token from the magic link URL.
max_age_seconds: Token validity period (default 15 minutes).
Returns:
Email address if token is valid, None otherwise.
"""
s = _get_serializer()
try:
data = s.loads(token, max_age=max_age_seconds)
return data.get("email")
except SignatureExpired:
logger.warning("Magic link token expired")
return None
except BadSignature:
logger.warning("Invalid magic link token")
return None
def _send_magic_email(email: str, magic_url: str) -> bool:
"""Send magic link email via SMTP relay.
Returns True if sent successfully, False otherwise.
"""
smtp_host = Config.SMTP_HOST
if not smtp_host:
return False
msg = MIMEMultipart("alternative")
msg["Subject"] = f"Sign in to {Config.INSTANCE_NAME}"
msg["From"] = Config.SMTP_FROM
msg["To"] = email
text_body = (
f"Sign in to {Config.INSTANCE_NAME}\n\n"
f"Click the link below to sign in:\n{magic_url}\n\n"
f"This link expires in 15 minutes.\n"
f"If you didn't request this, ignore this email."
)
html_body = f"""