alpha
Login
or
Join now
foxwitch.net
/
tranquil-pds
forked from
tranquil.farm/tranquil-pds
Star
0
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
Our Personal Data Server from scratch!
Star
0
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
Overview
Issues
Pulls
Pipelines
JWT token refresh good error
author
lewis
date
7 months ago
(Dec 25, 2025, 6:23 PM +0200)
commit
d1dcf02c
d1dcf02c00417010ee37f34dd111cbddcda2b5fe
parent
bbc9d142
bbc9d1421678517c4547f1dc050c327c6b1f4462
+170
-42
4 changed files
Expand all
Collapse all
Unified
Split
src
api
error.rs
auth
extractor.rs
mod.rs
verify.rs
+1
src/api/error.rs
View file
Reviewed
···
192
192
crate::auth::TokenValidationError::AccountTakedown => Self::AccountTakedown,
193
193
crate::auth::TokenValidationError::KeyDecryptionFailed => Self::InternalError,
194
194
crate::auth::TokenValidationError::AuthenticationFailed => Self::AuthenticationFailed,
195
195
+
crate::auth::TokenValidationError::TokenExpired => Self::ExpiredToken,
195
196
}
196
197
}
197
198
}
+18
-2
src/auth/extractor.rs
View file
Reviewed
···
19
19
MissingToken,
20
20
InvalidFormat,
21
21
AuthenticationFailed,
22
22
+
TokenExpired,
22
23
AccountDeactivated,
23
24
AccountTakedown,
24
25
AdminRequired,
···
39
40
),
40
41
AuthError::AuthenticationFailed => (
41
42
StatusCode::UNAUTHORIZED,
42
42
-
"AuthenticationFailed",
43
43
-
"Invalid or expired token",
43
43
+
"InvalidToken",
44
44
+
"Token could not be verified",
45
45
+
),
46
46
+
AuthError::TokenExpired => (
47
47
+
StatusCode::UNAUTHORIZED,
48
48
+
"ExpiredToken",
49
49
+
"Token has expired",
44
50
),
45
51
AuthError::AccountDeactivated => (
46
52
StatusCode::UNAUTHORIZED,
···
174
180
Ok(user) => Ok(BearerAuth(user)),
175
181
Err(TokenValidationError::AccountDeactivated) => Err(AuthError::AccountDeactivated),
176
182
Err(TokenValidationError::AccountTakedown) => Err(AuthError::AccountTakedown),
183
183
+
Err(TokenValidationError::TokenExpired) => Err(AuthError::TokenExpired),
177
184
Err(_) => Err(AuthError::AuthenticationFailed),
178
185
}
179
186
} else {
···
181
188
Ok(user) => Ok(BearerAuth(user)),
182
189
Err(TokenValidationError::AccountDeactivated) => Err(AuthError::AccountDeactivated),
183
190
Err(TokenValidationError::AccountTakedown) => Err(AuthError::AccountTakedown),
191
191
+
Err(TokenValidationError::TokenExpired) => Err(AuthError::TokenExpired),
184
192
Err(_) => Err(AuthError::AuthenticationFailed),
185
193
}
186
194
}
···
224
232
{
225
233
Ok(user) => Ok(BearerAuthAllowDeactivated(user)),
226
234
Err(TokenValidationError::AccountTakedown) => Err(AuthError::AccountTakedown),
235
235
+
Err(TokenValidationError::TokenExpired) => Err(AuthError::TokenExpired),
227
236
Err(_) => Err(AuthError::AuthenticationFailed),
228
237
}
229
238
} else {
···
236
245
{
237
246
Ok(user) => Ok(BearerAuthAllowDeactivated(user)),
238
247
Err(TokenValidationError::AccountTakedown) => Err(AuthError::AccountTakedown),
248
248
+
Err(TokenValidationError::TokenExpired) => Err(AuthError::TokenExpired),
239
249
Err(_) => Err(AuthError::AuthenticationFailed),
240
250
}
241
251
}
···
284
294
Err(TokenValidationError::AccountTakedown) => {
285
295
return Err(AuthError::AccountTakedown);
286
296
}
297
297
+
Err(TokenValidationError::TokenExpired) => {
298
298
+
return Err(AuthError::TokenExpired);
299
299
+
}
287
300
Err(_) => return Err(AuthError::AuthenticationFailed),
288
301
}
289
302
} else {
···
294
307
}
295
308
Err(TokenValidationError::AccountTakedown) => {
296
309
return Err(AuthError::AccountTakedown);
310
310
+
}
311
311
+
Err(TokenValidationError::TokenExpired) => {
312
312
+
return Err(AuthError::TokenExpired);
297
313
}
298
314
Err(_) => return Err(AuthError::AuthenticationFailed),
299
315
}
+51
-40
src/auth/mod.rs
View file
Reviewed
···
28
28
create_service_token,
29
29
};
30
30
pub use verify::{
31
31
-
get_did_from_token, get_jti_from_token, verify_access_token, verify_refresh_token, verify_token,
31
31
+
TokenVerifyError, get_did_from_token, get_jti_from_token, verify_access_token,
32
32
+
verify_access_token_typed, verify_refresh_token, verify_token,
32
33
};
33
34
34
35
const KEY_CACHE_TTL_SECS: u64 = 300;
···
40
41
AccountTakedown,
41
42
KeyDecryptionFailed,
42
43
AuthenticationFailed,
44
44
+
TokenExpired,
43
45
}
44
46
45
47
impl fmt::Display for TokenValidationError {
···
49
51
Self::AccountTakedown => write!(f, "AccountTakedown"),
50
52
Self::KeyDecryptionFailed => write!(f, "KeyDecryptionFailed"),
51
53
Self::AuthenticationFailed => write!(f, "AuthenticationFailed"),
54
54
+
Self::TokenExpired => write!(f, "ExpiredToken"),
52
55
}
53
56
}
54
57
}
···
193
196
return Err(TokenValidationError::AccountTakedown);
194
197
}
195
198
196
196
-
if let Ok(token_data) = verify_access_token(token, &decrypted_key) {
197
197
-
let jti = &token_data.claims.jti;
198
198
-
let session_cache_key = format!("auth:session:{}:{}", did, jti);
199
199
-
let mut session_valid = false;
199
199
+
match verify_access_token_typed(token, &decrypted_key) {
200
200
+
Ok(token_data) => {
201
201
+
let jti = &token_data.claims.jti;
202
202
+
let session_cache_key = format!("auth:session:{}:{}", did, jti);
203
203
+
let mut session_valid = false;
200
204
201
201
-
if let Some(c) = cache {
202
202
-
if let Some(cached_value) = c.get(&session_cache_key).await {
203
203
-
session_valid = cached_value == "1";
204
204
-
crate::metrics::record_auth_cache_hit("session");
205
205
-
} else {
206
206
-
crate::metrics::record_auth_cache_miss("session");
205
205
+
if let Some(c) = cache {
206
206
+
if let Some(cached_value) = c.get(&session_cache_key).await {
207
207
+
session_valid = cached_value == "1";
208
208
+
crate::metrics::record_auth_cache_hit("session");
209
209
+
} else {
210
210
+
crate::metrics::record_auth_cache_miss("session");
211
211
+
}
207
212
}
208
208
-
}
209
213
210
210
-
if !session_valid {
211
211
-
let session_exists = sqlx::query_scalar!(
212
212
-
"SELECT 1 as one FROM session_tokens WHERE did = $1 AND access_jti = $2 AND access_expires_at > NOW()",
213
213
-
did,
214
214
-
jti
215
215
-
)
216
216
-
.fetch_optional(db)
217
217
-
.await
218
218
-
.ok()
219
219
-
.flatten();
214
214
+
if !session_valid {
215
215
+
let session_exists = sqlx::query_scalar!(
216
216
+
"SELECT 1 as one FROM session_tokens WHERE did = $1 AND access_jti = $2 AND access_expires_at > NOW()",
217
217
+
did,
218
218
+
jti
219
219
+
)
220
220
+
.fetch_optional(db)
221
221
+
.await
222
222
+
.ok()
223
223
+
.flatten();
220
224
221
221
-
session_valid = session_exists.is_some();
225
225
+
session_valid = session_exists.is_some();
226
226
+
227
227
+
if session_valid && let Some(c) = cache {
228
228
+
let _ = c
229
229
+
.set(
230
230
+
&session_cache_key,
231
231
+
"1",
232
232
+
Duration::from_secs(SESSION_CACHE_TTL_SECS),
233
233
+
)
234
234
+
.await;
235
235
+
}
236
236
+
}
222
237
223
223
-
if session_valid && let Some(c) = cache {
224
224
-
let _ = c
225
225
-
.set(
226
226
-
&session_cache_key,
227
227
-
"1",
228
228
-
Duration::from_secs(SESSION_CACHE_TTL_SECS),
229
229
-
)
230
230
-
.await;
238
238
+
if session_valid {
239
239
+
return Ok(AuthenticatedUser {
240
240
+
did: did.clone(),
241
241
+
key_bytes: Some(decrypted_key),
242
242
+
is_oauth: false,
243
243
+
is_admin,
244
244
+
scope: None,
245
245
+
});
231
246
}
232
247
}
233
233
-
234
234
-
if session_valid {
235
235
-
return Ok(AuthenticatedUser {
236
236
-
did: did.clone(),
237
237
-
key_bytes: Some(decrypted_key),
238
238
-
is_oauth: false,
239
239
-
is_admin,
240
240
-
scope: None,
241
241
-
});
248
248
+
Err(verify::TokenVerifyError::Expired) => {
249
249
+
return Err(TokenValidationError::TokenExpired);
242
250
}
251
251
+
Err(verify::TokenVerifyError::Invalid) => {}
243
252
}
244
253
}
245
254
}
···
283
292
is_admin: oauth_token.is_admin,
284
293
scope: oauth_info.scope,
285
294
});
295
295
+
} else {
296
296
+
return Err(TokenValidationError::TokenExpired);
286
297
}
287
298
}
288
299
+100
src/auth/verify.rs
View file
Reviewed
···
10
10
use hmac::{Hmac, Mac};
11
11
use k256::ecdsa::{Signature, SigningKey, VerifyingKey, signature::Verifier};
12
12
use sha2::Sha256;
13
13
+
use std::fmt;
13
14
use subtle::ConstantTimeEq;
14
15
15
16
type HmacSha256 = Hmac<Sha256>;
17
17
+
18
18
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19
19
+
pub enum TokenVerifyError {
20
20
+
Expired,
21
21
+
Invalid,
22
22
+
}
23
23
+
24
24
+
impl fmt::Display for TokenVerifyError {
25
25
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26
26
+
match self {
27
27
+
Self::Expired => write!(f, "Token expired"),
28
28
+
Self::Invalid => write!(f, "Token invalid"),
29
29
+
}
30
30
+
}
31
31
+
}
32
32
+
33
33
+
impl std::error::Error for TokenVerifyError {}
16
34
17
35
pub fn get_did_from_token(token: &str) -> Result<String, String> {
18
36
let parts: Vec<&str> = token.split('.').collect();
···
228
246
let token_scope = claims.scope.as_deref().unwrap_or("");
229
247
if !scopes.contains(&token_scope) {
230
248
return Err(anyhow!("Invalid token scope: {}", token_scope));
249
249
+
}
250
250
+
}
251
251
+
252
252
+
Ok(TokenData { claims })
253
253
+
}
254
254
+
255
255
+
pub fn verify_access_token_typed(
256
256
+
token: &str,
257
257
+
key_bytes: &[u8],
258
258
+
) -> Result<TokenData<Claims>, TokenVerifyError> {
259
259
+
verify_token_typed_internal(
260
260
+
token,
261
261
+
key_bytes,
262
262
+
Some(TOKEN_TYPE_ACCESS),
263
263
+
Some(&[SCOPE_ACCESS, SCOPE_APP_PASS, SCOPE_APP_PASS_PRIVILEGED]),
264
264
+
)
265
265
+
}
266
266
+
267
267
+
fn verify_token_typed_internal(
268
268
+
token: &str,
269
269
+
key_bytes: &[u8],
270
270
+
expected_typ: Option<&str>,
271
271
+
allowed_scopes: Option<&[&str]>,
272
272
+
) -> Result<TokenData<Claims>, TokenVerifyError> {
273
273
+
let parts: Vec<&str> = token.split('.').collect();
274
274
+
if parts.len() != 3 {
275
275
+
return Err(TokenVerifyError::Invalid);
276
276
+
}
277
277
+
278
278
+
let header_b64 = parts[0];
279
279
+
let claims_b64 = parts[1];
280
280
+
let signature_b64 = parts[2];
281
281
+
282
282
+
let Ok(header_bytes) = URL_SAFE_NO_PAD.decode(header_b64) else {
283
283
+
return Err(TokenVerifyError::Invalid);
284
284
+
};
285
285
+
286
286
+
let Ok(header) = serde_json::from_slice::<Header>(&header_bytes) else {
287
287
+
return Err(TokenVerifyError::Invalid);
288
288
+
};
289
289
+
290
290
+
if let Some(expected) = expected_typ
291
291
+
&& header.typ != expected
292
292
+
{
293
293
+
return Err(TokenVerifyError::Invalid);
294
294
+
}
295
295
+
296
296
+
let Ok(signature_bytes) = URL_SAFE_NO_PAD.decode(signature_b64) else {
297
297
+
return Err(TokenVerifyError::Invalid);
298
298
+
};
299
299
+
300
300
+
let Ok(signature) = Signature::from_slice(&signature_bytes) else {
301
301
+
return Err(TokenVerifyError::Invalid);
302
302
+
};
303
303
+
304
304
+
let Ok(signing_key) = SigningKey::from_slice(key_bytes) else {
305
305
+
return Err(TokenVerifyError::Invalid);
306
306
+
};
307
307
+
let verifying_key = VerifyingKey::from(&signing_key);
308
308
+
309
309
+
let message = format!("{}.{}", header_b64, claims_b64);
310
310
+
if verifying_key.verify(message.as_bytes(), &signature).is_err() {
311
311
+
return Err(TokenVerifyError::Invalid);
312
312
+
}
313
313
+
314
314
+
let Ok(claims_bytes) = URL_SAFE_NO_PAD.decode(claims_b64) else {
315
315
+
return Err(TokenVerifyError::Invalid);
316
316
+
};
317
317
+
318
318
+
let Ok(claims) = serde_json::from_slice::<Claims>(&claims_bytes) else {
319
319
+
return Err(TokenVerifyError::Invalid);
320
320
+
};
321
321
+
322
322
+
let now = Utc::now().timestamp() as usize;
323
323
+
if claims.exp < now {
324
324
+
return Err(TokenVerifyError::Expired);
325
325
+
}
326
326
+
327
327
+
if let Some(scopes) = allowed_scopes {
328
328
+
let token_scope = claims.scope.as_deref().unwrap_or("");
329
329
+
if !scopes.contains(&token_scope) {
330
330
+
return Err(TokenVerifyError::Invalid);
231
331
}
232
332
}
233
333