Skip to main content

m4a_product_example/
app.rs

1//! HTTP surface of the example product server. `/product/v1/*` is the
2//! product's own API; everything else goes through the signing link.
3
4use std::sync::Arc;
5
6use argon2::password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
7use argon2::Argon2;
8use axum::extract::{Path, Request, State};
9use axum::http::{HeaderMap, StatusCode};
10use axum::response::{IntoResponse, Response};
11use axum::routing::{any, get, post, put};
12use axum::{Json, Router};
13use m4a_product_kit::door::{DoorError, LoginDoor};
14use m4a_product_kit::model::UserStore;
15use crate::store::Role;
16use m4a_product_kit::service::{AuthUser, ServiceError, UserService};
17use m4a_product_kit::{EdgeLink, EventPublisher};
18use serde_json::{json, Value};
19use sha2::{Digest, Sha256};
20
21use crate::store::SqliteStore;
22
23pub struct ProductApp {
24    pub users: UserService<SqliteStore>,
25    pub link: EdgeLink,
26    pub events: EventPublisher,
27    /// Bearer for `/product/v1/admin/*`; empty disables the admin API.
28    pub admin_token: String,
29    pub doors: Vec<Arc<dyn LoginDoor>>,
30    /// Serve unauthenticated reads of public rooms through the marked read-only mode
31    /// (`M4A_PRODUCT_ANON_READ=on`; the core must have `M4A_ANON_READ=on` too).
32    pub anon_read: bool,
33    /// One-time login tokens for `login/get_token`.
34    pub tokens: m4a_product_kit::LoginTokens,
35    /// Lifetime of an access token handed out with a refresh token (0 turns refresh tokens off).
36    pub access_ttl_ms: i64,
37    /// Open key-login challenges (replay protection) and this product's audience name.
38    pub challenges: m4a_product_kit::ChallengeBook,
39}
40
41/// A refresh token lives this long.
42const REFRESH_TTL_MS: i64 = 30 * 24 * 3600 * 1000;
43/// A login token (`login/get_token`) lives this long.
44const LOGIN_TOKEN_TTL_MS: i64 = 120_000;
45
46type App = Arc<ProductApp>;
47
48fn now_ms() -> i64 {
49    std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_millis() as i64
50}
51
52struct ApiError(StatusCode, Value);
53
54impl IntoResponse for ApiError {
55    fn into_response(self) -> Response {
56        (self.0, Json(self.1)).into_response()
57    }
58}
59
60fn err(status: StatusCode, code: &str, msg: &str) -> ApiError {
61    ApiError(status, json!({ "errcode": code, "error": msg }))
62}
63
64impl From<ServiceError> for ApiError {
65    fn from(e: ServiceError) -> Self {
66        match e {
67            ServiceError::InvalidNick(m) => err(StatusCode::BAD_REQUEST, "M4A_INVALID_NICK", &m),
68            ServiceError::NickTaken => err(StatusCode::CONFLICT, "M4A_NICK_TAKEN", "nick is taken"),
69            ServiceError::Cooldown { retry_after_ms } => ApiError(StatusCode::TOO_MANY_REQUESTS, json!({ "errcode": "M4A_NICK_COOLDOWN", "error": "nick was changed recently", "retry_after_ms": retry_after_ms })),
70            ServiceError::NotFound => err(StatusCode::NOT_FOUND, "M_NOT_FOUND", "not found"),
71            ServiceError::Unauthorized => err(StatusCode::UNAUTHORIZED, "M_UNKNOWN_TOKEN", "unknown or revoked token"),
72            ServiceError::Internal(m) => {
73                tracing::error!("internal: {m}");
74                err(StatusCode::INTERNAL_SERVER_ERROR, "M_UNKNOWN", "internal error")
75            }
76        }
77    }
78}
79
80fn bearer(h: &HeaderMap) -> Option<String> {
81    h.get("authorization")?.to_str().ok()?.strip_prefix("Bearer ").map(|t| t.trim().to_string())
82}
83
84/// Authenticates on the blocking pool: database work never runs on the async request
85/// thread, and no store lock is held while the request waits on the edge.
86async fn auth_blocking(app: &App, h: &HeaderMap) -> Result<Option<AuthUser>, ApiError> {
87    let Some(t) = bearer(h) else { return Ok(None) };
88    let app = Arc::clone(app);
89    tokio::task::spawn_blocking(move || app.users.authenticate(&t))
90        .await
91        .map_err(|_| err(StatusCode::INTERNAL_SERVER_ERROR, "M_UNKNOWN", "task failed"))??
92        .map(Some)
93        .ok_or_else(|| ServiceError::Unauthorized.into())
94}
95
96/// Runs store-touching work on the blocking pool (the store's writer must never be waited on
97/// from the async runtime).
98async fn blk<T: Send + 'static>(app: &App, f: impl FnOnce(&ProductApp) -> T + Send + 'static) -> Result<T, ApiError> {
99    let app = Arc::clone(app);
100    tokio::task::spawn_blocking(move || f(&app)).await.map_err(|_| err(StatusCode::INTERNAL_SERVER_ERROR, "M_UNKNOWN", "task failed"))
101}
102
103fn hash_secret(secret: &str) -> Result<String, ApiError> {
104    Argon2::default().hash_password(secret.as_bytes(), &SaltString::generate(&mut OsRng)).map(|h| h.to_string()).map_err(|_| err(StatusCode::INTERNAL_SERVER_ERROR, "M_UNKNOWN", "hash failed"))
105}
106
107fn check_secret(hash: &str, secret: &str) -> bool {
108    PasswordHash::new(hash).map(|h| Argon2::default().verify_password(secret.as_bytes(), &h).is_ok()).unwrap_or(false)
109}
110
111fn session_json(nick: &str, token: &str) -> Json<Value> {
112    Json(json!({ "nick": nick, "token": token }))
113}
114
115pub fn router(app: App) -> Router {
116    Router::new()
117        .route("/product/v1/register", post(register))
118        .route("/product/v1/login", post(login))
119        .route("/product/v1/enroll", post(key_enroll))
120        .route("/product/v1/login/key/challenge", post(key_challenge))
121        .route("/product/v1/login/key", post(key_login))
122        .route("/product/v1/login/key/token", post(key_login_token))
123        .route("/product/v1/admin/invite", post(admin_invite))
124        .route("/client/v3/login", get(matrix_login_flows).post(matrix_login))
125        .route("/_matrix/client/v3/login", get(matrix_login_flows).post(matrix_login))
126        .route("/client/v3/refresh", post(matrix_refresh))
127        .route("/_matrix/client/v3/refresh", post(matrix_refresh))
128        .route("/client/v1/login/get_token", post(get_login_token))
129        .route("/_matrix/client/v1/login/get_token", post(get_login_token))
130        .route("/client/v3/capabilities", get(capabilities))
131        .route("/_matrix/client/v3/capabilities", get(capabilities))
132        .route("/client/v3/account/password", post(account_password))
133        .route("/_matrix/client/v3/account/password", post(account_password))
134        .route("/client/v3/account/deactivate", post(account_deactivate))
135        .route("/_matrix/client/v3/account/deactivate", post(account_deactivate))
136        .route("/client/v3/account/3pid", get(threepids))
137        .route("/_matrix/client/v3/account/3pid", get(threepids))
138        .route("/client/v3/account/3pid/add", post(threepid_denied))
139        .route("/_matrix/client/v3/account/3pid/add", post(threepid_denied))
140        .route("/client/v3/account/3pid/bind", post(threepid_denied))
141        .route("/_matrix/client/v3/account/3pid/bind", post(threepid_denied))
142        .route("/client/v3/account/3pid/email/requestToken", post(threepid_denied))
143        .route("/_matrix/client/v3/account/3pid/email/requestToken", post(threepid_denied))
144        .route("/client/v3/account/3pid/msisdn/requestToken", post(threepid_denied))
145        .route("/_matrix/client/v3/account/3pid/msisdn/requestToken", post(threepid_denied))
146        .route("/client/v3/account/password/email/requestToken", post(threepid_denied))
147        .route("/_matrix/client/v3/account/password/email/requestToken", post(threepid_denied))
148        .route("/client/v3/account/password/msisdn/requestToken", post(threepid_denied))
149        .route("/_matrix/client/v3/account/password/msisdn/requestToken", post(threepid_denied))
150        .route("/client/v3/register/email/requestToken", post(threepid_denied))
151        .route("/_matrix/client/v3/register/email/requestToken", post(threepid_denied))
152        .route("/client/v3/register/msisdn/requestToken", post(threepid_denied))
153        .route("/_matrix/client/v3/register/msisdn/requestToken", post(threepid_denied))
154        .route("/client/v3/account/3pid/delete", post(threepid_unbind))
155        .route("/_matrix/client/v3/account/3pid/delete", post(threepid_unbind))
156        .route("/client/v3/account/3pid/unbind", post(threepid_unbind))
157        .route("/_matrix/client/v3/account/3pid/unbind", post(threepid_unbind))
158        .route("/client/v3/register", post(matrix_register))
159        .route("/_matrix/client/v3/register", post(matrix_register))
160        .route("/client/v3/register/available", get(matrix_available))
161        .route("/_matrix/client/v3/register/available", get(matrix_available))
162        .route("/product/v1/doors", get(doors))
163        .route("/product/v1/login/door/{id}", post(door_login))
164        .route("/product/v1/logout", post(logout))
165        .route("/product/v1/me", get(me))
166        .route("/product/v1/profile/{nick}", get(profile))
167        .route("/product/v1/nick", put(set_nick))
168        .route("/client/v3/push", get(push))
169        .route("/_matrix/client/v3/push", get(push))
170        .route("/product/v1/admin/revoke", post(admin_revoke))
171        .route("/product/v1/admin/delete", post(admin_delete))
172        .route("/product/v1/admin/tier", post(admin_tier))
173        .route("/product/v1/admin/role", post(admin_role))
174        .fallback(any(proxy))
175        .layer(axum::middleware::from_fn(cors))
176        .with_state(app)
177}
178
179/// Browser clients call from another origin: answer preflights and allow it.
180async fn cors(req: Request, next: axum::middleware::Next) -> Response {
181    use axum::http::{header, HeaderValue, Method};
182    let preflight = req.method() == Method::OPTIONS;
183    let mut resp = if preflight { Response::builder().status(StatusCode::OK).body(axum::body::Body::empty()).unwrap() } else { next.run(req).await };
184    let h = resp.headers_mut();
185    h.insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, HeaderValue::from_static("*"));
186    h.insert(header::ACCESS_CONTROL_ALLOW_METHODS, HeaderValue::from_static("GET, POST, PUT, DELETE, OPTIONS"));
187    h.insert(header::ACCESS_CONTROL_ALLOW_HEADERS, HeaderValue::from_static("X-Requested-With, Content-Type, Authorization, Date"));
188    resp
189}
190
191/// The Matrix client login flow list: a nick and a password, or a one-time login token.
192async fn matrix_login_flows() -> Json<Value> {
193    Json(json!({ "flows": [{ "type": "m.login.password" }, { "type": "m.login.token", "get_login_token": true }, { "type": m4a_seam::keyproof::LOGIN_TYPE }] }))
194}
195
196/// Matrix login on top of the product login: `m.login.password` (the nick is the user) or
197/// `m.login.token` (a token from `login/get_token`). The product session token is the access
198/// token; the user and device ids come from the messenger's own answer. A client that asks for
199/// `refresh_token: true` gets an expiring access token and a refresh token.
200async fn matrix_login(State(app): State<App>, Json(body): Json<Value>) -> Result<Json<Value>, ApiError> {
201    let bad = || err(StatusCode::FORBIDDEN, "M_FORBIDDEN", "invalid credentials");
202    if body.get("type").and_then(Value::as_str) == Some(m4a_seam::keyproof::LOGIN_TYPE) {
203        return matrix_key_login(&app, &body).await;
204    }
205    let user = if body.get("type").and_then(Value::as_str) == Some("m.login.token") {
206        let t = body.get("token").and_then(Value::as_str).unwrap_or("");
207        let nick = app.tokens.redeem(t, now_ms()).ok_or_else(|| err(StatusCode::FORBIDDEN, "M_FORBIDDEN", "invalid or expired login token"))?;
208        blk(&app, move |a| -> Result<_, ServiceError> { Ok(a.users.store.user_by_nick(&nick)?) }).await??.ok_or_else(bad)?
209    } else {
210        let user = body.pointer("/identifier/user").or_else(|| body.get("user")).and_then(Value::as_str).unwrap_or("");
211        let user = user.strip_prefix('@').unwrap_or(user);
212        let nick = user.split(':').next().unwrap_or("").to_string();
213        let pw = body.get("password").and_then(Value::as_str).unwrap_or("").to_string();
214        verify_password(&app, nick, pw).await?
215    };
216    let want_refresh = body.get("refresh_token").and_then(Value::as_bool) == Some(true) && app.access_ttl_ms > 0;
217    let ttl = app.access_ttl_ms;
218    let (session, refresh) = blk(&app, move |a| -> Result<_, ServiceError> {
219        if want_refresh && a.users.store.supports_refresh() {
220            let (s, r) = a.users.issue_refreshable_session(&user, now_ms(), ttl, REFRESH_TTL_MS)?;
221            Ok((s, Some(r)))
222        } else {
223            Ok((a.users.issue_session(&user, now_ms())?, None))
224        }
225    })
226    .await??;
227    let Json(mut out) = matrix_session(&app, session.token).await?;
228    if let Some(r) = refresh {
229        out["refresh_token"] = json!(r);
230        out["expires_in_ms"] = json!(ttl);
231    }
232    Ok(Json(out))
233}
234
235/// `POST /refresh`: a new access token and a new refresh token for the same session.
236async fn matrix_refresh(State(app): State<App>, Json(body): Json<Value>) -> Result<Json<Value>, ApiError> {
237    let t = body.get("refresh_token").and_then(Value::as_str).unwrap_or("").to_string();
238    let ttl = app.access_ttl_ms;
239    let r = if ttl > 0 { blk(&app, move |a| a.users.refresh(&t, now_ms(), ttl, REFRESH_TTL_MS)).await?? } else { None };
240    let (s, refresh) = r.ok_or_else(|| ApiError(StatusCode::UNAUTHORIZED, json!({ "errcode": "M_UNKNOWN_TOKEN", "error": "unknown or expired refresh token", "soft_logout": false })))?;
241    Ok(Json(json!({ "access_token": s.token, "refresh_token": refresh, "expires_in_ms": ttl })))
242}
243
244/// Matrix user-interactive auth with the account password (re-authentication for sensitive
245/// calls). `Ok` when the request carries a valid `m.login.password` auth; otherwise the 401 that
246/// tells the client what to send.
247async fn uia_password(app: &App, who: &AuthUser, body: &Value) -> Result<(), Response> {
248    let pw = body.pointer("/auth/password").and_then(Value::as_str);
249    if body.pointer("/auth/type").and_then(Value::as_str) != Some("m.login.password") || pw.is_none() {
250        let v = json!({ "flows": [{ "stages": ["m.login.password"] }], "params": {}, "session": "reauth" });
251        return Err((StatusCode::UNAUTHORIZED, Json(v)).into_response());
252    }
253    if let Some(u) = body.pointer("/auth/identifier/user").and_then(Value::as_str) {
254        if u.trim_start_matches('@').split(':').next().map(str::to_lowercase) != Some(who.nick.to_lowercase()) {
255            return Err(err(StatusCode::FORBIDDEN, "M_FORBIDDEN", "that is not your account").into_response());
256        }
257    }
258    verify_password(app, who.nick.clone(), pw.unwrap_or("").to_string()).await.map(|_| ()).map_err(|_| err(StatusCode::UNAUTHORIZED, "M_FORBIDDEN", "wrong password").into_response())
259}
260
261/// `POST /login/get_token` (spec v1.7): a signed-in client, re-authenticating with its password,
262/// gets a one-time token that another client can log in with.
263async fn get_login_token(State(app): State<App>, headers: HeaderMap, Json(body): Json<Value>) -> Result<Response, ApiError> {
264    let who = need(&app, &headers).await?;
265    if let Err(r) = uia_password(&app, &who, &body).await {
266        return Ok(r);
267    }
268    let t = app.tokens.issue(&who.nick, now_ms(), LOGIN_TOKEN_TTL_MS);
269    Ok(Json(json!({ "login_token": t, "expires_in_ms": LOGIN_TOKEN_TTL_MS })).into_response())
270}
271
272/// The messenger's capabilities, plus what the product itself provides.
273async fn capabilities(State(app): State<App>, req: Request) -> Response {
274    let who = match auth_blocking(&app, req.headers()).await {
275        Ok(Some(w)) => w,
276        Ok(None) => return err(StatusCode::UNAUTHORIZED, "M_MISSING_TOKEN", "missing access token").into_response(),
277        Err(e) => return e.into_response(),
278    };
279    let resp = app.link.forward(Some(&who), req).await;
280    let (parts, body) = resp.into_parts();
281    let Ok(bytes) = axum::body::to_bytes(body, 1 << 20).await else { return err(StatusCode::BAD_GATEWAY, "M_UNKNOWN", "messenger unavailable").into_response() };
282    let Ok(mut v) = serde_json::from_slice::<Value>(&bytes) else { return Response::from_parts(parts, axum::body::Body::from(bytes)) };
283    if parts.status.is_success() {
284        v["capabilities"]["m.get_login_token"] = json!({ "enabled": true });
285        v["capabilities"]["m.change_password"] = json!({ "enabled": true });
286        v["capabilities"]["m.3pid_changes"] = json!({ "enabled": false });
287    }
288    (parts.status, Json(v)).into_response()
289}
290
291/// `POST /account/password`: re-authenticate, set the new password, and (default) sign every
292/// other session out.
293async fn account_password(State(app): State<App>, headers: HeaderMap, Json(body): Json<Value>) -> Result<Response, ApiError> {
294    let who = need(&app, &headers).await?;
295    if let Err(r) = uia_password(&app, &who, &body).await {
296        return Ok(r);
297    }
298    let new = body.get("new_password").and_then(Value::as_str).unwrap_or("").to_string();
299    if new.len() < 8 {
300        return Err(err(StatusCode::BAD_REQUEST, "M_WEAK_PASSWORD", "password must have at least 8 characters"));
301    }
302    let hash = tokio::task::spawn_blocking(move || hash_secret(&new)).await.map_err(|_| err(StatusCode::INTERNAL_SERVER_ERROR, "M_UNKNOWN", "task failed"))??;
303    let logout_others = body.get("logout_devices").and_then(Value::as_bool).unwrap_or(true);
304    let (nick, mine) = (who.nick.clone(), who.cred_ref.clone());
305    let evs = blk(&app, move |a| -> Result<_, ServiceError> {
306        let u = a.users.store.user_by_nick(&nick)?.ok_or(ServiceError::Unauthorized)?;
307        a.users.store.set_secret_hash(u.id, &hash)?;
308        let mut evs = Vec::new();
309        if logout_others {
310            for c in a.users.store.credentials_of(u.id)?.into_iter().filter(|c| *c != mine) {
311                evs.extend(a.users.revoke(&c)?);
312            }
313        }
314        Ok(evs)
315    })
316    .await??;
317    app.events.publish(evs);
318    Ok(Json(json!({})).into_response())
319}
320
321/// `POST /account/deactivate`: re-authenticate, then delete the account (every session is
322/// revoked and the messenger retires the identity).
323async fn account_deactivate(State(app): State<App>, headers: HeaderMap, Json(body): Json<Value>) -> Result<Response, ApiError> {
324    let who = need(&app, &headers).await?;
325    if let Err(r) = uia_password(&app, &who, &body).await {
326        return Ok(r);
327    }
328    let nick = who.nick.clone();
329    let evs = blk(&app, move |a| a.users.delete(&nick)).await??;
330    app.events.publish(evs);
331    Ok(Json(json!({ "id_server_unbind_result": "no-support" })).into_response())
332}
333
334/// This product has no third-party identifiers: the list is empty, adding is refused.
335async fn threepids(State(app): State<App>, headers: HeaderMap) -> Result<Json<Value>, ApiError> {
336    need(&app, &headers).await?;
337    Ok(Json(json!({ "threepids": [] })))
338}
339
340async fn threepid_denied() -> ApiError {
341    err(StatusCode::FORBIDDEN, "M_THREEPID_DENIED", "this server does not use third-party identifiers")
342}
343
344async fn threepid_unbind() -> Json<Value> {
345    Json(json!({ "id_server_unbind_result": "no-support" }))
346}
347
348/// The password check shared by login and re-authentication.
349async fn verify_password(app: &App, nick: String, pw: String) -> Result<m4a_product_kit::User, ApiError> {
350    let bad = || err(StatusCode::FORBIDDEN, "M_FORBIDDEN", "invalid credentials");
351    let (u, hash) = blk(app, move |a| -> Result<_, ServiceError> {
352        let u = a.users.store.user_by_nick(&nick)?;
353        let hash = match &u {
354            Some(u) => a.users.store.secret_hash(u.id)?,
355            None => None,
356        };
357        Ok((u, hash))
358    })
359    .await??;
360    let (u, hash) = match (u, hash) {
361        (Some(u), Some(h)) => (u, h),
362        _ => return Err(bad()),
363    };
364    if !tokio::task::spawn_blocking(move || check_secret(&hash, &pw)).await.unwrap_or(false) {
365        return Err(bad());
366    }
367    Ok(u)
368}
369
370/// The Matrix login answer for a product session token: user and device ids come from the messenger.
371async fn matrix_session(app: &App, token: String) -> Result<Json<Value>, ApiError> {
372    let t2 = token.clone();
373    let who = blk(app, move |a| a.users.authenticate(&t2)).await??.ok_or_else(|| err(StatusCode::FORBIDDEN, "M_FORBIDDEN", "invalid credentials"))?;
374    let req = Request::builder().method("GET").uri("/client/v3/account/whoami").body(axum::body::Body::empty()).map_err(|_| err(StatusCode::INTERNAL_SERVER_ERROR, "M_UNKNOWN", "request"))?;
375    let resp = app.link.forward(Some(&who), req).await;
376    let bytes = axum::body::to_bytes(resp.into_body(), 1 << 16).await.map_err(|_| err(StatusCode::BAD_GATEWAY, "M_UNKNOWN", "messenger unavailable"))?;
377    let w: Value = serde_json::from_slice(&bytes).map_err(|_| err(StatusCode::BAD_GATEWAY, "M_UNKNOWN", "messenger unavailable"))?;
378    let user_id = w["user_id"].as_str().ok_or_else(|| err(StatusCode::BAD_GATEWAY, "M_UNKNOWN", "messenger refused the session"))?.to_string();
379    let home = user_id.split_once(':').map(|(_, d)| d.to_string()).unwrap_or_default();
380    Ok(Json(json!({ "user_id": user_id, "access_token": token, "device_id": w["device_id"], "home_server": home })))
381}
382
383/// Matrix `register` on the product: the username becomes the nick, one dummy auth stage.
384async fn matrix_register(State(app): State<App>, Json(body): Json<Value>) -> Result<Response, ApiError> {
385    if body.pointer("/auth/type").and_then(Value::as_str) != Some("m.login.dummy") {
386        let v = json!({ "flows": [{ "stages": ["m.login.dummy"] }], "params": {}, "session": "register" });
387        return Ok((StatusCode::UNAUTHORIZED, Json(v)).into_response());
388    }
389    let username = body.get("username").and_then(Value::as_str).unwrap_or("").to_string();
390    if username.is_empty() {
391        return Err(err(StatusCode::BAD_REQUEST, "M_INVALID_USERNAME", "a username is required"));
392    }
393    let taken = {
394        let n = username.clone();
395        blk(&app, move |a| -> Result<_, ServiceError> { Ok(a.users.store.user_by_nick(&n)?) }).await??.is_some()
396    };
397    if taken {
398        return Err(err(StatusCode::BAD_REQUEST, "M_USER_IN_USE", "that username is taken"));
399    }
400    let Json(sess) = register(State(app.clone()), Json(json!({ "password": body.get("password").cloned().unwrap_or_default() }))).await?;
401    let token = sess["token"].as_str().unwrap_or_default().to_string();
402    let (t2, nick) = (token.clone(), username.clone());
403    let who = blk(&app, move |a| a.users.authenticate(&t2)).await??.ok_or_else(|| err(StatusCode::INTERNAL_SERVER_ERROR, "M_UNKNOWN", "session"))?;
404    let (u, evs) = blk(&app, move |a| -> Result<_, ServiceError> {
405        let u = a.users.store.user_by_nick(&who.nick)?.ok_or(ServiceError::Unauthorized)?;
406        a.users.set_nick(&u, &nick, now_ms())
407    })
408    .await??;
409    let _ = u;
410    app.events.publish(evs);
411    if body.get("inhibit_login").and_then(Value::as_bool) == Some(true) {
412        return Ok(Json(json!({ "user_id": username })).into_response());
413    }
414    Ok(matrix_session(&app, token).await?.into_response())
415}
416
417async fn matrix_available(State(app): State<App>, axum::extract::Query(q): axum::extract::Query<std::collections::HashMap<String, String>>) -> Result<Json<Value>, ApiError> {
418    let n = q.get("username").cloned().unwrap_or_default();
419    if n.is_empty() {
420        return Err(err(StatusCode::BAD_REQUEST, "M_INVALID_USERNAME", "a username is required"));
421    }
422    if blk(&app, move |a| -> Result<_, ServiceError> { Ok(a.users.store.user_by_nick(&n)?) }).await??.is_some() {
423        return Err(err(StatusCode::BAD_REQUEST, "M_USER_IN_USE", "that username is taken"));
424    }
425    Ok(Json(json!({ "available": true })))
426}
427
428async fn register(State(app): State<App>, Json(body): Json<Value>) -> Result<Json<Value>, ApiError> {
429    let pw = body.get("password").and_then(Value::as_str).unwrap_or("");
430    if pw.len() < 8 {
431        return Err(err(StatusCode::BAD_REQUEST, "M_WEAK_PASSWORD", "password must have at least 8 characters"));
432    }
433    let hash = tokio::task::spawn_blocking({
434        let pw = pw.to_string();
435        move || hash_secret(&pw)
436    })
437    .await
438    .map_err(|_| err(StatusCode::INTERNAL_SERVER_ERROR, "M_UNKNOWN", "task failed"))??;
439    let (u, s) = blk(&app, move |a| -> Result<_, ServiceError> {
440        let u = a.users.create_user(Some(&hash), now_ms())?;
441        let s = a.users.issue_session(&u, now_ms())?;
442        Ok((u, s))
443    })
444    .await??;
445    Ok(session_json(&u.nick, &s.token))
446}
447
448async fn login(State(app): State<App>, Json(body): Json<Value>) -> Result<Json<Value>, ApiError> {
449    let nick = body.get("nick").and_then(Value::as_str).unwrap_or("").to_string();
450    let pw = body.get("password").and_then(Value::as_str).unwrap_or("").to_string();
451    let u = verify_password(&app, nick, pw).await?;
452    let (u, s) = blk(&app, move |a| -> Result<_, ServiceError> {
453        let s = a.users.issue_session(&u, now_ms())?;
454        Ok((u, s))
455    })
456    .await??;
457    Ok(session_json(&u.nick, &s.token))
458}
459
460async fn doors(State(app): State<App>) -> Json<Value> {
461    Json(json!({ "doors": app.doors.iter().map(|d| d.id()).collect::<Vec<_>>() }))
462}
463
464async fn door_login(State(app): State<App>, Path(id): Path<String>, Json(proof): Json<Value>) -> Result<Json<Value>, ApiError> {
465    let door = app.doors.iter().find(|d| d.id() == id).ok_or_else(|| err(StatusCode::NOT_FOUND, "M_NOT_FOUND", "no such door"))?;
466    let (source, subject) = door.verify(&proof).await.map_err(|e| match e {
467        DoorError::BadProof(m) => err(StatusCode::BAD_REQUEST, "M_BAD_JSON", &m),
468        DoorError::Refused(m) => err(StatusCode::FORBIDDEN, "M_FORBIDDEN", &m),
469        DoorError::Unverified(m) => err(StatusCode::UNAUTHORIZED, "M_UNAUTHORIZED", &m),
470    })?;
471    let (u, s) = blk(&app, move |a| -> Result<_, ServiceError> {
472        let u = a.users.user_for_door(&source, &subject, now_ms())?;
473        let s = a.users.issue_session(&u, now_ms())?;
474        Ok((u, s))
475    })
476    .await??;
477    Ok(session_json(&u.nick, &s.token))
478}
479
480async fn need(app: &App, h: &HeaderMap) -> Result<AuthUser, ApiError> {
481    auth_blocking(app, h).await?.ok_or_else(|| ServiceError::Unauthorized.into())
482}
483
484async fn logout(State(app): State<App>, headers: HeaderMap) -> Result<Json<Value>, ApiError> {
485    let who = need(&app, &headers).await?;
486    let cred = who.cred_ref.clone();
487    let evs = blk(&app, move |a| a.users.revoke(&cred)).await??;
488    app.events.publish(evs);
489    Ok(Json(json!({})))
490}
491
492/// `GET /product/v1/profile/{nick}`: public account profile — kind (human|agent) and server role. No
493/// secrets, nothing a member could not already see as a Matrix profile. 404 for an unknown nick.
494async fn profile(State(app): State<App>, Path(nick): Path<String>) -> Result<Json<Value>, ApiError> {
495    let (n, kind, role) = blk(&app, move |a| -> Result<(String, String, Role), ServiceError> {
496        let u = a.users.store.user_by_nick(&nick)?.ok_or(ServiceError::NotFound)?;
497        let (k, r) = a.users.store.roles(u.id)?;
498        Ok((u.nick, k, r))
499    })
500    .await??;
501    Ok(Json(json!({ "nick": n, "kind": kind, "role": role.as_str() })))
502}
503
504async fn me(State(app): State<App>, headers: HeaderMap) -> Result<Json<Value>, ApiError> {
505    let who = need(&app, &headers).await?;
506    let nick = who.nick.clone();
507    let (kind, role) = blk(&app, move |a| -> Result<(String, Role), ServiceError> {
508        let u = a.users.store.user_by_nick(&nick)?.ok_or(ServiceError::Unauthorized)?;
509        Ok(a.users.store.roles(u.id)?)
510    })
511    .await??;
512    Ok(Json(json!({ "nick": who.nick, "flag": who.flag, "kind": kind, "role": role.as_str() })))
513}
514
515async fn set_nick(State(app): State<App>, headers: HeaderMap, Json(body): Json<Value>) -> Result<Json<Value>, ApiError> {
516    let who = need(&app, &headers).await?;
517    let new = body.get("nick").and_then(Value::as_str).unwrap_or("");
518    let (nick, new) = (who.nick.clone(), new.to_string());
519    let (u, evs) = blk(&app, move |a| -> Result<_, ServiceError> {
520        let u = a.users.store.user_by_nick(&nick)?.ok_or(ServiceError::Unauthorized)?;
521        a.users.set_nick(&u, &new, now_ms())
522    })
523    .await??;
524    app.events.publish(evs);
525    Ok(Json(json!({ "nick": u.nick })))
526}
527
528fn is_admin_token(app: &App, h: &HeaderMap) -> bool {
529    !app.admin_token.is_empty() && bearer(h).map(|t| Sha256::digest(t.as_bytes()) == Sha256::digest(app.admin_token.as_bytes())).unwrap_or(false)
530}
531
532/// The admin token, or a signed-in account whose server role is at least `min`. Returns the acting
533/// account's role (`Admin` for the token).
534async fn staff(app: &App, h: &HeaderMap, min: Role) -> Result<Role, ApiError> {
535    if is_admin_token(app, h) {
536        return Ok(Role::Admin);
537    }
538    let who = need(app, h).await.map_err(|_| err(StatusCode::FORBIDDEN, "M_FORBIDDEN", "admin token required"))?;
539    let nick = who.nick.clone();
540    let role = blk(app, move |a| -> Result<Role, ServiceError> {
541        let u = a.users.store.user_by_nick(&nick)?.ok_or(ServiceError::Unauthorized)?;
542        Ok(a.users.store.roles(u.id)?.1)
543    })
544    .await??;
545    if role >= min {
546        Ok(role)
547    } else {
548        Err(err(StatusCode::FORBIDDEN, "M_FORBIDDEN", "not allowed for this account"))
549    }
550}
551
552/// `POST /product/v1/admin/role {nick, kind?: human|agent, role?: admin|moderator|user}`. Admin token only:
553/// an admin account can never grant or revoke roles, so the token stays the root of trust.
554async fn admin_role(State(app): State<App>, headers: HeaderMap, Json(b): Json<Value>) -> Result<Json<Value>, ApiError> {
555    admin(&app, &headers)?;
556    let nick = b.get("nick").and_then(Value::as_str).unwrap_or("").to_string();
557    let kind = b.get("kind").and_then(Value::as_str).map(str::to_string);
558    if kind.as_deref().is_some_and(|k| k != "human" && k != "agent") {
559        return Err(err(StatusCode::BAD_REQUEST, "M_BAD_JSON", "kind is human or agent"));
560    }
561    let role = match b.get("role").and_then(Value::as_str) {
562        Some(r) => Some(Role::parse(r).ok_or_else(|| err(StatusCode::BAD_REQUEST, "M_BAD_JSON", "role is admin, moderator or user"))?),
563        None => None,
564    };
565    let (k, r) = blk(&app, move |a| -> Result<(String, Role), ServiceError> {
566        let u = a.users.store.user_by_nick(&nick)?.ok_or(ServiceError::NotFound)?;
567        Ok(a.users.store.set_roles(u.id, kind.as_deref(), role)?)
568    })
569    .await??;
570    Ok(Json(json!({ "kind": k, "role": r.as_str() })))
571}
572
573fn admin(app: &App, h: &HeaderMap) -> Result<(), ApiError> {
574    let ok = !app.admin_token.is_empty() && bearer(h).map(|t| Sha256::digest(t.as_bytes()) == Sha256::digest(app.admin_token.as_bytes())).unwrap_or(false);
575    if ok {
576        Ok(())
577    } else {
578        Err(err(StatusCode::FORBIDDEN, "M_FORBIDDEN", "admin token required"))
579    }
580}
581
582async fn admin_revoke(State(app): State<App>, headers: HeaderMap, Json(b): Json<Value>) -> Result<Json<Value>, ApiError> {
583    // Moderators may end sessions/keys (e.g. a misbehaving agent); admins and the token too.
584    staff(&app, &headers, Role::Moderator).await?;
585    let cred = b.get("cred_ref").and_then(Value::as_str).unwrap_or("").to_string();
586    let evs = blk(&app, move |a| a.users.revoke(&cred)).await??;
587    let n = evs.len();
588    app.events.publish(evs);
589    Ok(Json(json!({ "revoked": n })))
590}
591
592async fn admin_delete(State(app): State<App>, headers: HeaderMap, Json(b): Json<Value>) -> Result<Json<Value>, ApiError> {
593    admin(&app, &headers)?;
594    let nick = b.get("nick").and_then(Value::as_str).unwrap_or("").to_string();
595    let evs = blk(&app, move |a| a.users.delete(&nick)).await??;
596    app.events.publish(evs);
597    Ok(Json(json!({})))
598}
599
600async fn admin_tier(State(app): State<App>, headers: HeaderMap, Json(b): Json<Value>) -> Result<Json<Value>, ApiError> {
601    staff(&app, &headers, Role::Admin).await?;
602    let tier = b.get("tier").and_then(Value::as_str).unwrap_or("");
603    if !app.users.tiers.knows(tier) {
604        return Err(err(StatusCode::BAD_REQUEST, "M_BAD_JSON", "unknown tier"));
605    }
606    let (nick, tier) = (b.get("nick").and_then(Value::as_str).unwrap_or("").to_string(), tier.to_string());
607    blk(&app, move |a| -> Result<(), ServiceError> {
608        let u = a.users.store.user_by_nick(&nick)?.ok_or(ServiceError::NotFound)?;
609        a.users.store.set_tier(u.id, &tier)?;
610        Ok(())
611    })
612    .await??;
613    Ok(Json(json!({})))
614}
615
616/// Everything else: authenticate, sign, forward to the edge. A bad token is
617/// refused here; no token forwards unasserted (the messenger decides what an
618/// unasserted caller may do).
619async fn proxy(State(app): State<App>, req: Request) -> Response {
620    let who = match auth_blocking(&app, req.headers()).await {
621        Ok(w) => w,
622        Err(e) => return e.into_response(),
623    };
624    if who.is_none() && app.anon_read && m4a_seam::anon_read_path_ok(req.method().as_str(), req.uri().path_and_query().map(|p| p.as_str()).unwrap_or("")) {
625        return app.link.forward_anon(req).await;
626    }
627    app.link.forward(who.as_ref(), req).await
628}
629
630/// Push socket: the handshake is authenticated here (Bearer header or `access_token`
631/// query), signed, and relayed to the messenger; anonymous sockets are refused.
632async fn push(State(app): State<App>, headers: HeaderMap, axum::extract::Query(q): axum::extract::Query<std::collections::HashMap<String, String>>, ws: axum::extract::WebSocketUpgrade) -> Response {
633    let token = bearer(&headers).or_else(|| q.get("access_token").cloned());
634    let Some(token) = token else { return m4a_product_kit::push_relay::unauthorized() };
635    let who = {
636        let app = Arc::clone(&app);
637        match tokio::task::spawn_blocking(move || app.users.authenticate(&token)).await {
638            Ok(Ok(Some(w))) => w,
639            _ => return m4a_product_kit::push_relay::unauthorized(),
640        }
641    };
642    app.link.relay_push(who, ws)
643}
644
645// ---- Login by proof of possession of a key (no password). See `m4a_seam::keyproof`.
646
647fn key_ttl(app: &App) -> i64 {
648    if app.access_ttl_ms > 0 { app.access_ttl_ms } else { 24 * 3_600_000 }
649}
650
651fn unauthorized_key() -> ApiError {
652    err(StatusCode::UNAUTHORIZED, "M_FORBIDDEN", "key proof refused")
653}
654
655/// `POST /product/v1/admin/invite {nick?, tier?, existing?}`: the operator approves one identity. With
656/// `existing: true` and the nick of an account that exists, the invite adds a key to that account. The nick is
657/// assigned now. The invite code is returned once and is given to the client, never to the agent.
658async fn admin_invite(State(app): State<App>, headers: HeaderMap, Json(b): Json<Value>) -> Result<Json<Value>, ApiError> {
659    staff(&app, &headers, Role::Admin).await?;
660    let nick = b.get("nick").and_then(Value::as_str).map(str::to_string);
661    let tier = b.get("tier").and_then(Value::as_str).map(str::to_string);
662    if b.get("existing").and_then(Value::as_bool) == Some(true) {
663        // A new key for an account that exists (lost key, second device); the nick is required and must exist.
664        let Some(n) = nick else { return Err(err(StatusCode::BAD_REQUEST, "M_BAD_JSON", "existing needs the nick of the account")) };
665        let (code, user) = blk(&app, move |a| a.users.invite_existing(&n, now_ms())).await??;
666        return Ok(Json(json!({ "invite": code, "nick": user.nick, "existing": true, "audience": app.challenges.audience() })));
667    }
668    let (code, user) = blk(&app, move |a| a.users.invite(nick.as_deref(), tier.as_deref(), now_ms())).await??;
669    Ok(Json(json!({ "invite": code, "nick": user.nick, "audience": app.challenges.audience() })))
670}
671
672/// `POST /product/v1/enroll {invite, public_key, signature, label?}`.
673async fn key_enroll(State(app): State<App>, Json(b): Json<Value>) -> Result<Json<Value>, ApiError> {
674    let s = |k: &str| b.get(k).and_then(Value::as_str).unwrap_or("").to_string();
675    let (code, pk, sig, label) = (s("invite"), s("public_key"), s("signature"), s("label"));
676    // The nick the client asks for (its session name). Optional; the invite decides whether it is honoured.
677    let want = s("nick");
678    let ttl = key_ttl(&app);
679    let (user, session, key_id) = blk(&app, move |a| a.users.enroll_with_nick(a.challenges.audience(), &code, &pk, &sig, &label, Some(&want), now_ms(), ttl)).await?.map_err(|e| match e {
680        ServiceError::Unauthorized => unauthorized_key(),
681        other => other.into(),
682    })?;
683    Ok(Json(json!({ "nick": user.nick, "token": session.token, "key_id": key_id })))
684}
685
686/// `POST /product/v1/login/key/challenge {key_id}`.
687async fn key_challenge(State(app): State<App>, Json(b): Json<Value>) -> Json<Value> {
688    let kid = b.get("key_id").and_then(Value::as_str).unwrap_or("");
689    Json(serde_json::to_value(app.challenges.issue(kid, now_ms())).unwrap_or(Value::Null))
690}
691
692async fn key_session_for(app: &App, kid: String, cid: String, sig: String) -> Result<(m4a_product_kit::User, m4a_product_kit::service::Session), ApiError> {
693    let ttl = key_ttl(app);
694    blk(app, move |a| a.users.key_login(&a.challenges, &kid, &cid, &sig, now_ms(), ttl)).await?.map_err(|e| match e {
695        ServiceError::Unauthorized => unauthorized_key(),
696        other => other.into(),
697    })
698}
699
700/// `POST /product/v1/login/key {key_id, challenge_id, signature}`.
701async fn key_login(State(app): State<App>, Json(b): Json<Value>) -> Result<Json<Value>, ApiError> {
702    let s = |k: &str| b.get(k).and_then(Value::as_str).unwrap_or("").to_string();
703    let (user, session) = key_session_for(&app, s("key_id"), s("challenge_id"), s("signature")).await?;
704    Ok(session_json(&user.nick, &session.token))
705}
706
707/// `POST /product/v1/login/key/token {key_id, challenge_id, signature}`: the same proof as the key
708/// login, answered with a one-time Matrix login token (`m.login.token`, short lifetime) instead of a
709/// session, so a Matrix client such as Element Web can open with `#/login?loginToken=`. The proof
710/// replaces the password that `login/get_token` asks for; no session of the key is touched.
711async fn key_login_token(State(app): State<App>, Json(b): Json<Value>) -> Result<Json<Value>, ApiError> {
712    let s = |k: &str| b.get(k).and_then(Value::as_str).unwrap_or("").to_string();
713    let (kid, cid, sig) = (s("key_id"), s("challenge_id"), s("signature"));
714    let user = blk(&app, move |a| a.users.key_proof(&a.challenges, &kid, &cid, &sig, now_ms())).await?.map_err(|e| match e {
715        ServiceError::Unauthorized => unauthorized_key(),
716        other => other.into(),
717    })?;
718    let t = app.tokens.issue(&user.nick, now_ms(), LOGIN_TOKEN_TTL_MS);
719    Ok(Json(json!({ "login_token": t, "expires_in_ms": LOGIN_TOKEN_TTL_MS })))
720}
721
722/// The Matrix carriage of the same login: type `org.m4a.login.signature`. With only `key_id` the
723/// answer is 401 carrying the challenge under the type name; with the signature it logs in.
724async fn matrix_key_login(app: &App, body: &Value) -> Result<Json<Value>, ApiError> {
725    let s = |k: &str| body.get(k).and_then(Value::as_str).unwrap_or("").to_string();
726    let (kid, cid, sig) = (s("key_id"), s("challenge_id"), s("signature"));
727    if cid.is_empty() || sig.is_empty() {
728        let c = app.challenges.issue(&kid, now_ms());
729        return Err(ApiError(StatusCode::UNAUTHORIZED, json!({ "errcode": "M_UNAUTHORIZED", "error": "sign the challenge", m4a_seam::keyproof::LOGIN_TYPE: c })));
730    }
731    let (_, session) = key_session_for(app, kid, cid, sig).await?;
732    matrix_session(app, session.token).await
733}