Skip to main content

tuwunel_api_oidc/
userinfo.rs

1use axum::{
2	Json, RequestPartsExt, body,
3	body::Body,
4	extract::{Request, State},
5	http::Method,
6	response::IntoResponse,
7};
8use axum_extra::{
9	TypedHeader,
10	headers::{Authorization, authorization::Bearer},
11};
12use futures::future::join;
13use http::{HeaderValue, Response, StatusCode, header};
14use serde::Deserialize;
15use serde_json::json;
16use tuwunel_core::{Err, Result, err, utils::TryFutureExtExt};
17use tuwunel_service::Services;
18
19use super::oauth_error;
20
21#[derive(Deserialize)]
22struct AccessTokenForm {
23	access_token: Option<String>,
24}
25
26pub(crate) async fn userinfo_route(
27	State(services): State<crate::State>,
28	request: Request,
29) -> Response<Body> {
30	userinfo_inner(&services, request)
31		.await
32		.unwrap_or_else(|e| {
33			let status = e.status_code();
34			let msg = e.sanitized_message();
35			let mut resp = oauth_error(status, "invalid_token", &msg);
36
37			// RFC 6750 §3: include WWW-Authenticate on 401 responses.
38			if status == StatusCode::UNAUTHORIZED {
39				resp.headers_mut().insert(
40					header::WWW_AUTHENTICATE,
41					HeaderValue::from_static(r#"Bearer realm="Matrix", error="invalid_token""#),
42				);
43			}
44
45			resp
46		})
47}
48
49async fn userinfo_inner(services: &Services, request: Request) -> Result<Response<Body>> {
50	let (mut parts, body) = request.into_parts();
51
52	// Authorization header takes priority (required for GET, preferred for POST).
53	let bearer: Option<TypedHeader<Authorization<Bearer>>> =
54		parts.extract().await.unwrap_or(None);
55
56	let token = if let Some(TypedHeader(Authorization(b))) = bearer {
57		b.token().to_owned()
58	} else if parts.method == Method::POST {
59		// RFC 6750 §2.2: POST body may carry access_token as form parameter.
60		let bytes = body::to_bytes(body, 8192)
61			.await
62			.map_err(|_| err!(Request(BadJson("Failed to read request body"))))?;
63
64		serde_html_form::from_bytes::<AccessTokenForm>(&bytes)
65			.ok()
66			.and_then(|f| f.access_token)
67			.ok_or_else(|| {
68				tuwunel_core::err!(Request(MissingToken("No access token provided")))
69			})?
70	} else {
71		return Err!(Request(MissingToken("No access token provided")));
72	};
73
74	let Some((user_id, device_id)) = services
75		.users
76		.find_from_unexpired_token(&token)
77		.await
78	else {
79		return Err!(Request(Unauthorized("Invalid access token")));
80	};
81
82	// RFC OIDC Core §5.3: the userinfo endpoint MUST only respond to tokens
83	// that were issued through an OIDC flow (i.e. with the openid scope).
84	// Reject plain Matrix access tokens that were not issued via OIDC.
85	if !services
86		.users
87		.is_oidc_device(&user_id, &device_id)
88		.await
89	{
90		return Err!(Request(Unauthorized("Token was not issued through OIDC")));
91	}
92
93	let avatar_url = services.profile.avatar_url(&user_id).ok();
94
95	let displayname = services.profile.displayname(&user_id).ok();
96
97	let (avatar_url, displayname) = join(avatar_url, displayname).await;
98
99	let response = json!({
100		"sub": user_id.to_string(),
101		"name": displayname,
102		"picture": avatar_url,
103	});
104
105	Ok(Json(response).into_response())
106}