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
This commit is contained in:
Apple
2025-11-26 12:06:55 -08:00
parent 064ac5af64
commit 5fdb839f5a
3 changed files with 188 additions and 1 deletions

View File

@@ -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
)

View File

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

View File

@@ -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):