tuwunel_api/client/session/sso/
uiaa.rs1use axum::extract::State;
2use ruma::api::client::uiaa::{AuthType, UiaaInfo, get_uiaa_fallback_page};
3use serde_json::Value as JsonValue;
4use tuwunel_core::{Err, Result, trace, utils::BoolExt};
5
6use crate::{Ruma, oidc::url_encode};
7
8#[tracing::instrument(
12 name = "sso_fallback",
13 level = "debug",
14 skip_all,
15 fields(session = body.body.session),
16)]
17pub(crate) async fn sso_fallback_route(
18 State(services): State<crate::State>,
19 body: Ruma<get_uiaa_fallback_page::v3::Request>,
20) -> Result<get_uiaa_fallback_page::v3::Response> {
21 use get_uiaa_fallback_page::v3::Response;
22
23 let session = &body.body.session;
24
25 let completed = |uiaainfo: &UiaaInfo| {
27 uiaainfo.completed.contains(&AuthType::Sso)
28 || uiaainfo.completed.contains(&AuthType::OAuth)
29 };
30
31 let session_data = services
35 .uiaa
36 .get_uiaa_session_by_session_id(session)
37 .await
38 .inspect(|session_data| trace!(?session_data));
39
40 if session_data
41 .as_ref()
42 .is_some_and(|(_, _, uiaainfo)| completed(uiaainfo))
43 {
44 let html = include_str!("complete.html");
45
46 return Ok(Response::html(html.as_bytes().to_vec()));
47 }
48
49 let has_flow_with_sso_stage = || {
51 session_data
52 .as_ref()
53 .is_some_and(|(_, _, uiaainfo)| {
54 uiaainfo
55 .flows
56 .iter()
57 .any(|flow| flow.stages.contains(&AuthType::Sso))
58 })
59 };
60
61 let idp_id: Option<String> = session_data
66 .as_ref()
67 .map(|(_, _, uiaainfo)| uiaainfo)
68 .inspect(|uiaainfo| trace!(?uiaainfo))
69 .and_then(|uiaainfo| {
70 let raw = uiaainfo.params.as_ref()?.get();
71 let params: JsonValue = serde_json::from_str(raw).ok()?;
72
73 params["m.login.sso"]["identity_providers"]
74 .as_array()?
75 .first()?["id"]
76 .as_str()
77 .map(ToOwned::to_owned)
78 })
79 .or_else(|| {
80 has_flow_with_sso_stage()
81 .is_false()
82 .then_some(String::new())
83 });
84
85 let Some(ref idp) = idp_id else {
89 return Err!(Request(Forbidden(
90 "No SSO provider bound to this UIAA session; cannot complete re-authentication"
91 )));
92 };
93
94 let empty_or_slash = idp
95 .is_empty()
96 .then_some(idp.as_str())
97 .unwrap_or("/");
98
99 let url_str = format!(
100 "/_matrix/client/v3/login/sso/redirect{}{}?redirectUrl=uiaa:{}",
101 empty_or_slash,
102 url_encode(idp),
103 url_encode(session)
104 );
105
106 let html = include_str!("required.html");
107 let output = html.replace("{{url_str}}", &url_str);
108
109 Ok(Response::html(output.into_bytes()))
110}