1use chrono::{DateTime, Duration, Utc};
2use secrecy::{ExposeSecret, SecretString};
3use sqlx::{Connection, PgConnection};
4use uuid::Uuid;
5
6use crate::domain::exercise_services::token::invalidate_cached_users;
7use crate::domain::oauth::errors::TokenGrantError;
8use crate::domain::oauth::helpers::split_and_validate_scopes;
9use crate::domain::oauth::pkce::verify_token_pkce;
10use headless_lms_utils::cache::Cache;
11
12use headless_lms_models::library::oauth::Digest;
13use headless_lms_models::library::oauth::tokens::token_digest_sha256;
14use headless_lms_models::oauth_access_token::TokenType;
15use headless_lms_models::oauth_auth_code::OAuthAuthCode;
16use headless_lms_models::oauth_client::OAuthClient;
17use headless_lms_models::oauth_device_codes::{DeviceCodeStatus, OAuthDeviceCode};
18use headless_lms_models::oauth_refresh_tokens::{
19 IssueTokensFromAuthCodeParams, OAuthRefreshTokens, RotateRefreshTokenParams,
20};
21
22use super::token_query::TokenGrant;
23
24pub struct TokenPair {
26 pub access_token: String,
27 pub refresh_token: String,
28 pub access_digest: Digest,
29 pub refresh_digest: Digest,
30}
31
32fn resolve_refresh_scopes(
36 original: &[String],
37 requested: Option<&str>,
38) -> Result<Vec<String>, TokenGrantError> {
39 match requested {
40 None => Ok(original.to_vec()),
41 Some(s) if s.split_whitespace().next().is_none() => Ok(original.to_vec()),
42 Some(s) => split_and_validate_scopes(s, original).map_err(|scope| {
43 TokenGrantError::InvalidScope(format!(
44 "requested scope \"{}\" was not included in the original grant",
45 scope
46 ))
47 }),
48 }
49}
50
51pub fn generate_token_pair(key: &SecretString) -> TokenPair {
53 let access_token = headless_lms_models::library::oauth::tokens::generate_access_token();
54 let refresh_token = headless_lms_models::library::oauth::tokens::generate_access_token();
55 TokenPair {
56 access_token: access_token.clone(),
57 refresh_token: refresh_token.clone(),
58 access_digest: token_digest_sha256(&access_token, key),
59 refresh_digest: token_digest_sha256(&refresh_token, key),
60 }
61}
62
63pub struct TokenGrantRequest<'a> {
64 pub grant: &'a TokenGrant,
65 pub client: &'a OAuthClient,
66 pub token_pair: TokenPair,
67 pub access_expires_at: DateTime<Utc>,
68 pub refresh_expires_at: DateTime<Utc>,
69 pub issued_token_type: TokenType,
70 pub dpop_jkt: Option<&'a str>,
71 pub token_hmac_key: &'a SecretString,
72}
73
74#[derive(Debug)]
75pub struct TokenGrantResult {
76 pub user_id: Uuid,
77 pub scopes: Vec<String>,
78 pub nonce: Option<String>,
79 pub access_expires_at: DateTime<Utc>,
80 pub issue_id_token: bool,
81}
82
83async fn revoke_family_on_refresh_token_reuse(
96 conn: &mut PgConnection,
97 cache: &Cache,
98 client: &OAuthClient,
99 refresh_token: &SecretString,
100 token_hmac_key: &SecretString,
101) -> Result<(), TokenGrantError> {
102 let presented = token_digest_sha256(refresh_token.expose_secret(), token_hmac_key);
103 let found = OAuthRefreshTokens::find_any_by_digest(conn, presented, client.id)
104 .await
105 .map_err(|e| TokenGrantError::ServerError(format!("{}", e)))?;
106 let Some(token) = found else {
107 return Ok(());
108 };
109 if !token.revoked {
110 return Ok(());
111 }
112
113 tracing::warn!(
114 user_id = %token.user_id,
115 client_id = %client.id,
116 "OAuth token: reuse of a revoked refresh token; revoking the whole (user, client) token family"
117 );
118 let revoked_access_digests = OAuthRefreshTokens::revoke_grant(conn, token.user_id, client.id)
119 .await
120 .map_err(|e| TokenGrantError::ServerError(format!("{}", e)))?;
121 invalidate_cached_users(cache, &revoked_access_digests, token_hmac_key).await;
122
123 Err(TokenGrantError::InvalidGrant(
124 "Given grant is invalid".to_string(),
125 ))
126}
127
128pub async fn process_token_grant(
129 conn: &mut PgConnection,
130 cache: &Cache,
131 request: TokenGrantRequest<'_>,
132) -> Result<TokenGrantResult, TokenGrantError> {
133 if let TokenGrant::DeviceCode { device_code } = request.grant {
137 return process_device_code_grant(conn, &request, device_code).await;
138 }
139
140 if let TokenGrant::RefreshToken { refresh_token, .. } = request.grant {
141 revoke_family_on_refresh_token_reuse(
142 conn,
143 cache,
144 request.client,
145 refresh_token,
146 request.token_hmac_key,
147 )
148 .await?;
149 }
150
151 let mut tx = conn
152 .begin()
153 .await
154 .map_err(|e| TokenGrantError::ServerError(format!("Failed to start transaction: {}", e)))?;
155
156 let result = match request.grant {
157 TokenGrant::AuthorizationCode {
158 code,
159 redirect_uri,
160 code_verifier,
161 } => {
162 let code_digest = token_digest_sha256(code.expose_secret(), request.token_hmac_key);
163 let code_row = if let Some(ref_uri) = redirect_uri {
165 OAuthAuthCode::consume_with_redirect_in_transaction(
166 &mut tx,
167 code_digest,
168 request.client.id,
169 ref_uri,
170 )
171 .await
172 .map_err(|e| {
173 tracing::warn!(
174 err = %e,
175 "OAuth token: auth code consume failed (redirect_uri check); possible causes: code already used, wrong redirect_uri, expired, or wrong client"
176 );
177 TokenGrantError::InvalidGrant("Given grant is invalid".to_string())
178 })?
179 } else {
180 OAuthAuthCode::consume_in_transaction(&mut tx, code_digest, request.client.id)
181 .await
182 .map_err(|e| {
183 tracing::warn!(
184 err = %e,
185 "OAuth token: auth code consume failed; possible causes: code already used, expired, or wrong client"
186 );
187 TokenGrantError::InvalidGrant("Given grant is invalid".to_string())
188 })?
189 };
190
191 verify_token_pkce(
193 request.client,
194 code_row.code_challenge.as_deref(),
195 code_row.code_challenge_method,
196 code_verifier.as_ref().map(|v| v.expose_secret()),
197 )
198 .map_err(|_| TokenGrantError::PkceVerificationFailed)?;
199
200 OAuthRefreshTokens::issue_tokens_from_auth_code_in_transaction(
201 &mut tx,
202 IssueTokensFromAuthCodeParams {
203 user_id: code_row.user_id,
204 client_id: code_row.client_id,
205 scopes: &code_row.scopes,
206 access_token_digest: &request.token_pair.access_digest,
207 refresh_token_digest: &request.token_pair.refresh_digest,
208 access_token_expires_at: request.access_expires_at,
209 refresh_token_expires_at: request.refresh_expires_at,
210 access_token_type: request.issued_token_type,
211 access_token_dpop_jkt: request.dpop_jkt,
212 refresh_token_dpop_jkt: request.dpop_jkt,
213 },
214 )
215 .await
216 .map_err(|e| TokenGrantError::ServerError(format!("{}", e)))?;
217
218 let has_openid = code_row.scopes.iter().any(|s| s == "openid");
220
221 Ok(TokenGrantResult {
222 user_id: code_row.user_id,
223 scopes: code_row.scopes,
224 nonce: code_row.nonce.clone(),
225 access_expires_at: request.access_expires_at,
226 issue_id_token: has_openid,
227 })
228 }
229 TokenGrant::RefreshToken {
230 refresh_token,
231 scope,
232 } => {
233 let presented =
234 token_digest_sha256(refresh_token.expose_secret(), request.token_hmac_key);
235 let old =
240 OAuthRefreshTokens::consume_in_transaction(&mut tx, presented, request.client.id)
241 .await
242 .map_err(|e| TokenGrantError::InvalidGrant(format!("{}", e)))?;
243
244 let effective_scopes = resolve_refresh_scopes(&old.scopes, scope.as_deref())?;
247
248 if let Some(expected_jkt) = old.dpop_jkt.as_deref() {
249 let presented_jkt = request.dpop_jkt.ok_or_else(|| {
250 TokenGrantError::InvalidClient(
251 "missing DPoP header for sender-constrained refresh".into(),
252 )
253 })?;
254 if presented_jkt != expected_jkt {
255 return Err(TokenGrantError::DpopMismatch);
256 }
257 }
258
259 let refresh_issue_type = if old.dpop_jkt.is_some() {
260 TokenType::DPoP
261 } else {
262 request.issued_token_type
263 };
264 let at_jkt = old.dpop_jkt.as_deref().or(request.dpop_jkt);
265 let refresh_jkt = old.dpop_jkt.as_deref().or(request.dpop_jkt);
266
267 OAuthRefreshTokens::complete_refresh_token_rotation_in_transaction(
268 &mut tx,
269 &old,
270 RotateRefreshTokenParams {
271 new_refresh_token_digest: &request.token_pair.refresh_digest,
272 new_access_token_digest: &request.token_pair.access_digest,
273 access_token_expires_at: request.access_expires_at,
274 refresh_token_expires_at: request.refresh_expires_at,
275 access_token_type: refresh_issue_type,
276 access_token_dpop_jkt: at_jkt,
277 refresh_token_dpop_jkt: refresh_jkt,
278 scopes: &effective_scopes,
279 },
280 )
281 .await
282 .map_err(|e| TokenGrantError::ServerError(format!("{}", e)))?;
283
284 Ok(TokenGrantResult {
285 user_id: old.user_id,
286 scopes: effective_scopes,
287 nonce: None,
288 access_expires_at: request.access_expires_at,
289 issue_id_token: false,
290 })
291 }
292 TokenGrant::DeviceCode { .. } => {
293 unreachable!("device_code grant is dispatched before the issuance transaction")
295 }
296 TokenGrant::Unknown => Err(TokenGrantError::UnsupportedGrantType),
297 };
298
299 match result {
300 Ok(res) => {
301 tx.commit().await.map_err(|e| {
302 TokenGrantError::ServerError(format!("Failed to commit transaction: {}", e))
303 })?;
304 Ok(res)
305 }
306 Err(e) => {
307 Err(e)
309 }
310 }
311}
312
313async fn process_device_code_grant(
322 conn: &mut PgConnection,
323 request: &TokenGrantRequest<'_>,
324 device_code: &SecretString,
325) -> Result<TokenGrantResult, TokenGrantError> {
326 let digest = token_digest_sha256(device_code.expose_secret(), request.token_hmac_key);
327
328 let poll = OAuthDeviceCode::record_poll(conn, &digest)
331 .await
332 .map_err(|e| TokenGrantError::InvalidGrant(format!("{}", e)))?;
333
334 if poll.client_id != request.client.id {
339 return Err(TokenGrantError::InvalidGrant(
340 "Given grant is invalid".to_string(),
341 ));
342 }
343
344 let now = Utc::now();
345
346 if poll.status == DeviceCodeStatus::Denied {
347 return Err(TokenGrantError::AccessDenied);
348 }
349 if poll.expires_at <= now {
350 return Err(TokenGrantError::ExpiredToken);
351 }
352 if poll.status == DeviceCodeStatus::Approved {
353 let mut tx = conn.begin().await.map_err(|e| {
354 TokenGrantError::ServerError(format!("Failed to start transaction: {}", e))
355 })?;
356
357 let device = OAuthDeviceCode::consume_approved_in_transaction(&mut tx, &digest)
359 .await
360 .map_err(|e| TokenGrantError::InvalidGrant(format!("{}", e)))?;
361
362 let user_id = device.user_id.ok_or_else(|| {
363 TokenGrantError::ServerError("approved device code missing user_id".into())
364 })?;
365
366 OAuthRefreshTokens::issue_tokens_from_auth_code_in_transaction(
367 &mut tx,
368 IssueTokensFromAuthCodeParams {
369 user_id,
370 client_id: device.client_id,
371 scopes: &device.scopes,
372 access_token_digest: &request.token_pair.access_digest,
373 refresh_token_digest: &request.token_pair.refresh_digest,
374 access_token_expires_at: request.access_expires_at,
375 refresh_token_expires_at: request.refresh_expires_at,
376 access_token_type: request.issued_token_type,
377 access_token_dpop_jkt: request.dpop_jkt,
378 refresh_token_dpop_jkt: request.dpop_jkt,
379 },
380 )
381 .await
382 .map_err(|e| TokenGrantError::ServerError(format!("{}", e)))?;
383
384 tx.commit().await.map_err(|e| {
385 TokenGrantError::ServerError(format!("Failed to commit transaction: {}", e))
386 })?;
387
388 return Ok(TokenGrantResult {
389 user_id,
390 scopes: device.scopes,
391 nonce: None,
392 access_expires_at: request.access_expires_at,
393 issue_id_token: false,
394 });
395 }
396
397 let polled_too_soon = poll
400 .previous_polled_at
401 .map(|prev| {
402 now.signed_duration_since(prev) < Duration::seconds(poll.interval_seconds as i64)
403 })
404 .unwrap_or(false);
405 if polled_too_soon {
406 Err(TokenGrantError::SlowDown)
407 } else {
408 Err(TokenGrantError::AuthorizationPending)
409 }
410}
411
412#[cfg(test)]
413mod tests {
414 use super::*;
415 use crate::test_helper::*;
416 use headless_lms_models::PKeyPolicy;
417 use headless_lms_models::library::oauth::pkce::PkceMethod;
418 use headless_lms_models::library::oauth::{
419 GrantTypeName, generate_access_token, generate_user_code,
420 };
421 use headless_lms_models::oauth_access_token::OAuthAccessToken;
422 use headless_lms_models::oauth_client::{
423 ApplicationType, NewClientParams, OAuthClient, TokenEndpointAuthMethod,
424 };
425 use headless_lms_models::oauth_device_codes::{NewDeviceCodeParams, OAuthDeviceCode};
426 use headless_lms_models::oauth_refresh_tokens::NewRefreshTokenParams;
427 use headless_lms_models::users;
428
429 fn hmac_key() -> SecretString {
430 SecretString::new("test-token-service-hmac-key".to_string().into())
431 }
432
433 fn test_cache() -> Cache {
436 Cache::new("redis://127.0.0.1:1").expect("cache")
437 }
438
439 fn build_request_with_pair<'a>(
442 grant: &'a TokenGrant,
443 client: &'a OAuthClient,
444 key: &'a SecretString,
445 token_pair: TokenPair,
446 ) -> TokenGrantRequest<'a> {
447 TokenGrantRequest {
448 grant,
449 client,
450 token_pair,
451 access_expires_at: Utc::now() + Duration::hours(1),
452 refresh_expires_at: Utc::now() + Duration::days(30),
453 issued_token_type: TokenType::Bearer,
454 dpop_jkt: None,
455 token_hmac_key: key,
456 }
457 }
458
459 async fn insert_refresh_token(
460 conn: &mut PgConnection,
461 user_id: Uuid,
462 client_id: Uuid,
463 ) -> String {
464 let plaintext = generate_access_token();
465 let digest = token_digest_sha256(&plaintext, &hmac_key());
466 OAuthRefreshTokens::insert(
467 conn,
468 NewRefreshTokenParams {
469 digest: &digest,
470 user_id,
471 client_id,
472 scopes: &["exercise-services".to_string()],
473 audience: None,
474 expires_at: Utc::now() + Duration::days(30),
475 rotated_from: None,
476 metadata: serde_json::Map::new(),
477 dpop_jkt: None,
478 },
479 )
480 .await
481 .unwrap();
482 plaintext
483 }
484
485 fn refresh_grant(plaintext: &str) -> TokenGrant {
486 TokenGrant::RefreshToken {
487 refresh_token: plaintext.into(),
488 scope: None,
489 }
490 }
491
492 fn refresh_grant_with_scope(plaintext: &str, scope: &str) -> TokenGrant {
495 TokenGrant::RefreshToken {
496 refresh_token: plaintext.into(),
497 scope: Some(scope.to_string()),
498 }
499 }
500
501 async fn insert_refresh_token_with_scopes(
504 conn: &mut PgConnection,
505 user_id: Uuid,
506 client_id: Uuid,
507 scopes: &[String],
508 ) -> String {
509 let plaintext = generate_access_token();
510 let digest = token_digest_sha256(&plaintext, &hmac_key());
511 OAuthRefreshTokens::insert(
512 conn,
513 NewRefreshTokenParams {
514 digest: &digest,
515 user_id,
516 client_id,
517 scopes,
518 audience: None,
519 expires_at: Utc::now() + Duration::days(30),
520 rotated_from: None,
521 metadata: serde_json::Map::new(),
522 dpop_jkt: None,
523 },
524 )
525 .await
526 .unwrap();
527 plaintext
528 }
529
530 async fn insert_device_client(conn: &mut PgConnection) -> OAuthClient {
531 let client_id = format!("cli-{}", &generate_access_token()[..12]);
532 OAuthClient::insert(
533 conn,
534 NewClientParams {
535 client_id: &client_id,
536 client_name: "Device flow token test client",
537 application_type: ApplicationType::Native,
538 token_endpoint_auth_method: TokenEndpointAuthMethod::None,
539 client_secret: None,
540 client_secret_expires_at: None,
541 redirect_uris: &["urn:ietf:wg:oauth:2.0:oob".to_string()],
542 post_logout_redirect_uris: None,
543 allowed_grant_types: &[GrantTypeName::DeviceCode, GrantTypeName::RefreshToken],
544 scopes: &["exercise-services".to_string()],
545 require_pkce: true,
546 pkce_methods_allowed: &[PkceMethod::S256],
547 allowed_origins: None,
548 bearer_allowed: true,
549 },
550 )
551 .await
552 .unwrap()
553 }
554
555 async fn insert_device_code(
558 conn: &mut PgConnection,
559 client_id: Uuid,
560 expires_at: DateTime<Utc>,
561 ) -> (String, String) {
562 let device_code = generate_access_token();
563 let user_code = generate_user_code();
564 let digest = token_digest_sha256(&device_code, &hmac_key());
565 OAuthDeviceCode::insert(
566 conn,
567 NewDeviceCodeParams {
568 device_code_digest: &digest,
569 user_code: &user_code,
570 client_id,
571 scopes: &["exercise-services".to_string()],
572 interval_seconds: 5,
573 expires_at,
574 metadata: serde_json::Map::new(),
575 },
576 )
577 .await
578 .unwrap();
579 (device_code, user_code)
580 }
581
582 fn build_request<'a>(
583 grant: &'a TokenGrant,
584 client: &'a OAuthClient,
585 key: &'a SecretString,
586 ) -> TokenGrantRequest<'a> {
587 TokenGrantRequest {
588 grant,
589 client,
590 token_pair: generate_token_pair(key),
591 access_expires_at: Utc::now() + Duration::hours(1),
592 refresh_expires_at: Utc::now() + Duration::days(30),
593 issued_token_type: TokenType::Bearer,
594 dpop_jkt: None,
595 token_hmac_key: key,
596 }
597 }
598
599 #[actix_web::test]
600 async fn device_grant_happy_path_issues_tokens_and_is_single_use() {
601 let mut conn = Conn::init().await;
602 let mut tx = conn.begin().await;
603 let key = hmac_key();
604
605 let user = users::insert(
606 tx.as_mut(),
607 PKeyPolicy::Generate,
608 "device-happy@example.com",
609 None,
610 None,
611 )
612 .await
613 .unwrap();
614 let client = insert_device_client(tx.as_mut()).await;
615 let (device_code, user_code) =
616 insert_device_code(tx.as_mut(), client.id, Utc::now() + Duration::minutes(15)).await;
617 OAuthDeviceCode::approve(tx.as_mut(), &user_code, user)
618 .await
619 .unwrap();
620
621 let grant = TokenGrant::DeviceCode {
622 device_code: device_code.clone().into(),
623 };
624 let result = process_token_grant(
625 tx.as_mut(),
626 &test_cache(),
627 build_request(&grant, &client, &key),
628 )
629 .await
630 .expect("device grant should succeed");
631 assert_eq!(result.user_id, user);
632 assert_eq!(result.scopes, vec!["exercise-services".to_string()]);
633 assert!(!result.issue_id_token);
634
635 let grant2 = TokenGrant::DeviceCode {
637 device_code: device_code.into(),
638 };
639 let err = process_token_grant(
640 tx.as_mut(),
641 &test_cache(),
642 build_request(&grant2, &client, &key),
643 )
644 .await
645 .expect_err("replay should fail");
646 assert!(matches!(err, TokenGrantError::InvalidGrant(_)));
647
648 tx.rollback().await;
649 }
650
651 #[actix_web::test]
652 async fn device_grant_pending_returns_authorization_pending() {
653 let mut conn = Conn::init().await;
654 let mut tx = conn.begin().await;
655 let key = hmac_key();
656
657 let client = insert_device_client(tx.as_mut()).await;
658 let (device_code, _user_code) =
659 insert_device_code(tx.as_mut(), client.id, Utc::now() + Duration::minutes(15)).await;
660
661 let grant = TokenGrant::DeviceCode {
662 device_code: device_code.into(),
663 };
664 let err = process_token_grant(
665 tx.as_mut(),
666 &test_cache(),
667 build_request(&grant, &client, &key),
668 )
669 .await
670 .expect_err("pending grant should not succeed");
671 assert!(matches!(err, TokenGrantError::AuthorizationPending));
672
673 tx.rollback().await;
674 }
675
676 #[actix_web::test]
677 async fn device_grant_denied_returns_access_denied() {
678 let mut conn = Conn::init().await;
679 let mut tx = conn.begin().await;
680 let key = hmac_key();
681
682 let client = insert_device_client(tx.as_mut()).await;
683 let (device_code, user_code) =
684 insert_device_code(tx.as_mut(), client.id, Utc::now() + Duration::minutes(15)).await;
685 OAuthDeviceCode::deny(tx.as_mut(), &user_code)
686 .await
687 .unwrap();
688
689 let grant = TokenGrant::DeviceCode {
690 device_code: device_code.into(),
691 };
692 let err = process_token_grant(
693 tx.as_mut(),
694 &test_cache(),
695 build_request(&grant, &client, &key),
696 )
697 .await
698 .expect_err("denied grant should not succeed");
699 assert!(matches!(err, TokenGrantError::AccessDenied));
700
701 tx.rollback().await;
702 }
703
704 #[actix_web::test]
705 async fn device_grant_expired_returns_expired_token() {
706 let mut conn = Conn::init().await;
707 let mut tx = conn.begin().await;
708 let key = hmac_key();
709
710 let client = insert_device_client(tx.as_mut()).await;
711 let (device_code, _user_code) =
713 insert_device_code(tx.as_mut(), client.id, Utc::now() - Duration::minutes(1)).await;
714
715 let grant = TokenGrant::DeviceCode {
716 device_code: device_code.into(),
717 };
718 let err = process_token_grant(
719 tx.as_mut(),
720 &test_cache(),
721 build_request(&grant, &client, &key),
722 )
723 .await
724 .expect_err("expired grant should not succeed");
725 assert!(matches!(err, TokenGrantError::ExpiredToken));
726
727 tx.rollback().await;
728 }
729
730 #[actix_web::test]
731 async fn device_grant_fast_polling_returns_slow_down() {
732 let mut conn = Conn::init().await;
733 let mut tx = conn.begin().await;
734 let key = hmac_key();
735
736 let client = insert_device_client(tx.as_mut()).await;
737 let (device_code, _user_code) =
738 insert_device_code(tx.as_mut(), client.id, Utc::now() + Duration::minutes(15)).await;
739
740 let grant1 = TokenGrant::DeviceCode {
742 device_code: device_code.clone().into(),
743 };
744 let first = process_token_grant(
745 tx.as_mut(),
746 &test_cache(),
747 build_request(&grant1, &client, &key),
748 )
749 .await
750 .expect_err("first poll should be pending");
751 assert!(matches!(first, TokenGrantError::AuthorizationPending));
752
753 let grant2 = TokenGrant::DeviceCode {
755 device_code: device_code.into(),
756 };
757 let second = process_token_grant(
758 tx.as_mut(),
759 &test_cache(),
760 build_request(&grant2, &client, &key),
761 )
762 .await
763 .expect_err("fast second poll should slow down");
764 assert!(matches!(second, TokenGrantError::SlowDown));
765
766 tx.rollback().await;
767 }
768
769 #[actix_web::test]
770 async fn device_grant_unknown_code_is_invalid_grant() {
771 let mut conn = Conn::init().await;
772 let mut tx = conn.begin().await;
773 let key = hmac_key();
774
775 let client = insert_device_client(tx.as_mut()).await;
776
777 let grant = TokenGrant::DeviceCode {
778 device_code: "does-not-exist".into(),
779 };
780 let err = process_token_grant(
781 tx.as_mut(),
782 &test_cache(),
783 build_request(&grant, &client, &key),
784 )
785 .await
786 .expect_err("unknown device code should fail");
787 assert!(matches!(err, TokenGrantError::InvalidGrant(_)));
788
789 tx.rollback().await;
790 }
791
792 #[actix_web::test]
793 async fn device_grant_rejects_mismatched_client() {
794 let mut conn = Conn::init().await;
795 let mut tx = conn.begin().await;
796 let key = hmac_key();
797
798 let user = users::insert(
799 tx.as_mut(),
800 PKeyPolicy::Generate,
801 "device-mismatch@example.com",
802 None,
803 None,
804 )
805 .await
806 .unwrap();
807 let client_a = insert_device_client(tx.as_mut()).await;
809 let client_b = insert_device_client(tx.as_mut()).await;
810
811 let (device_code, user_code) =
813 insert_device_code(tx.as_mut(), client_a.id, Utc::now() + Duration::minutes(15)).await;
814 OAuthDeviceCode::approve(tx.as_mut(), &user_code, user)
815 .await
816 .unwrap();
817
818 let grant_b = TokenGrant::DeviceCode {
820 device_code: device_code.clone().into(),
821 };
822 let err = process_token_grant(
823 tx.as_mut(),
824 &test_cache(),
825 build_request(&grant_b, &client_b, &key),
826 )
827 .await
828 .expect_err("cross-client device code redemption must fail");
829 assert!(matches!(err, TokenGrantError::InvalidGrant(_)));
830
831 let grant_a = TokenGrant::DeviceCode {
834 device_code: device_code.into(),
835 };
836 let ok = process_token_grant(
837 tx.as_mut(),
838 &test_cache(),
839 build_request(&grant_a, &client_a, &key),
840 )
841 .await
842 .expect("the client the code was issued to should still succeed");
843 assert_eq!(ok.user_id, user);
844
845 tx.rollback().await;
846 }
847
848 #[actix_web::test]
853 async fn refresh_reuse_of_a_rotated_token_revokes_the_whole_family() {
854 let mut conn = Conn::init().await;
855 let mut tx = conn.begin().await;
856 let key = hmac_key();
857
858 let user = users::insert(
859 tx.as_mut(),
860 PKeyPolicy::Generate,
861 "refresh-reuse@example.com",
862 None,
863 None,
864 )
865 .await
866 .unwrap();
867 let client = insert_device_client(tx.as_mut()).await;
868
869 let rt1 = insert_refresh_token(tx.as_mut(), user, client.id).await;
871 let pair2 = generate_token_pair(&key);
872 let rt2 = pair2.refresh_token.clone();
873 let at2 = pair2.access_token.clone();
874 let grant1 = refresh_grant(&rt1);
875 process_token_grant(
876 tx.as_mut(),
877 &test_cache(),
878 build_request_with_pair(&grant1, &client, &key, pair2),
879 )
880 .await
881 .expect("initial rotation should succeed");
882
883 assert!(
885 OAuthRefreshTokens::find_valid(tx.as_mut(), token_digest_sha256(&rt1, &key))
886 .await
887 .is_err(),
888 "the rotated token must no longer be valid"
889 );
890
891 let grant2 = refresh_grant(&rt1);
893 let err = process_token_grant(
894 tx.as_mut(),
895 &test_cache(),
896 build_request_with_pair(&grant2, &client, &key, generate_token_pair(&key)),
897 )
898 .await
899 .expect_err("reusing a rotated refresh token must fail");
900 assert!(matches!(err, TokenGrantError::InvalidGrant(_)));
901
902 assert!(
905 OAuthRefreshTokens::find_valid(tx.as_mut(), token_digest_sha256(&rt2, &key))
906 .await
907 .is_err(),
908 "RT2 must be dead after a reuse of its predecessor was detected"
909 );
910 assert!(
912 OAuthAccessToken::find_valid(tx.as_mut(), token_digest_sha256(&at2, &key))
913 .await
914 .is_err(),
915 "the access token from the rotated pair must be revoked with the family"
916 );
917
918 tx.rollback().await;
919 }
920
921 #[actix_web::test]
922 async fn refresh_with_subset_scope_narrows_the_issued_tokens() {
923 let mut conn = Conn::init().await;
924 let mut tx = conn.begin().await;
925 let key = hmac_key();
926
927 let user = users::insert(
928 tx.as_mut(),
929 PKeyPolicy::Generate,
930 "refresh-downscope@example.com",
931 None,
932 None,
933 )
934 .await
935 .unwrap();
936 let client = insert_device_client(tx.as_mut()).await;
937
938 let original_scopes = vec!["exercise-services".to_string(), "openid".to_string()];
939 let rt1 =
940 insert_refresh_token_with_scopes(tx.as_mut(), user, client.id, &original_scopes).await;
941
942 let pair2 = generate_token_pair(&key);
943 let new_refresh_plaintext = pair2.refresh_token.clone();
946 let new_access_plaintext = pair2.access_token.clone();
947 let grant = refresh_grant_with_scope(&rt1, "exercise-services");
948 let result = process_token_grant(
949 tx.as_mut(),
950 &test_cache(),
951 build_request_with_pair(&grant, &client, &key, pair2),
952 )
953 .await
954 .expect("requesting a subset of the original scope should be granted");
955
956 assert_eq!(result.scopes, vec!["exercise-services".to_string()]);
958
959 let new_refresh_row = OAuthRefreshTokens::find_valid(
964 tx.as_mut(),
965 token_digest_sha256(&new_refresh_plaintext, &key),
966 )
967 .await
968 .expect("new refresh token should be valid");
969 assert_eq!(
970 new_refresh_row.scopes,
971 vec!["exercise-services".to_string()]
972 );
973
974 let new_access_row = OAuthAccessToken::find_valid(
975 tx.as_mut(),
976 token_digest_sha256(&new_access_plaintext, &key),
977 )
978 .await
979 .expect("new access token should be valid");
980 assert_eq!(new_access_row.scopes, vec!["exercise-services".to_string()]);
981
982 tx.rollback().await;
983 }
984
985 #[actix_web::test]
986 async fn refresh_requesting_scope_outside_original_grant_is_rejected_without_consuming_token() {
987 let mut conn = Conn::init().await;
988 let mut tx = conn.begin().await;
989 let key = hmac_key();
990
991 let user = users::insert(
992 tx.as_mut(),
993 PKeyPolicy::Generate,
994 "refresh-widescope@example.com",
995 None,
996 None,
997 )
998 .await
999 .unwrap();
1000 let client = insert_device_client(tx.as_mut()).await;
1001
1002 let original_scopes = vec!["exercise-services".to_string()];
1003 let rt1 =
1004 insert_refresh_token_with_scopes(tx.as_mut(), user, client.id, &original_scopes).await;
1005
1006 let grant = refresh_grant_with_scope(&rt1, "exercise-services admin");
1008 let err = process_token_grant(
1009 tx.as_mut(),
1010 &test_cache(),
1011 build_request_with_pair(&grant, &client, &key, generate_token_pair(&key)),
1012 )
1013 .await
1014 .expect_err("a wider-than-original scope request must be rejected");
1015 assert!(matches!(err, TokenGrantError::InvalidScope(_)));
1016
1017 let rt1_digest = token_digest_sha256(&rt1, &key);
1020 let still_valid = OAuthRefreshTokens::find_valid(tx.as_mut(), rt1_digest).await;
1021 assert!(
1022 still_valid.is_ok(),
1023 "a rejected down-scope request must not consume/rotate the refresh token"
1024 );
1025
1026 let grant2 = refresh_grant(&rt1);
1028 process_token_grant(
1029 tx.as_mut(),
1030 &test_cache(),
1031 build_request_with_pair(&grant2, &client, &key, generate_token_pair(&key)),
1032 )
1033 .await
1034 .expect("the untouched refresh token should still work for a normal refresh");
1035
1036 tx.rollback().await;
1037 }
1038}