feat: SSO

This commit is contained in:
2026-05-28 23:59:14 +00:00
parent fc5699a04c
commit 7526736e80
9 changed files with 442 additions and 27 deletions
+76
View File
@@ -0,0 +1,76 @@
"""Logto OIDC helpers for external sign-in."""
import logging
import os
from authlib.integrations.flask_client import OAuth
REQUIRED_LOGTO_VARS = (
'LOGTO_OIDC_ENDPOINT',
'LOGTO_APP_ID',
'LOGTO_APP_SECRET',
)
oauth = OAuth()
def logto_sso_available() -> bool:
return all(os.environ.get(k) for k in REQUIRED_LOGTO_VARS)
def _endpoint() -> str:
return os.environ['LOGTO_OIDC_ENDPOINT'].rstrip('/')
def init_logto_oauth(app):
"""Register Authlib OAuth client when Logto env vars are present."""
oauth.init_app(app)
if not logto_sso_available():
return
endpoint = _endpoint()
oauth.register(
name='logto',
client_id=app.config['LOGTO_APP_ID'],
client_secret=app.config['LOGTO_APP_SECRET'],
server_metadata_url=f'{endpoint}/oidc/.well-known/openid-configuration',
client_kwargs={'scope': 'openid email profile'},
)
def extract_logto_email(oauth_client, token) -> str | None:
"""Resolve the user's email from Logto token claims or the userinfo endpoint."""
if not isinstance(token, dict):
return None
def normalize(value) -> str | None:
if not value or not isinstance(value, str):
return None
normalized = value.strip().lower()
return normalized if '@' in normalized else None
userinfo = token.get('userinfo')
for source in (userinfo, token):
if not source:
continue
email = normalize(source.get('email'))
if email:
return email
email = normalize(source.get('preferred_username'))
if email:
return email
try:
fetched = oauth_client.userinfo(token=token)
email = normalize(fetched.get('email'))
if email:
return email
email = normalize(fetched.get('preferred_username'))
if email:
return email
logging.warning(
'Logto userinfo did not include an email claim (scope=%s)',
token.get('scope'),
)
except Exception:
logging.exception('Failed to fetch Logto userinfo')
return None