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