Skip to main content

tuwunel_api_oidc/
mod.rs

1pub(crate) mod account;
2pub(crate) mod auth_issuer;
3pub(crate) mod auth_metadata;
4pub(crate) mod authorize;
5pub(crate) mod complete;
6pub(crate) mod device;
7pub(crate) mod jwks;
8pub(crate) mod native;
9pub(crate) mod registration;
10pub(crate) mod revoke;
11pub(crate) mod token;
12pub(crate) mod userinfo;
13
14/// Provides the OpenID Connect route builder.
15///
16/// It groups discovery, authorization and callback endpoints.
17pub mod routes;
18
19use axum::{Json, body::Body, response::IntoResponse};
20use http::{Response, StatusCode};
21use log as _;
22use ruma::{OwnedUserId, UserId};
23use serde_json::json;
24use tuwunel_api::router::{ClientIp, RateLimitIp, State};
25pub(crate) use tuwunel_core::utils::url::{redirect_allowlisted, url_encode};
26use tuwunel_core::{Result, err, mod_ctor, mod_dtor};
27use tuwunel_service::{Services, users::LoginToken};
28use url::Url;
29
30pub(crate) use self::{
31	account::{
32		ACCOUNT_MANAGEMENT_ACTIONS_SUPPORTED, account_css_route, account_js_route,
33		get_account_callback_route, get_account_route, post_account_callback_route,
34	},
35	auth_issuer::auth_issuer_route,
36	auth_metadata::openid_configuration_route,
37	authorize::authorize_route,
38	complete::{complete_route, post_complete_route},
39	device::{
40		device_authorization_route, get_device_callback_route, get_device_route,
41		post_device_callback_route,
42	},
43	jwks::jwks_route,
44	native::{native_get_route, native_submit_route},
45	registration::registration_route,
46	revoke::revoke_route,
47	token::token_route,
48	userinfo::userinfo_route,
49};
50
51const OIDC_REQ_ID_LENGTH: usize = 32;
52
53fn oauth_error(status: StatusCode, error: &str, description: &str) -> Response<Body> {
54	let body = json!({
55		"error": error,
56		"error_description": description,
57	});
58
59	(status, Json(body)).into_response()
60}
61
62#[tracing::instrument(level = "debug", skip_all)]
63async fn require_account_usable(services: &Services, user_id: &UserId) -> Result {
64	services.users.deactivated_check(user_id).await?;
65	services.users.locked_check(user_id).await
66}
67
68async fn consume_login_token(services: &Services, token: Option<&str>) -> Result<OwnedUserId> {
69	let token = token.ok_or_else(|| err!(Request(Forbidden("Missing login token"))))?;
70
71	services
72		.users
73		.find_from_login_token(token)
74		.await
75		.map_err(|_| err!(Request(Forbidden("Invalid or expired login token"))))
76}
77
78async fn consume_login_token_with_provider(
79	services: &Services,
80	token: Option<&str>,
81) -> Result<LoginToken> {
82	let token = token.ok_or_else(|| err!(Request(Forbidden("Missing login token"))))?;
83
84	services
85		.users
86		.find_login_token_with_provider(token)
87		.await
88		.map_err(|_| err!(Request(Forbidden("Invalid or expired login token"))))
89}
90
91/// Verify a login token without consuming it; it is consumed later when the
92/// confirmation form is submitted.
93async fn peek_login_token(services: &Services, token: Option<&str>) -> Result<OwnedUserId> {
94	let token = token.ok_or_else(|| err!(Request(Forbidden("Missing login token"))))?;
95
96	services
97		.users
98		.peek_login_token(token)
99		.await
100		.map_err(|_| err!(Request(Forbidden("Invalid or expired login token"))))
101}
102
103async fn peek_login_token_with_provider(
104	services: &Services,
105	token: Option<&str>,
106) -> Result<LoginToken> {
107	let token = token.ok_or_else(|| err!(Request(Forbidden("Missing login token"))))?;
108
109	services
110		.users
111		.peek_login_token_with_provider(token)
112		.await
113		.map_err(|_| err!(Request(Forbidden("Invalid or expired login token"))))
114}
115
116/// Build the upstream SSO redirect URL for a pending authorization request.
117///
118/// The provider hands the browser back to the completion route carrying the
119/// request id, where the authorization code is minted. A trailing slash on the
120/// issuer is ignored.
121fn authorization_sso_url(issuer: &str, idp_id: &str, req_id: &str) -> Result<Url> {
122	let base = issuer.trim_end_matches('/');
123	let complete = format!("{base}/_tuwunel/oidc/_complete");
124	let callback = Url::parse_with_params(&complete, [("oidc_req_id", req_id)])
125		.map_err(|_| err!(error!("Failed to build complete URL")))?;
126
127	sso_redirect_url(base, idp_id, &callback)
128}
129
130fn sso_redirect_url(base: &str, idp_id: &str, callback: &Url) -> Result<Url> {
131	let idp_id_enc = url_encode(idp_id);
132	let mut sso_url =
133		Url::parse(&format!("{base}/_matrix/client/v3/login/sso/redirect/{idp_id_enc}"))
134			.map_err(|_| err!(error!("Failed to build SSO URL")))?;
135
136	sso_url
137		.query_pairs_mut()
138		.append_pair("redirectUrl", callback.as_str());
139
140	Ok(sso_url)
141}
142
143mod_ctor! {}
144mod_dtor! {}