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