From 5fdb839f5a1fd241185941dbf0aafd079bc1a837 Mon Sep 17 00:00:00 2001 From: Apple Date: Wed, 26 Nov 2025 12:06:55 -0800 Subject: [PATCH] feat: Add Matrix user provisioning on registration - matrix_provisioning.py: Create Matrix users via Synapse admin API - Auto-create Matrix account when user registers in DAARION - Return matrix_user_id in registration response --- services/auth-service/main.py | 16 +- services/auth-service/matrix_provisioning.py | 172 +++++++++++++++++++ services/auth-service/models.py | 1 + 3 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 services/auth-service/matrix_provisioning.py diff --git a/services/auth-service/main.py b/services/auth-service/main.py index 236383dc..5f5f8e78 100644 --- a/services/auth-service/main.py +++ b/services/auth-service/main.py @@ -28,6 +28,7 @@ from security import ( create_access_token, create_refresh_token, decode_access_token, decode_refresh_token ) +from matrix_provisioning import provision_matrix_user # Setup logging logging.basicConfig(level=logging.INFO) @@ -117,11 +118,24 @@ async def register(request: RegisterRequest): logger.info(f"User registered: {request.email}") + # Provision Matrix user (async, don't block registration) + matrix_info = None + try: + matrix_info = await provision_matrix_user( + user_id=str(user['id']), + email=user['email'], + display_name=user['display_name'] + ) + logger.info(f"Matrix user provisioned: {matrix_info.get('matrix_user_id')}") + except Exception as e: + logger.warning(f"Matrix provisioning failed (non-blocking): {e}") + return RegisterResponse( user_id=user['id'], email=user['email'], display_name=user['display_name'], - roles=["user"] + roles=["user"], + matrix_user_id=matrix_info.get('matrix_user_id') if matrix_info else None ) diff --git a/services/auth-service/matrix_provisioning.py b/services/auth-service/matrix_provisioning.py new file mode 100644 index 00000000..7c03f987 --- /dev/null +++ b/services/auth-service/matrix_provisioning.py @@ -0,0 +1,172 @@ +""" +Matrix User Provisioning for DAARION Auth Service + +This module handles automatic Matrix user creation when users register in DAARION. +""" + +import hashlib +import hmac +import httpx +import logging +from typing import Optional +from config import settings + +logger = logging.getLogger(__name__) + +SYNAPSE_ADMIN_URL = "http://127.0.0.1:8018" +REGISTRATION_SECRET = "daarion_reg_secret_2024" + + +async def generate_matrix_mac( + nonce: str, + username: str, + password: str, + admin: bool = False, + user_type: Optional[str] = None +) -> str: + """Generate HMAC for Synapse registration.""" + mac = hmac.new( + key=REGISTRATION_SECRET.encode('utf-8'), + digestmod=hashlib.sha1 + ) + + mac.update(nonce.encode('utf-8')) + mac.update(b"\x00") + mac.update(username.encode('utf-8')) + mac.update(b"\x00") + mac.update(password.encode('utf-8')) + mac.update(b"\x00") + mac.update(b"admin" if admin else b"notadmin") + + if user_type: + mac.update(b"\x00") + mac.update(user_type.encode('utf-8')) + + return mac.hexdigest() + + +async def provision_matrix_user( + user_id: str, + email: str, + display_name: Optional[str] = None, + is_admin: bool = False +) -> dict: + """ + Create a Matrix user for a DAARION user. + + Args: + user_id: DAARION user ID (UUID) + email: User email + display_name: Optional display name + is_admin: Whether user should be Matrix admin + + Returns: + dict with matrix_user_id and access_token + """ + # Generate Matrix username from DAARION user_id + # Use first 8 chars of UUID for readability + matrix_username = f"daarion_{user_id[:8].replace('-', '')}" + + # Generate a secure password (user won't need it - they'll use SSO) + matrix_password = hashlib.sha256( + f"{user_id}:{REGISTRATION_SECRET}".encode() + ).hexdigest()[:32] + + async with httpx.AsyncClient() as client: + try: + # Step 1: Get nonce + nonce_response = await client.get( + f"{SYNAPSE_ADMIN_URL}/_synapse/admin/v1/register" + ) + nonce_response.raise_for_status() + nonce = nonce_response.json()["nonce"] + + # Step 2: Generate MAC + mac = await generate_matrix_mac( + nonce=nonce, + username=matrix_username, + password=matrix_password, + admin=is_admin + ) + + # Step 3: Register user + register_response = await client.post( + f"{SYNAPSE_ADMIN_URL}/_synapse/admin/v1/register", + json={ + "nonce": nonce, + "username": matrix_username, + "password": matrix_password, + "admin": is_admin, + "mac": mac, + "displayname": display_name or email.split("@")[0], + } + ) + register_response.raise_for_status() + + result = register_response.json() + logger.info(f"Matrix user created: {result['user_id']}") + + return { + "matrix_user_id": result["user_id"], + "access_token": result.get("access_token"), + "device_id": result.get("device_id"), + "home_server": "app.daarion.space" + } + + except httpx.HTTPStatusError as e: + if e.response.status_code == 400: + error_detail = e.response.json().get("error", "") + if "User ID already taken" in error_detail: + logger.info(f"Matrix user already exists: {matrix_username}") + return { + "matrix_user_id": f"@{matrix_username}:daarion.space", + "access_token": None, + "device_id": None, + "home_server": "app.daarion.space", + "already_exists": True + } + logger.error(f"Failed to create Matrix user: {e.response.text}") + raise + except Exception as e: + logger.error(f"Matrix provisioning error: {e}") + raise + + +async def get_matrix_login_token(matrix_user_id: str) -> Optional[str]: + """ + Get a login token for existing Matrix user. + This allows SSO-style login without password. + """ + # TODO: Implement when needed + # This requires Synapse's login token API + pass + + +async def update_matrix_profile( + matrix_user_id: str, + display_name: Optional[str] = None, + avatar_url: Optional[str] = None, + access_token: str = None +) -> bool: + """Update Matrix user profile.""" + async with httpx.AsyncClient() as client: + try: + if display_name: + await client.put( + f"{SYNAPSE_ADMIN_URL}/_matrix/client/v3/profile/{matrix_user_id}/displayname", + json={"displayname": display_name}, + headers={"Authorization": f"Bearer {access_token}"} if access_token else {} + ) + + if avatar_url: + await client.put( + f"{SYNAPSE_ADMIN_URL}/_matrix/client/v3/profile/{matrix_user_id}/avatar_url", + json={"avatar_url": avatar_url}, + headers={"Authorization": f"Bearer {access_token}"} if access_token else {} + ) + + return True + except Exception as e: + logger.error(f"Failed to update Matrix profile: {e}") + return False + diff --git a/services/auth-service/models.py b/services/auth-service/models.py index 72d8a455..e54c9f83 100644 --- a/services/auth-service/models.py +++ b/services/auth-service/models.py @@ -47,6 +47,7 @@ class RegisterResponse(BaseModel): email: str display_name: Optional[str] = None roles: List[str] = ["user"] + matrix_user_id: Optional[str] = None class TokenResponse(BaseModel):