1mod access_token;
11mod authorization_grant;
12mod client;
13mod device_code_grant;
14mod refresh_token;
15mod session;
16
17pub use self::{
18 access_token::PgOAuth2AccessTokenRepository,
19 authorization_grant::PgOAuth2AuthorizationGrantRepository, client::PgOAuth2ClientRepository,
20 device_code_grant::PgOAuth2DeviceCodeGrantRepository,
21 refresh_token::PgOAuth2RefreshTokenRepository, session::PgOAuth2SessionRepository,
22};
23
24#[cfg(test)]
25mod tests {
26 use chrono::Duration;
27 use mas_data_model::{AuthorizationCode, UserAgent};
28 use mas_storage::{
29 Clock, Pagination,
30 clock::MockClock,
31 oauth2::{OAuth2DeviceCodeGrantParams, OAuth2SessionFilter, OAuth2SessionRepository},
32 };
33 use oauth2_types::{
34 requests::{GrantType, ResponseMode},
35 scope::{EMAIL, OPENID, PROFILE, Scope},
36 };
37 use rand::SeedableRng;
38 use rand_chacha::ChaChaRng;
39 use sqlx::PgPool;
40 use ulid::Ulid;
41
42 use crate::PgRepository;
43
44 #[sqlx::test(migrator = "crate::MIGRATOR")]
45 async fn test_repositories(pool: PgPool) {
46 let mut rng = ChaChaRng::seed_from_u64(42);
47 let clock = MockClock::default();
48 let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
49
50 let client = repo.oauth2_client().lookup(Ulid::nil()).await.unwrap();
52 assert_eq!(client, None);
53
54 let client = repo
56 .oauth2_client()
57 .find_by_client_id("some-client-id")
58 .await
59 .unwrap();
60 assert_eq!(client, None);
61
62 let client = repo
64 .oauth2_client()
65 .add(
66 &mut rng,
67 &clock,
68 vec!["https://example.com/redirect".parse().unwrap()],
69 None,
70 None,
71 None,
72 vec![GrantType::AuthorizationCode],
73 Some("Test client".to_owned()),
74 Some("https://example.com/logo.png".parse().unwrap()),
75 Some("https://example.com/".parse().unwrap()),
76 Some("https://example.com/policy".parse().unwrap()),
77 Some("https://example.com/tos".parse().unwrap()),
78 Some("https://example.com/jwks.json".parse().unwrap()),
79 None,
80 None,
81 None,
82 None,
83 None,
84 Some("https://example.com/login".parse().unwrap()),
85 )
86 .await
87 .unwrap();
88
89 let client_lookup = repo
91 .oauth2_client()
92 .lookup(client.id)
93 .await
94 .unwrap()
95 .expect("client not found");
96 assert_eq!(client, client_lookup);
97
98 let client_lookup = repo
100 .oauth2_client()
101 .find_by_client_id(&client.client_id)
102 .await
103 .unwrap()
104 .expect("client not found");
105 assert_eq!(client, client_lookup);
106
107 let grant = repo
109 .oauth2_authorization_grant()
110 .lookup(Ulid::nil())
111 .await
112 .unwrap();
113 assert_eq!(grant, None);
114
115 let grant = repo
117 .oauth2_authorization_grant()
118 .find_by_code("code")
119 .await
120 .unwrap();
121 assert_eq!(grant, None);
122
123 let grant = repo
125 .oauth2_authorization_grant()
126 .add(
127 &mut rng,
128 &clock,
129 &client,
130 "https://example.com/redirect".parse().unwrap(),
131 Scope::from_iter([OPENID]),
132 Some(AuthorizationCode {
133 code: "code".to_owned(),
134 pkce: None,
135 }),
136 Some("state".to_owned()),
137 Some("nonce".to_owned()),
138 ResponseMode::Query,
139 true,
140 None,
141 )
142 .await
143 .unwrap();
144 assert!(grant.is_pending());
145
146 let grant_lookup = repo
148 .oauth2_authorization_grant()
149 .lookup(grant.id)
150 .await
151 .unwrap()
152 .expect("grant not found");
153 assert_eq!(grant, grant_lookup);
154
155 let grant_lookup = repo
157 .oauth2_authorization_grant()
158 .find_by_code("code")
159 .await
160 .unwrap()
161 .expect("grant not found");
162 assert_eq!(grant, grant_lookup);
163
164 let user = repo
166 .user()
167 .add(&mut rng, &clock, "john".to_owned())
168 .await
169 .unwrap();
170 let user_session = repo
171 .browser_session()
172 .add(&mut rng, &clock, &user, None)
173 .await
174 .unwrap();
175
176 let session = repo.oauth2_session().lookup(Ulid::nil()).await.unwrap();
178 assert_eq!(session, None);
179
180 let session = repo
182 .oauth2_session()
183 .add_from_browser_session(
184 &mut rng,
185 &clock,
186 &client,
187 &user_session,
188 grant.scope.clone(),
189 )
190 .await
191 .unwrap();
192
193 let grant = repo
195 .oauth2_authorization_grant()
196 .fulfill(&clock, &session, grant)
197 .await
198 .unwrap();
199 assert!(grant.is_fulfilled());
200
201 let session_lookup = repo
203 .oauth2_session()
204 .lookup(session.id)
205 .await
206 .unwrap()
207 .expect("session not found");
208 assert_eq!(session, session_lookup);
209
210 let grant = repo
212 .oauth2_authorization_grant()
213 .exchange(&clock, grant)
214 .await
215 .unwrap();
216 assert!(grant.is_exchanged());
217
218 let token = repo
220 .oauth2_access_token()
221 .lookup(Ulid::nil())
222 .await
223 .unwrap();
224 assert_eq!(token, None);
225
226 let token = repo
228 .oauth2_access_token()
229 .find_by_token("aabbcc")
230 .await
231 .unwrap();
232 assert_eq!(token, None);
233
234 let access_token = repo
236 .oauth2_access_token()
237 .add(
238 &mut rng,
239 &clock,
240 &session,
241 "aabbcc".to_owned(),
242 Some(Duration::try_minutes(5).unwrap()),
243 )
244 .await
245 .unwrap();
246
247 let access_token_lookup = repo
249 .oauth2_access_token()
250 .lookup(access_token.id)
251 .await
252 .unwrap()
253 .expect("token not found");
254 assert_eq!(access_token, access_token_lookup);
255
256 let access_token_lookup = repo
258 .oauth2_access_token()
259 .find_by_token("aabbcc")
260 .await
261 .unwrap()
262 .expect("token not found");
263 assert_eq!(access_token, access_token_lookup);
264
265 let refresh_token = repo
267 .oauth2_refresh_token()
268 .lookup(Ulid::nil())
269 .await
270 .unwrap();
271 assert_eq!(refresh_token, None);
272
273 let refresh_token = repo
275 .oauth2_refresh_token()
276 .find_by_token("aabbcc")
277 .await
278 .unwrap();
279 assert_eq!(refresh_token, None);
280
281 let refresh_token = repo
283 .oauth2_refresh_token()
284 .add(
285 &mut rng,
286 &clock,
287 &session,
288 &access_token,
289 "aabbcc".to_owned(),
290 )
291 .await
292 .unwrap();
293
294 let refresh_token_lookup = repo
296 .oauth2_refresh_token()
297 .lookup(refresh_token.id)
298 .await
299 .unwrap()
300 .expect("refresh token not found");
301 assert_eq!(refresh_token, refresh_token_lookup);
302
303 let refresh_token_lookup = repo
305 .oauth2_refresh_token()
306 .find_by_token("aabbcc")
307 .await
308 .unwrap()
309 .expect("refresh token not found");
310 assert_eq!(refresh_token, refresh_token_lookup);
311
312 assert!(access_token.is_valid(clock.now()));
313 clock.advance(Duration::try_minutes(6).unwrap());
314 assert!(!access_token.is_valid(clock.now()));
315
316 clock.advance(Duration::try_minutes(-6).unwrap()); assert!(access_token.is_valid(clock.now()));
319
320 let new_refresh_token = repo
322 .oauth2_refresh_token()
323 .add(
324 &mut rng,
325 &clock,
326 &session,
327 &access_token,
328 "ddeeff".to_owned(),
329 )
330 .await
331 .unwrap();
332
333 let access_token = repo
335 .oauth2_access_token()
336 .revoke(&clock, access_token)
337 .await
338 .unwrap();
339 assert!(!access_token.is_valid(clock.now()));
340
341 assert!(refresh_token.is_valid());
343 let refresh_token = repo
344 .oauth2_refresh_token()
345 .consume(&clock, refresh_token, &new_refresh_token)
346 .await
347 .unwrap();
348 assert!(!refresh_token.is_valid());
349
350 assert!(session.user_agent.is_none());
352 let session = repo
353 .oauth2_session()
354 .record_user_agent(session, UserAgent::parse("Mozilla/5.0".to_owned()))
355 .await
356 .unwrap();
357 assert_eq!(session.user_agent.as_deref(), Some("Mozilla/5.0"));
358
359 let session = repo
361 .oauth2_session()
362 .lookup(session.id)
363 .await
364 .unwrap()
365 .expect("session not found");
366 assert_eq!(session.user_agent.as_deref(), Some("Mozilla/5.0"));
367
368 assert!(session.is_valid());
370 let session = repo.oauth2_session().finish(&clock, session).await.unwrap();
371 assert!(!session.is_valid());
372 }
373
374 #[sqlx::test(migrator = "crate::MIGRATOR")]
377 async fn test_list_sessions(pool: PgPool) {
378 let mut rng = ChaChaRng::seed_from_u64(42);
379 let clock = MockClock::default();
380 let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
381
382 let user1 = repo
384 .user()
385 .add(&mut rng, &clock, "alice".to_owned())
386 .await
387 .unwrap();
388 let user1_session = repo
389 .browser_session()
390 .add(&mut rng, &clock, &user1, None)
391 .await
392 .unwrap();
393
394 let user2 = repo
395 .user()
396 .add(&mut rng, &clock, "bob".to_owned())
397 .await
398 .unwrap();
399 let user2_session = repo
400 .browser_session()
401 .add(&mut rng, &clock, &user2, None)
402 .await
403 .unwrap();
404
405 let client1 = repo
407 .oauth2_client()
408 .add(
409 &mut rng,
410 &clock,
411 vec!["https://first.example.com/redirect".parse().unwrap()],
412 None,
413 None,
414 None,
415 vec![GrantType::AuthorizationCode],
416 Some("First client".to_owned()),
417 Some("https://first.example.com/logo.png".parse().unwrap()),
418 Some("https://first.example.com/".parse().unwrap()),
419 Some("https://first.example.com/policy".parse().unwrap()),
420 Some("https://first.example.com/tos".parse().unwrap()),
421 Some("https://first.example.com/jwks.json".parse().unwrap()),
422 None,
423 None,
424 None,
425 None,
426 None,
427 Some("https://first.example.com/login".parse().unwrap()),
428 )
429 .await
430 .unwrap();
431 let client2 = repo
432 .oauth2_client()
433 .add(
434 &mut rng,
435 &clock,
436 vec!["https://second.example.com/redirect".parse().unwrap()],
437 None,
438 None,
439 None,
440 vec![GrantType::AuthorizationCode],
441 Some("Second client".to_owned()),
442 Some("https://second.example.com/logo.png".parse().unwrap()),
443 Some("https://second.example.com/".parse().unwrap()),
444 Some("https://second.example.com/policy".parse().unwrap()),
445 Some("https://second.example.com/tos".parse().unwrap()),
446 Some("https://second.example.com/jwks.json".parse().unwrap()),
447 None,
448 None,
449 None,
450 None,
451 None,
452 Some("https://second.example.com/login".parse().unwrap()),
453 )
454 .await
455 .unwrap();
456
457 let scope = Scope::from_iter([OPENID, EMAIL]);
458 let scope2 = Scope::from_iter([OPENID, PROFILE]);
459
460 let session11 = repo
464 .oauth2_session()
465 .add_from_browser_session(&mut rng, &clock, &client1, &user1_session, scope.clone())
466 .await
467 .unwrap();
468 clock.advance(Duration::try_minutes(1).unwrap());
469
470 let session12 = repo
471 .oauth2_session()
472 .add_from_browser_session(&mut rng, &clock, &client1, &user2_session, scope.clone())
473 .await
474 .unwrap();
475 clock.advance(Duration::try_minutes(1).unwrap());
476
477 let session21 = repo
478 .oauth2_session()
479 .add_from_browser_session(&mut rng, &clock, &client2, &user1_session, scope2.clone())
480 .await
481 .unwrap();
482 clock.advance(Duration::try_minutes(1).unwrap());
483
484 let session22 = repo
485 .oauth2_session()
486 .add_from_browser_session(&mut rng, &clock, &client2, &user2_session, scope2.clone())
487 .await
488 .unwrap();
489 clock.advance(Duration::try_minutes(1).unwrap());
490
491 let session11 = repo
493 .oauth2_session()
494 .finish(&clock, session11)
495 .await
496 .unwrap();
497 let session22 = repo
498 .oauth2_session()
499 .finish(&clock, session22)
500 .await
501 .unwrap();
502
503 let pagination = Pagination::first(10);
504
505 let filter = OAuth2SessionFilter::new().for_any_user();
507 let list = repo
508 .oauth2_session()
509 .list(filter, pagination)
510 .await
511 .unwrap();
512 assert!(!list.has_next_page);
513 assert_eq!(list.edges.len(), 4);
514 assert_eq!(list.edges[0], session11);
515 assert_eq!(list.edges[1], session12);
516 assert_eq!(list.edges[2], session21);
517 assert_eq!(list.edges[3], session22);
518
519 assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 4);
520
521 let filter = OAuth2SessionFilter::new().for_user(&user1);
523 let list = repo
524 .oauth2_session()
525 .list(filter, pagination)
526 .await
527 .unwrap();
528 assert!(!list.has_next_page);
529 assert_eq!(list.edges.len(), 2);
530 assert_eq!(list.edges[0], session11);
531 assert_eq!(list.edges[1], session21);
532
533 assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 2);
534
535 let filter = OAuth2SessionFilter::new().for_client(&client1);
537 let list = repo
538 .oauth2_session()
539 .list(filter, pagination)
540 .await
541 .unwrap();
542 assert!(!list.has_next_page);
543 assert_eq!(list.edges.len(), 2);
544 assert_eq!(list.edges[0], session11);
545 assert_eq!(list.edges[1], session12);
546
547 assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 2);
548
549 let filter = OAuth2SessionFilter::new()
551 .for_user(&user2)
552 .for_client(&client2);
553 let list = repo
554 .oauth2_session()
555 .list(filter, pagination)
556 .await
557 .unwrap();
558 assert!(!list.has_next_page);
559 assert_eq!(list.edges.len(), 1);
560 assert_eq!(list.edges[0], session22);
561
562 assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 1);
563
564 let filter = OAuth2SessionFilter::new().active_only();
566 let list = repo
567 .oauth2_session()
568 .list(filter, pagination)
569 .await
570 .unwrap();
571 assert!(!list.has_next_page);
572 assert_eq!(list.edges.len(), 2);
573 assert_eq!(list.edges[0], session12);
574 assert_eq!(list.edges[1], session21);
575
576 assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 2);
577
578 let filter = OAuth2SessionFilter::new().finished_only();
580 let list = repo
581 .oauth2_session()
582 .list(filter, pagination)
583 .await
584 .unwrap();
585 assert!(!list.has_next_page);
586 assert_eq!(list.edges.len(), 2);
587 assert_eq!(list.edges[0], session11);
588 assert_eq!(list.edges[1], session22);
589
590 assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 2);
591
592 let filter = OAuth2SessionFilter::new().finished_only().for_user(&user2);
594 let list = repo
595 .oauth2_session()
596 .list(filter, pagination)
597 .await
598 .unwrap();
599 assert!(!list.has_next_page);
600 assert_eq!(list.edges.len(), 1);
601 assert_eq!(list.edges[0], session22);
602
603 assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 1);
604
605 let filter = OAuth2SessionFilter::new()
607 .finished_only()
608 .for_client(&client2);
609 let list = repo
610 .oauth2_session()
611 .list(filter, pagination)
612 .await
613 .unwrap();
614 assert!(!list.has_next_page);
615 assert_eq!(list.edges.len(), 1);
616 assert_eq!(list.edges[0], session22);
617
618 assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 1);
619
620 let filter = OAuth2SessionFilter::new().active_only().for_user(&user2);
622 let list = repo
623 .oauth2_session()
624 .list(filter, pagination)
625 .await
626 .unwrap();
627 assert!(!list.has_next_page);
628 assert_eq!(list.edges.len(), 1);
629 assert_eq!(list.edges[0], session12);
630
631 assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 1);
632
633 let filter = OAuth2SessionFilter::new()
635 .active_only()
636 .for_client(&client2);
637 let list = repo
638 .oauth2_session()
639 .list(filter, pagination)
640 .await
641 .unwrap();
642 assert!(!list.has_next_page);
643 assert_eq!(list.edges.len(), 1);
644 assert_eq!(list.edges[0], session21);
645
646 assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 1);
647
648 let scope = Scope::from_iter([OPENID]);
650 let filter = OAuth2SessionFilter::new().with_scope(&scope);
651 let list = repo
652 .oauth2_session()
653 .list(filter, pagination)
654 .await
655 .unwrap();
656 assert!(!list.has_next_page);
657 assert_eq!(list.edges.len(), 4);
658 assert_eq!(list.edges[0], session11);
659 assert_eq!(list.edges[1], session12);
660 assert_eq!(list.edges[2], session21);
661 assert_eq!(list.edges[3], session22);
662 assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 4);
663
664 let scope = Scope::from_iter([OPENID, EMAIL]);
666 let filter = OAuth2SessionFilter::new().with_scope(&scope);
667 let list = repo
668 .oauth2_session()
669 .list(filter, pagination)
670 .await
671 .unwrap();
672 assert!(!list.has_next_page);
673 assert_eq!(list.edges.len(), 2);
674 assert_eq!(list.edges[0], session11);
675 assert_eq!(list.edges[1], session12);
676 assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 2);
677
678 let filter = OAuth2SessionFilter::new()
680 .with_scope(&scope)
681 .for_user(&user1);
682 let list = repo
683 .oauth2_session()
684 .list(filter, pagination)
685 .await
686 .unwrap();
687 assert_eq!(list.edges.len(), 1);
688 assert_eq!(list.edges[0], session11);
689 assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 1);
690
691 let affected = repo
693 .oauth2_session()
694 .finish_bulk(
695 &clock,
696 OAuth2SessionFilter::new()
697 .for_client(&client1)
698 .active_only(),
699 )
700 .await
701 .unwrap();
702 assert_eq!(affected, 1);
703
704 assert_eq!(
706 repo.oauth2_session()
707 .count(OAuth2SessionFilter::new().finished_only())
708 .await
709 .unwrap(),
710 3
711 );
712
713 assert_eq!(
715 repo.oauth2_session()
716 .count(OAuth2SessionFilter::new().active_only())
717 .await
718 .unwrap(),
719 1
720 );
721 }
722
723 #[sqlx::test(migrator = "crate::MIGRATOR")]
725 async fn test_device_code_grant_repository(pool: PgPool) {
726 let mut rng = ChaChaRng::seed_from_u64(42);
727 let clock = MockClock::default();
728 let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
729
730 let client = repo
732 .oauth2_client()
733 .add(
734 &mut rng,
735 &clock,
736 vec!["https://example.com/redirect".parse().unwrap()],
737 None,
738 None,
739 None,
740 vec![GrantType::AuthorizationCode],
741 Some("Example".to_owned()),
742 Some("https://example.com/logo.png".parse().unwrap()),
743 Some("https://example.com/".parse().unwrap()),
744 Some("https://example.com/policy".parse().unwrap()),
745 Some("https://example.com/tos".parse().unwrap()),
746 Some("https://example.com/jwks.json".parse().unwrap()),
747 None,
748 None,
749 None,
750 None,
751 None,
752 Some("https://example.com/login".parse().unwrap()),
753 )
754 .await
755 .unwrap();
756
757 let user = repo
759 .user()
760 .add(&mut rng, &clock, "john".to_owned())
761 .await
762 .unwrap();
763
764 let browser_session = repo
766 .browser_session()
767 .add(&mut rng, &clock, &user, None)
768 .await
769 .unwrap();
770
771 let user_code = "usercode";
772 let device_code = "devicecode";
773 let scope = Scope::from_iter([OPENID, EMAIL]);
774
775 let grant = repo
777 .oauth2_device_code_grant()
778 .add(
779 &mut rng,
780 &clock,
781 OAuth2DeviceCodeGrantParams {
782 client: &client,
783 scope: scope.clone(),
784 device_code: device_code.to_owned(),
785 user_code: user_code.to_owned(),
786 expires_in: Duration::try_minutes(5).unwrap(),
787 ip_address: None,
788 user_agent: None,
789 },
790 )
791 .await
792 .unwrap();
793
794 assert!(grant.is_pending());
795
796 let id = grant.id;
798 let lookup = repo.oauth2_device_code_grant().lookup(id).await.unwrap();
799 assert_eq!(lookup.as_ref(), Some(&grant));
800
801 let lookup = repo
803 .oauth2_device_code_grant()
804 .find_by_device_code(device_code)
805 .await
806 .unwrap();
807 assert_eq!(lookup.as_ref(), Some(&grant));
808
809 let lookup = repo
811 .oauth2_device_code_grant()
812 .find_by_user_code(user_code)
813 .await
814 .unwrap();
815 assert_eq!(lookup.as_ref(), Some(&grant));
816
817 let grant = repo
819 .oauth2_device_code_grant()
820 .fulfill(&clock, grant, &browser_session)
821 .await
822 .unwrap();
823 assert!(!grant.is_pending());
824 assert!(grant.is_fulfilled());
825
826 let res = repo
828 .oauth2_device_code_grant()
829 .reject(&clock, grant, &browser_session)
830 .await;
831 assert!(res.is_err());
832
833 let grant = repo
835 .oauth2_device_code_grant()
836 .lookup(id)
837 .await
838 .unwrap()
839 .unwrap();
840
841 let res = repo
843 .oauth2_device_code_grant()
844 .fulfill(&clock, grant, &browser_session)
845 .await;
846 assert!(res.is_err());
847
848 let grant = repo
850 .oauth2_device_code_grant()
851 .lookup(id)
852 .await
853 .unwrap()
854 .unwrap();
855
856 let session = repo
858 .oauth2_session()
859 .add_from_browser_session(&mut rng, &clock, &client, &browser_session, scope.clone())
860 .await
861 .unwrap();
862
863 let grant = repo
865 .oauth2_device_code_grant()
866 .exchange(&clock, grant, &session)
867 .await
868 .unwrap();
869 assert!(!grant.is_pending());
870 assert!(!grant.is_fulfilled());
871 assert!(grant.is_exchanged());
872
873 let res = repo
875 .oauth2_device_code_grant()
876 .exchange(&clock, grant, &session)
877 .await;
878 assert!(res.is_err());
879
880 let grant = repo
882 .oauth2_device_code_grant()
883 .add(
884 &mut rng,
885 &clock,
886 OAuth2DeviceCodeGrantParams {
887 client: &client,
888 scope: scope.clone(),
889 device_code: "second_devicecode".to_owned(),
890 user_code: "second_usercode".to_owned(),
891 expires_in: Duration::try_minutes(5).unwrap(),
892 ip_address: None,
893 user_agent: None,
894 },
895 )
896 .await
897 .unwrap();
898
899 let id = grant.id;
900
901 let grant = repo
903 .oauth2_device_code_grant()
904 .reject(&clock, grant, &browser_session)
905 .await
906 .unwrap();
907 assert!(!grant.is_pending());
908 assert!(grant.is_rejected());
909
910 let res = repo
912 .oauth2_device_code_grant()
913 .reject(&clock, grant, &browser_session)
914 .await;
915 assert!(res.is_err());
916
917 let grant = repo
919 .oauth2_device_code_grant()
920 .lookup(id)
921 .await
922 .unwrap()
923 .unwrap();
924
925 let res = repo
927 .oauth2_device_code_grant()
928 .fulfill(&clock, grant, &browser_session)
929 .await;
930 assert!(res.is_err());
931
932 let grant = repo
934 .oauth2_device_code_grant()
935 .lookup(id)
936 .await
937 .unwrap()
938 .unwrap();
939
940 let res = repo
942 .oauth2_device_code_grant()
943 .exchange(&clock, grant, &session)
944 .await;
945 assert!(res.is_err());
946 }
947}