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