Files
microdao-daarion/services/auth-service/matrix_provisioning.py
Apple 5fdb839f5a 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
2025-11-26 12:06:55 -08:00

173 lines
5.5 KiB
Python

"""
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