1use std::time::Duration;
2
3use axum::{
4 Json,
5 body::Body,
6 extract::{Form, State},
7 response::IntoResponse,
8};
9use http::{
10 Response, StatusCode,
11 header::{CACHE_CONTROL, PRAGMA},
12};
13use ruma::{OwnedDeviceId, UserId};
14use serde::Deserialize;
15use serde_json::json;
16use tuwunel_core::{
17 Err, Error, Result, err, info,
18 utils::{
19 BoolExt,
20 future::OptionFutureExt,
21 time::{now, timepoint_has_passed},
22 },
23 warn,
24};
25use tuwunel_service::{
26 Services,
27 oauth::server::{DeviceGrantPoll, IdTokenClaims, Server, narrow_scope},
28 users::device::{RefreshToken, generate_refresh_token},
29};
30
31use super::oauth_error;
32use crate::ClientIp;
33
34#[derive(Debug, Deserialize)]
35pub(crate) struct TokenRequest {
36 grant_type: String,
37 code: Option<String>,
38 redirect_uri: Option<String>,
39 client_id: Option<String>,
40 code_verifier: Option<String>,
41 refresh_token: Option<String>,
42 device_code: Option<String>,
43 #[serde(rename = "scope")]
44 _scope: Option<String>,
45}
46
47struct ApprovedGrant<'a> {
49 client_id: &'a str,
50 scope: &'a str,
51 user_id: &'a UserId,
52 nonce: Option<String>,
53 idp_id: Option<String>,
54}
55
56pub(crate) async fn token_route(
57 State(services): State<crate::State>,
58 ClientIp(client): ClientIp,
59 Form(body): Form<TokenRequest>,
60) -> impl IntoResponse {
61 let inner = if services.oauth.check_rate_limit(client).is_err() {
64 oauth_error(StatusCode::TOO_MANY_REQUESTS, "slow_down", "Too many token requests")
65 } else {
66 match body.grant_type.as_str() {
67 | "authorization_code" => token_authorization_code(&services, &body)
68 .await
69 .unwrap_or_else(token_error_response),
70
71 | "refresh_token" => token_refresh(&services, &body)
72 .await
73 .unwrap_or_else(token_error_response),
74
75 | "urn:ietf:params:oauth:grant-type:device_code" =>
76 token_device_code(&services, &body)
77 .await
78 .unwrap_or_else(token_error_response),
79
80 | _ => oauth_error(
81 StatusCode::BAD_REQUEST,
82 "unsupported_grant_type",
83 "Unsupported grant_type",
84 ),
85 }
86 };
87 let mut response = inner.into_response();
88 let headers = response.headers_mut();
89 headers.insert(CACHE_CONTROL, http::HeaderValue::from_static("no-store"));
90 headers.insert(PRAGMA, http::HeaderValue::from_static("no-cache"));
91 response
92}
93
94async fn token_authorization_code(
95 services: &Services,
96 body: &TokenRequest,
97) -> Result<Response<Body>> {
98 let code = body
99 .code
100 .as_deref()
101 .ok_or_else(|| err!(Request(InvalidParam("code is required"))))?;
102
103 let redirect_uri = body
104 .redirect_uri
105 .as_deref()
106 .ok_or_else(|| err!(Request(InvalidParam("redirect_uri is required"))))?;
107
108 let client_id = body
109 .client_id
110 .as_deref()
111 .ok_or_else(|| err!(Request(InvalidParam("client_id is required"))))?;
112
113 let session = services
114 .oauth
115 .get_server()?
116 .exchange_auth_code(
117 code,
118 client_id,
119 redirect_uri,
120 body.code_verifier.as_deref(),
121 services.server.config.oidc_require_pkce,
122 )
123 .await?;
124
125 issue_tokens(services, ApprovedGrant {
126 client_id: &session.client_id,
127 scope: &session.scope,
128 user_id: &session.user_id,
129 nonce: session.nonce,
130 idp_id: session.idp_id,
131 })
132 .await
133}
134
135async fn token_device_code(services: &Services, body: &TokenRequest) -> Result<Response<Body>> {
138 let device_code = body
139 .device_code
140 .as_deref()
141 .ok_or_else(|| err!(Request(InvalidParam("device_code is required"))))?;
142
143 let client_id = body
144 .client_id
145 .as_deref()
146 .ok_or_else(|| err!(Request(InvalidParam("client_id is required"))))?;
147
148 match services
149 .oauth
150 .get_server()?
151 .poll_device_grant(device_code, client_id)
152 .await?
153 {
154 | DeviceGrantPoll::Pending => Ok(oauth_error(
155 StatusCode::BAD_REQUEST,
156 "authorization_pending",
157 "The user has not yet completed authorization",
158 )),
159
160 | DeviceGrantPoll::Denied => Ok(oauth_error(
161 StatusCode::BAD_REQUEST,
162 "access_denied",
163 "The authorization request was denied",
164 )),
165
166 | DeviceGrantPoll::Expired => Ok(oauth_error(
167 StatusCode::BAD_REQUEST,
168 "expired_token",
169 "The device code has expired",
170 )),
171
172 | DeviceGrantPoll::Approved(grant) =>
173 issue_tokens(services, ApprovedGrant {
174 client_id: &grant.client_id,
175 scope: &grant.scope,
176 user_id: &grant.user_id,
177 nonce: None,
178 idp_id: grant.idp_id,
179 })
180 .await,
181 }
182}
183
184async fn issue_tokens(services: &Services, grant: ApprovedGrant<'_>) -> Result<Response<Body>> {
187 let ApprovedGrant { client_id, scope, user_id, nonce, idp_id } = grant;
188
189 let (granted_scope, requested_device_id) =
190 narrow_scope(scope, services.server.config.oidc_strict_scope)?;
191
192 let requested_device: Option<OwnedDeviceId> = requested_device_id
193 .as_deref()
194 .map(OwnedDeviceId::from);
195
196 if requested_device.is_none() && services.server.config.oidc_require_device_scope {
197 return Err!(Request(InvalidParam(
198 "a device scope (urn:matrix:client:device:<id>) is required"
199 )));
200 }
201
202 let (access_token, expires_in) = services.users.generate_access_token(true);
203 let refresh_token = generate_refresh_token();
204 let client_name = services
205 .oauth
206 .get_server()?
207 .get_client(client_id)
208 .await
209 .ok()
210 .and_then(|c| c.client_name);
211
212 let device_display_name = client_name.as_deref().unwrap_or("OIDC Client");
213
214 let iss = services.oauth.get_server()?.issuer_url()?;
215 let id_token = granted_scope
216 .contains("openid")
217 .then(|| {
218 let now = now().as_secs();
219 let claims = IdTokenClaims {
220 iss,
221 sub: user_id.to_string(),
222 aud: client_id.to_owned(),
223 exp: now.saturating_add(3600),
224 iat: now,
225 nonce,
226 at_hash: Some(Server::at_hash(&access_token)),
227 };
228
229 services
230 .oauth
231 .get_server()?
232 .sign_id_token(&claims)
233 })
234 .transpose()?;
235
236 let device_id = services
237 .users
238 .create_device(
239 user_id,
240 requested_device.as_deref(),
241 (Some(&access_token), expires_in),
242 Some(&refresh_token),
243 Some(device_display_name),
244 None,
245 )
246 .await?;
247
248 if let Some(idp_id) = idp_id.filter(|idp| !idp.is_empty()) {
251 services
252 .users
253 .mark_oidc_device(user_id, &device_id, &idp_id);
254 }
255
256 info!("{user_id} logged in via OIDC on {device_id} ({device_display_name})");
257
258 let scope = if requested_device.is_some() {
261 granted_scope
262 } else {
263 warn!(%user_id, %device_id, "OIDC client omitted the device scope; generated a device id");
264
265 let sep = if granted_scope.is_empty() { "" } else { " " };
266 format!("{granted_scope}{sep}urn:matrix:client:device:{device_id}")
267 };
268
269 let mut response = json!({
270 "access_token": access_token,
271 "refresh_token": refresh_token,
272 "scope": scope,
273 "token_type": "Bearer",
274 });
275
276 if let Some(id_token) = id_token {
277 response["id_token"] = json!(id_token);
278 }
279
280 if let Some(expires_in) = expires_in {
281 response["expires_in"] = json!(expires_in.as_secs());
282 }
283
284 Ok(Json(response).into_response())
285}
286
287async fn token_refresh(services: &Services, body: &TokenRequest) -> Result<Response<Body>> {
288 let presented = body
289 .refresh_token
290 .as_deref()
291 .ok_or_else(|| err!(Request(InvalidParam("refresh_token is required"))))?;
292
293 match services
294 .users
295 .classify_refresh_token(presented)
296 .await
297 {
298 | RefreshToken::Current { user_id, device_id, expires_at } => {
299 if expires_at.is_some_and(timepoint_has_passed) {
300 services
301 .server
302 .config
303 .refresh_token_hard_logout
304 .then_async(|| services.users.remove_device(&user_id, &device_id))
305 .unwrap_or_else_async(async || {
306 services
307 .users
308 .remove_refresh_token(&user_id, &device_id)
309 .await
310 .ok();
311 })
312 .await;
313
314 return Err!(Request(Forbidden("Refresh token has expired")));
315 }
316
317 let (access_token, expires_in) = services.users.generate_access_token(true);
318 let refresh_token = generate_refresh_token();
319 services
320 .users
321 .set_access_token(
322 &user_id,
323 &device_id,
324 &access_token,
325 expires_in,
326 Some(&refresh_token),
327 )
328 .await?;
329
330 token_refresh_response(&access_token, &refresh_token, expires_in)
331 },
332
333 | RefreshToken::Replayed { user_id, device_id, current, grace } if grace => {
334 let (access_token, expires_in) = services.users.generate_access_token(true);
337 services
338 .users
339 .set_access_token(&user_id, &device_id, &access_token, expires_in, None)
340 .await?;
341
342 token_refresh_response(&access_token, ¤t, expires_in)
343 },
344
345 | RefreshToken::Replayed { user_id, device_id, .. } => {
346 let revoke = services.server.config.refresh_token_reuse_revoke;
347 warn!(%user_id, %device_id, revoke, "OIDC refresh token reused after rotation");
348
349 if revoke {
350 services
351 .users
352 .remove_device(&user_id, &device_id)
353 .await;
354 }
355
356 Err!(Request(Forbidden("Refresh token has already been used")))
357 },
358
359 | RefreshToken::Unknown => Err!(Request(Forbidden("Invalid refresh token"))),
360 }
361}
362
363fn token_refresh_response(
364 access_token: &str,
365 refresh_token: &str,
366 expires_in: Option<Duration>,
367) -> Result<Response<Body>> {
368 let mut response = json!({
369 "access_token": access_token,
370 "refresh_token": refresh_token,
371 "token_type": "Bearer",
372 });
373
374 if let Some(expires_in) = expires_in {
375 response["expires_in"] = json!(expires_in.as_secs());
376 }
377
378 Ok(Json(response).into_response())
379}
380
381#[expect(clippy::needless_pass_by_value)]
385fn token_error_response(e: Error) -> Response<Body> {
386 if !e.status_code().is_client_error() {
387 return oauth_error(
388 StatusCode::INTERNAL_SERVER_ERROR,
389 "server_error",
390 "An internal error occurred",
391 );
392 }
393
394 oauth_error(StatusCode::BAD_REQUEST, "invalid_grant", &e.sanitized_message())
395}