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:
@@ -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
|
||||
)
|
||||
|
||||
|
||||
|
||||
172
services/auth-service/matrix_provisioning.py
Normal file
172
services/auth-service/matrix_provisioning.py
Normal 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
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user