Skip to main content

tuwunel_api/client/session/
refresh.rs

1use axum::extract::State;
2use ruma::api::{
3	client::session::refresh_token::v3::{Request, Response},
4	error::{ErrorKind, UnknownTokenErrorData},
5};
6use tuwunel_core::{
7	Err, Error, Result, debug_info,
8	utils::{BoolExt, future::OptionFutureExt, time::timepoint_has_passed},
9};
10use tuwunel_service::users::device::{RefreshToken, generate_refresh_token};
11
12use crate::{ClientIp, Ruma};
13
14/// # `POST /_matrix/client/v3/refresh`
15///
16/// Refresh an access token.
17///
18/// <https://spec.matrix.org/v1.15/client-server-api/#post_matrixclientv3refresh>
19#[tracing::instrument(skip_all, fields(%client), name = "refresh_token")]
20pub(crate) async fn refresh_token_route(
21	State(services): State<crate::State>,
22	ClientIp(client): ClientIp,
23	body: Ruma<Request>,
24) -> Result<Response> {
25	let refresh_token_claim = body.body.refresh_token;
26
27	if !refresh_token_claim.starts_with("refresh_") {
28		return Err!(Request(Forbidden("Refresh token is malformed.")));
29	}
30
31	match services
32		.users
33		.classify_refresh_token(&refresh_token_claim)
34		.await
35	{
36		| RefreshToken::Current { user_id, device_id, expires_at } => {
37			if expires_at.is_some_and(timepoint_has_passed) {
38				let hard = services.server.config.refresh_token_hard_logout;
39				hard.then_async(|| services.users.remove_device(&user_id, &device_id))
40					.unwrap_or_else_async(async || {
41						services
42							.users
43							.remove_refresh_token(&user_id, &device_id)
44							.await
45							.ok();
46					})
47					.await;
48
49				return Err(Error::BadRequest(
50					ErrorKind::UnknownToken(UnknownTokenErrorData { soft_logout: !hard }),
51					"Refresh token has expired.",
52				));
53			}
54
55			let refresh_token = Some(generate_refresh_token());
56			let (access_token, expires_in_ms) = services.users.generate_access_token(true);
57
58			services
59				.users
60				.set_access_token(
61					&user_id,
62					&device_id,
63					&access_token,
64					expires_in_ms,
65					refresh_token.as_deref(),
66				)
67				.await?;
68
69			debug_info!(?user_id, ?device_id, ?expires_in_ms, "refreshed their access_token",);
70
71			Ok(Response {
72				access_token,
73				refresh_token,
74				expires_in_ms,
75			})
76		},
77
78		| RefreshToken::Replayed { user_id, device_id, current, grace } if grace => {
79			// Benign double-submit: re-issue an access token for the unchanged
80			// refresh token rather than rotating it.
81			let (access_token, expires_in_ms) = services.users.generate_access_token(true);
82
83			services
84				.users
85				.set_access_token(&user_id, &device_id, &access_token, expires_in_ms, None)
86				.await?;
87
88			Ok(Response {
89				access_token,
90				refresh_token: Some(current),
91				expires_in_ms,
92			})
93		},
94
95		| RefreshToken::Replayed { user_id, device_id, .. } => {
96			let revoke = services.server.config.refresh_token_reuse_revoke;
97			debug_info!(?user_id, ?device_id, revoke, "refresh token reused after rotation");
98
99			if revoke {
100				services
101					.users
102					.remove_device(&user_id, &device_id)
103					.await;
104			}
105
106			Err(Error::BadRequest(
107				ErrorKind::UnknownToken(UnknownTokenErrorData { soft_logout: !revoke }),
108				"Refresh token has already been used.",
109			))
110		},
111
112		| RefreshToken::Unknown => Err!(Request(Forbidden("Refresh token is unrecognized."))),
113	}
114}