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			services.users.locked_check(&user_id).await?;
56
57			let refresh_token = Some(generate_refresh_token());
58			let (access_token, expires_in_ms) = services.users.generate_access_token(true);
59
60			services
61				.users
62				.set_access_token(
63					&user_id,
64					&device_id,
65					&access_token,
66					expires_in_ms,
67					refresh_token.as_deref(),
68				)
69				.await?;
70
71			debug_info!(?user_id, ?device_id, ?expires_in_ms, "refreshed their access_token",);
72
73			Ok(Response {
74				access_token,
75				refresh_token,
76				expires_in_ms,
77			})
78		},
79
80		| RefreshToken::Replayed { user_id, device_id, current, grace } if grace => {
81			services.users.locked_check(&user_id).await?;
82
83			// Benign double-submit: re-issue an access token for the unchanged
84			// refresh token rather than rotating it.
85			let (access_token, expires_in_ms) = services.users.generate_access_token(true);
86
87			services
88				.users
89				.set_access_token(&user_id, &device_id, &access_token, expires_in_ms, None)
90				.await?;
91
92			Ok(Response {
93				access_token,
94				refresh_token: Some(current),
95				expires_in_ms,
96			})
97		},
98
99		| RefreshToken::Replayed { user_id, device_id, .. } => {
100			let revoke = services.server.config.refresh_token_reuse_revoke;
101			debug_info!(?user_id, ?device_id, revoke, "refresh token reused after rotation");
102
103			if revoke {
104				services
105					.users
106					.remove_device(&user_id, &device_id)
107					.await;
108			}
109
110			Err(Error::BadRequest(
111				ErrorKind::UnknownToken(UnknownTokenErrorData { soft_logout: !revoke }),
112				"Refresh token has already been used.",
113			))
114		},
115
116		| RefreshToken::Unknown => Err!(Request(Forbidden("Refresh token is unrecognized."))),
117	}
118}