1use std::{fmt::Write, net::IpAddr};
2
3use axum::{
4 extract::{Form, Request, State},
5 response::{Redirect, Response},
6};
7use const_str::format as const_format;
8use http::StatusCode;
9use ruma::{OwnedUserId, UserId};
10use serde::Deserialize;
11use serde_json::json;
12use tuwunel_core::{
13 Err, Result, err,
14 utils::{self, hash, html::escape as html_escape},
15};
16use tuwunel_service::{Services, users::Register};
17use url::Url;
18
19use super::{
20 account::{
21 ACCOUNT_HEAD, account_error_response, account_html_response, account_redirect_response,
22 },
23 url_encode,
24};
25use crate::ClientIp;
26
27const LOGIN_TOKEN_LENGTH: usize = 32;
28
29#[derive(Debug, Default, Deserialize)]
30struct NativeQuery {
31 oidc_req_id: Option<String>,
32 view: Option<String>,
33}
34
35#[derive(Debug, Deserialize)]
36pub(crate) struct NativeSubmit {
37 oidc_req_id: String,
38 #[serde(default)]
39 mode: Option<String>,
40 username: String,
41 password: String,
42 #[serde(default)]
43 registration_token: Option<String>,
44 #[serde(default)]
45 accept_terms: Option<String>,
46}
47
48pub(crate) async fn native_get_route(
51 State(services): State<crate::State>,
52 request: Request,
53) -> Response {
54 if let Err(e) = require_native(&services) {
55 return account_error_response(&e);
56 }
57
58 let params: NativeQuery =
59 match serde_html_form::from_str(request.uri().query().unwrap_or_default()) {
60 | Ok(params) => params,
61 | Err(e) => return account_error_response(&e.into()),
62 };
63
64 let req_id = params.oidc_req_id.as_deref().unwrap_or_default();
65 let view = params.view.as_deref().unwrap_or("login");
66
67 account_html_response(StatusCode::OK, render_page(&services, view, req_id, None).await)
68}
69
70pub(crate) async fn native_submit_route(
74 State(services): State<crate::State>,
75 ClientIp(client): ClientIp,
76 Form(body): Form<NativeSubmit>,
77) -> Response {
78 match native_submit(&services, client, &body).await {
79 | Ok(response) => response,
80 | Err(e) => {
81 let view = match body.mode.as_deref() {
82 | Some("register") => "register",
83 | _ => "login",
84 };
85
86 let msg = e.sanitized_message();
87 let html = render_page(&services, view, &body.oidc_req_id, Some(&msg)).await;
88
89 account_html_response(e.status_code(), html)
90 },
91 }
92}
93
94async fn native_submit(
95 services: &Services,
96 client: IpAddr,
97 body: &NativeSubmit,
98) -> Result<Response> {
99 require_native(services)?;
100 services.oauth.check_device_rate_limit(client)?;
102 services.oauth.check_rate_limit(client)?;
103
104 let user_id = match body.mode.as_deref() {
105 | Some("register") => do_register(services, body).await?,
106 | _ => verify_credentials(services, &body.username, &body.password).await?,
107 };
108
109 let token = utils::random_string(LOGIN_TOKEN_LENGTH);
110 let _expires_in = services
111 .users
112 .create_login_token(&user_id, &token);
113
114 let redirect = complete_redirect(services, &body.oidc_req_id, &token)?;
115
116 Ok(account_redirect_response(redirect))
117}
118
119async fn verify_credentials(
122 services: &Services,
123 username: &str,
124 password: &str,
125) -> Result<OwnedUserId> {
126 let invalid = || err!(Request(Forbidden("Invalid username or password.")));
127 let server_name = &services.config.server_name;
128
129 let user_id = UserId::parse_with_server_name(username, server_name).map_err(|_| invalid())?;
130
131 if !services.globals.user_is_local(&user_id) {
132 return Err(invalid());
133 }
134
135 let (user_id, hash) = match services.users.password_hash(&user_id).await {
138 | Ok(hash) => (user_id, hash),
139 | Err(_) => {
140 let lowercased = UserId::parse_with_server_name(username.to_lowercase(), server_name)
141 .map_err(|_| invalid())?;
142
143 let hash = services
144 .users
145 .password_hash(&lowercased)
146 .await
147 .map_err(|_| invalid())?;
148
149 (lowercased, hash)
150 },
151 };
152
153 if services
155 .users
156 .origin(&user_id)
157 .await
158 .is_ok_and(|origin| origin != "password")
159 {
160 return Err(invalid());
161 }
162
163 if hash.is_empty() {
164 return Err(invalid());
165 }
166
167 hash::verify_password(password, &hash).map_err(|_| invalid())?;
168
169 Ok(user_id)
170}
171
172async fn do_register(services: &Services, body: &NativeSubmit) -> Result<OwnedUserId> {
173 if !services.config.allow_registration {
174 return Err!(Request(Forbidden("Registration is disabled on this server.")));
175 }
176
177 let username = body.username.trim().to_lowercase();
178 if username.is_empty() {
179 return Err!(Request(InvalidUsername("A username is required.")));
180 }
181
182 if body.password.is_empty() {
183 return Err!(Request(InvalidParam("A password is required.")));
184 }
185
186 let token_required = services.registration_tokens.is_enabled().await;
189 let smtp = &services.config.smtp;
190 let email_required = smtp.connection_uri.is_some()
191 && (smtp.require_email_for_registration
192 || (token_required && smtp.require_email_for_token_registration));
193
194 if email_required {
195 return Err!(Request(Forbidden(
196 "This server requires an email to register, which this page cannot collect."
197 )));
198 }
199
200 if services
201 .config
202 .forbidden_usernames
203 .is_match(&username)
204 {
205 return Err!(Request(Forbidden("That username is not allowed.")));
206 }
207
208 let user_id = UserId::parse_with_server_name(&username, &services.config.server_name)
209 .map_err(|_| err!(Request(InvalidUsername("That username is not valid."))))?;
210
211 user_id.validate_strict().map_err(|_| {
212 err!(Request(InvalidUsername("That username contains disallowed characters.")))
213 })?;
214
215 if services
216 .appservice
217 .is_exclusive_user_id(&user_id)
218 .await
219 {
220 return Err!(Request(Exclusive("That username is reserved by an appservice.")));
221 }
222
223 if services.users.exists(&user_id).await {
224 return Err!(Request(UserInUse("That username is taken.")));
225 }
226
227 if !services.config.registration_terms.is_empty()
230 && body.accept_terms.as_deref() != Some("on")
231 {
232 return Err!(Request(Forbidden("You must accept the terms to register.")));
233 }
234
235 if token_required {
236 let token = body
237 .registration_token
238 .as_deref()
239 .unwrap_or_default();
240
241 services
242 .registration_tokens
243 .try_consume(token)
244 .await?;
245 }
246
247 services
248 .users
249 .full_register(Register {
250 user_id: Some(&user_id),
251 password: Some(&body.password),
252 grant_first_user_admin: true,
253 ..Default::default()
254 })
255 .await?;
256
257 record_accepted_terms(services, &user_id).await?;
258
259 Ok(user_id)
260}
261
262async fn record_accepted_terms(services: &Services, user_id: &UserId) -> Result {
263 let accepted: Vec<String> = services
264 .config
265 .registration_terms
266 .values()
267 .flat_map(|policy| policy.translations.values())
268 .map(|translation| translation.url.to_string())
269 .collect();
270
271 if accepted.is_empty() {
272 return Ok(());
273 }
274
275 let event_type = "m.accepted_terms";
276 let event = json!({
277 "type": event_type,
278 "content": { "accepted": accepted },
279 });
280
281 services
282 .account_data
283 .update(None, user_id, event_type.into(), &event)
284 .await
285}
286
287fn complete_redirect(services: &Services, req_id: &str, login_token: &str) -> Result<Redirect> {
288 let issuer = services.oauth.get_server()?.issuer_url()?;
289 let base = issuer.trim_end_matches('/');
290
291 let url = Url::parse(&format!("{base}/_tuwunel/oidc/_complete"))
292 .map_err(|_| err!(error!("Failed to build complete URL")))
293 .map(|mut url| {
294 url.query_pairs_mut()
295 .append_pair("oidc_req_id", req_id)
296 .append_pair("loginToken", login_token);
297
298 url
299 })?;
300
301 Ok(Redirect::temporary(url.as_str()))
302}
303
304fn require_native(services: &Services) -> Result {
305 services.oauth.get_server()?;
306
307 services
308 .config
309 .oidc_native_auth
310 .then_some(())
311 .ok_or_else(|| err!(Request(NotFound("Native authentication is not enabled"))))
312}
313
314async fn render_page(
315 services: &Services,
316 view: &str,
317 req_id: &str,
318 error: Option<&str>,
319) -> String {
320 let registration_enabled = services.config.allow_registration;
321
322 match view {
323 | "register" if registration_enabled => render_register(services, req_id, error).await,
324 | _ => render_login(req_id, error, registration_enabled),
325 }
326}
327
328fn render_login(req_id: &str, error: Option<&str>, show_register: bool) -> String {
329 let register_link = show_register
330 .then(|| {
331 format!(
332 r#"<p class="nav">No account? <a href="/_tuwunel/oidc/native?oidc_req_id={}&view=register">Create one</a>.</p>"#,
333 url_encode(req_id),
334 )
335 })
336 .unwrap_or_default();
337
338 LOGIN_HTML
339 .replace("{register_link}", ®ister_link)
340 .replace("{error}", &error_block(error))
341 .replace("{req_id}", &html_escape(req_id))
343}
344
345async fn render_register(services: &Services, req_id: &str, error: Option<&str>) -> String {
346 let token_field = services
347 .registration_tokens
348 .is_enabled()
349 .await
350 .then_some(TOKEN_FIELD)
351 .unwrap_or_default();
352
353 REGISTER_HTML
354 .replace("{token_field}", token_field)
355 .replace("{req_id_enc}", &url_encode(req_id))
356 .replace("{terms}", &terms_block(services))
357 .replace("{error}", &error_block(error))
358 .replace("{req_id}", &html_escape(req_id))
360}
361
362fn error_block(error: Option<&str>) -> String {
363 error
364 .map(|msg| format!(r#"<p class="err">{}</p>"#, html_escape(msg)))
365 .unwrap_or_default()
366}
367
368fn terms_block(services: &Services) -> String {
369 let policies = &services.config.registration_terms;
370 if policies.is_empty() {
371 return String::new();
372 }
373
374 let links = policies
375 .values()
376 .filter_map(|policy| {
377 policy
378 .translations
379 .get("en")
380 .or_else(|| policy.translations.values().next())
381 })
382 .fold(String::new(), |mut links, translation| {
383 write!(
384 links,
385 r#"<li><a href="{}" target="_blank" rel="noopener noreferrer">{}</a></li>"#,
386 html_escape(translation.url.as_str()),
387 html_escape(&translation.name),
388 )
389 .ok();
390
391 links
392 });
393
394 format!(
395 r#"<fieldset class="terms"><legend>Terms</legend><ul>{links}</ul><label><input type="checkbox" name="accept_terms" value="on" required> I accept the terms above.</label></fieldset>"#
396 )
397}
398
399static LOGIN_HTML: &str = const_format!(
400 r#"
401<!DOCTYPE html>
402<html lang="en">
403 <head>
404 {ACCOUNT_HEAD}
405 <title>Sign In</title>
406 </head>
407 <body>
408 <h1>Sign In</h1>
409 {{error}}
410 <form method="POST" action="/_tuwunel/oidc/native">
411 <input type="hidden" name="oidc_req_id" value="{{req_id}}">
412 <input type="hidden" name="mode" value="login">
413 <label>
414 Username
415 <input type="text" name="username" autocomplete="username" autofocus required>
416 </label>
417 <label>
418 Password
419 <input type="password" name="password" autocomplete="current-password" required>
420 </label>
421 <button type="submit">Sign in</button>
422 </form>
423 {{register_link}}
424 </body>
425</html>"#
426);
427
428static REGISTER_HTML: &str = const_format!(
429 r#"
430<!DOCTYPE html>
431<html lang="en">
432 <head>
433 {ACCOUNT_HEAD}
434 <title>Create Account</title>
435 </head>
436 <body>
437 <h1>Create Account</h1>
438 {{error}}
439 <form method="POST" action="/_tuwunel/oidc/native">
440 <input type="hidden" name="oidc_req_id" value="{{req_id}}">
441 <input type="hidden" name="mode" value="register">
442 <label>
443 Username
444 <input type="text" name="username" autocomplete="username" autofocus required>
445 </label>
446 <label>
447 Password
448 <input type="password" name="password" autocomplete="new-password" required>
449 </label>
450 {{token_field}}
451 {{terms}}
452 <button type="submit">Create account</button>
453 </form>
454 <p class="nav">Have an account? <a href="/_tuwunel/oidc/native?oidc_req_id={{req_id_enc}}&view=login">Sign in</a>.</p>
455 </body>
456</html>"#
457);
458
459static TOKEN_FIELD: &str = r#"<label>
460 Registration token
461 <input type="text" name="registration_token" autocomplete="off" required>
462 </label>"#;
463
464#[cfg(test)]
465mod tests {
466 use super::{error_block, render_login};
467
468 #[test]
469 fn login_page_has_form_and_hidden_req_id() {
470 let html = render_login("REQ123", None, false);
471
472 assert!(html.contains(r#"action="/_tuwunel/oidc/native""#));
473 assert!(html.contains(r#"name="oidc_req_id" value="REQ123""#));
474 assert!(html.contains(r#"name="username""#));
475 assert!(html.contains(r#"name="password""#));
476 assert!(!html.contains("view=register"));
477 }
478
479 #[test]
480 fn login_page_links_to_register_when_enabled() {
481 let html = render_login("REQ123", None, true);
482
483 assert!(html.contains("oidc_req_id=REQ123&view=register"));
484 }
485
486 #[test]
487 fn login_page_escapes_error_and_req_id() {
488 let html = render_login("a<b>c", Some("<script>alert(1)</script>"), false);
489
490 assert!(!html.contains("<script>"));
491 assert!(html.contains("<script>"));
492 assert!(!html.contains("a<b>c"));
493 assert!(html.contains("a<b>c"));
494 }
495
496 #[test]
497 fn login_page_does_not_expand_smuggled_placeholder() {
498 let html = render_login("{error}", Some("BOOM"), false);
500
501 assert_eq!(html.matches("BOOM").count(), 1);
502 assert!(html.contains(r#"value="{error}""#));
503 }
504
505 #[test]
506 fn error_block_renders_only_when_present() {
507 assert!(error_block(None).is_empty());
508 assert!(error_block(Some("oops")).contains(r#"class="err""#));
509 }
510}