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