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 Ok((user_id, device_id, _expires)) = services.users.find_from_token(&token).await else {
75		return Err!(Request(Unauthorized("Invalid access token")));
76	};
77
78	// RFC OIDC Core §5.3: the userinfo endpoint MUST only respond to tokens
79	// that were issued through an OIDC flow (i.e. with the openid scope).
80	// Reject plain Matrix access tokens that were not issued via OIDC.
81	if !services
82		.users
83		.is_oidc_device(&user_id, &device_id)
84		.await
85	{
86		return Err!(Request(Unauthorized("Token was not issued through OIDC")));
87	}
88
89	let avatar_url = services.users.avatar_url(&user_id).ok();
90
91	let displayname = services.users.displayname(&user_id).ok();
92
93	let (avatar_url, displayname) = join(avatar_url, displayname).await;
94
95	let response = json!({
96		"sub": user_id.to_string(),
97		"name": displayname,
98		"picture": avatar_url,
99	});
100
101	Ok(Json(response).into_response())
102}