mas_storage_pg/oauth2/
mod.rs

1// Copyright 2024 New Vector Ltd.
2// Copyright 2021-2024 The Matrix.org Foundation C.I.C.
3//
4// SPDX-License-Identifier: AGPL-3.0-only
5// Please see LICENSE in the repository root for full details.
6
7//! A module containing the PostgreSQL implementations of the OAuth2-related
8//! repositories
9
10mod 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        // Lookup a non-existing client
51        let client = repo.oauth2_client().lookup(Ulid::nil()).await.unwrap();
52        assert_eq!(client, None);
53
54        // Find a non-existing client by client id
55        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        // Create a client
63        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                vec![GrantType::AuthorizationCode],
72                Some("Test client".to_owned()),
73                Some("https://example.com/logo.png".parse().unwrap()),
74                Some("https://example.com/".parse().unwrap()),
75                Some("https://example.com/policy".parse().unwrap()),
76                Some("https://example.com/tos".parse().unwrap()),
77                Some("https://example.com/jwks.json".parse().unwrap()),
78                None,
79                None,
80                None,
81                None,
82                None,
83                Some("https://example.com/login".parse().unwrap()),
84            )
85            .await
86            .unwrap();
87
88        // Lookup the same client by id
89        let client_lookup = repo
90            .oauth2_client()
91            .lookup(client.id)
92            .await
93            .unwrap()
94            .expect("client not found");
95        assert_eq!(client, client_lookup);
96
97        // Find the same client by client id
98        let client_lookup = repo
99            .oauth2_client()
100            .find_by_client_id(&client.client_id)
101            .await
102            .unwrap()
103            .expect("client not found");
104        assert_eq!(client, client_lookup);
105
106        // Lookup a non-existing grant
107        let grant = repo
108            .oauth2_authorization_grant()
109            .lookup(Ulid::nil())
110            .await
111            .unwrap();
112        assert_eq!(grant, None);
113
114        // Find a non-existing grant by code
115        let grant = repo
116            .oauth2_authorization_grant()
117            .find_by_code("code")
118            .await
119            .unwrap();
120        assert_eq!(grant, None);
121
122        // Create an authorization grant
123        let grant = repo
124            .oauth2_authorization_grant()
125            .add(
126                &mut rng,
127                &clock,
128                &client,
129                "https://example.com/redirect".parse().unwrap(),
130                Scope::from_iter([OPENID]),
131                Some(AuthorizationCode {
132                    code: "code".to_owned(),
133                    pkce: None,
134                }),
135                Some("state".to_owned()),
136                Some("nonce".to_owned()),
137                None,
138                ResponseMode::Query,
139                true,
140                false,
141                None,
142            )
143            .await
144            .unwrap();
145        assert!(grant.is_pending());
146
147        // Lookup the same grant by id
148        let grant_lookup = repo
149            .oauth2_authorization_grant()
150            .lookup(grant.id)
151            .await
152            .unwrap()
153            .expect("grant not found");
154        assert_eq!(grant, grant_lookup);
155
156        // Find the same grant by code
157        let grant_lookup = repo
158            .oauth2_authorization_grant()
159            .find_by_code("code")
160            .await
161            .unwrap()
162            .expect("grant not found");
163        assert_eq!(grant, grant_lookup);
164
165        // Create a user and a start a user session
166        let user = repo
167            .user()
168            .add(&mut rng, &clock, "john".to_owned())
169            .await
170            .unwrap();
171        let user_session = repo
172            .browser_session()
173            .add(&mut rng, &clock, &user, None)
174            .await
175            .unwrap();
176
177        // Lookup the consent the user gave to the client
178        let consent = repo
179            .oauth2_client()
180            .get_consent_for_user(&client, &user)
181            .await
182            .unwrap();
183        assert!(consent.is_empty());
184
185        // Give consent to the client
186        let scope = Scope::from_iter([OPENID]);
187        repo.oauth2_client()
188            .give_consent_for_user(&mut rng, &clock, &client, &user, &scope)
189            .await
190            .unwrap();
191
192        // Lookup the consent the user gave to the client
193        let consent = repo
194            .oauth2_client()
195            .get_consent_for_user(&client, &user)
196            .await
197            .unwrap();
198        assert_eq!(scope, consent);
199
200        // Lookup a non-existing session
201        let session = repo.oauth2_session().lookup(Ulid::nil()).await.unwrap();
202        assert_eq!(session, None);
203
204        // Create an OAuth session
205        let session = repo
206            .oauth2_session()
207            .add_from_browser_session(
208                &mut rng,
209                &clock,
210                &client,
211                &user_session,
212                grant.scope.clone(),
213            )
214            .await
215            .unwrap();
216
217        // Mark the grant as fulfilled
218        let grant = repo
219            .oauth2_authorization_grant()
220            .fulfill(&clock, &session, grant)
221            .await
222            .unwrap();
223        assert!(grant.is_fulfilled());
224
225        // Lookup the same session by id
226        let session_lookup = repo
227            .oauth2_session()
228            .lookup(session.id)
229            .await
230            .unwrap()
231            .expect("session not found");
232        assert_eq!(session, session_lookup);
233
234        // Mark the grant as exchanged
235        let grant = repo
236            .oauth2_authorization_grant()
237            .exchange(&clock, grant)
238            .await
239            .unwrap();
240        assert!(grant.is_exchanged());
241
242        // Lookup a non-existing token
243        let token = repo
244            .oauth2_access_token()
245            .lookup(Ulid::nil())
246            .await
247            .unwrap();
248        assert_eq!(token, None);
249
250        // Find a non-existing token
251        let token = repo
252            .oauth2_access_token()
253            .find_by_token("aabbcc")
254            .await
255            .unwrap();
256        assert_eq!(token, None);
257
258        // Create an access token
259        let access_token = repo
260            .oauth2_access_token()
261            .add(
262                &mut rng,
263                &clock,
264                &session,
265                "aabbcc".to_owned(),
266                Some(Duration::try_minutes(5).unwrap()),
267            )
268            .await
269            .unwrap();
270
271        // Lookup the same token by id
272        let access_token_lookup = repo
273            .oauth2_access_token()
274            .lookup(access_token.id)
275            .await
276            .unwrap()
277            .expect("token not found");
278        assert_eq!(access_token, access_token_lookup);
279
280        // Find the same token by token
281        let access_token_lookup = repo
282            .oauth2_access_token()
283            .find_by_token("aabbcc")
284            .await
285            .unwrap()
286            .expect("token not found");
287        assert_eq!(access_token, access_token_lookup);
288
289        // Lookup a non-existing refresh token
290        let refresh_token = repo
291            .oauth2_refresh_token()
292            .lookup(Ulid::nil())
293            .await
294            .unwrap();
295        assert_eq!(refresh_token, None);
296
297        // Find a non-existing refresh token
298        let refresh_token = repo
299            .oauth2_refresh_token()
300            .find_by_token("aabbcc")
301            .await
302            .unwrap();
303        assert_eq!(refresh_token, None);
304
305        // Create a refresh token
306        let refresh_token = repo
307            .oauth2_refresh_token()
308            .add(
309                &mut rng,
310                &clock,
311                &session,
312                &access_token,
313                "aabbcc".to_owned(),
314            )
315            .await
316            .unwrap();
317
318        // Lookup the same refresh token by id
319        let refresh_token_lookup = repo
320            .oauth2_refresh_token()
321            .lookup(refresh_token.id)
322            .await
323            .unwrap()
324            .expect("refresh token not found");
325        assert_eq!(refresh_token, refresh_token_lookup);
326
327        // Find the same refresh token by token
328        let refresh_token_lookup = repo
329            .oauth2_refresh_token()
330            .find_by_token("aabbcc")
331            .await
332            .unwrap()
333            .expect("refresh token not found");
334        assert_eq!(refresh_token, refresh_token_lookup);
335
336        assert!(access_token.is_valid(clock.now()));
337        clock.advance(Duration::try_minutes(6).unwrap());
338        assert!(!access_token.is_valid(clock.now()));
339
340        // XXX: we might want to create a new access token
341        clock.advance(Duration::try_minutes(-6).unwrap()); // Go back in time
342        assert!(access_token.is_valid(clock.now()));
343
344        // Create a new refresh token to be able to consume the old one
345        let new_refresh_token = repo
346            .oauth2_refresh_token()
347            .add(
348                &mut rng,
349                &clock,
350                &session,
351                &access_token,
352                "ddeeff".to_owned(),
353            )
354            .await
355            .unwrap();
356
357        // Mark the access token as revoked
358        let access_token = repo
359            .oauth2_access_token()
360            .revoke(&clock, access_token)
361            .await
362            .unwrap();
363        assert!(!access_token.is_valid(clock.now()));
364
365        // Mark the refresh token as consumed
366        assert!(refresh_token.is_valid());
367        let refresh_token = repo
368            .oauth2_refresh_token()
369            .consume(&clock, refresh_token, &new_refresh_token)
370            .await
371            .unwrap();
372        assert!(!refresh_token.is_valid());
373
374        // Record the user-agent on the session
375        assert!(session.user_agent.is_none());
376        let session = repo
377            .oauth2_session()
378            .record_user_agent(session, UserAgent::parse("Mozilla/5.0".to_owned()))
379            .await
380            .unwrap();
381        assert_eq!(session.user_agent.as_deref(), Some("Mozilla/5.0"));
382
383        // Reload the session and check the user-agent
384        let session = repo
385            .oauth2_session()
386            .lookup(session.id)
387            .await
388            .unwrap()
389            .expect("session not found");
390        assert_eq!(session.user_agent.as_deref(), Some("Mozilla/5.0"));
391
392        // Mark the session as finished
393        assert!(session.is_valid());
394        let session = repo.oauth2_session().finish(&clock, session).await.unwrap();
395        assert!(!session.is_valid());
396    }
397
398    /// Test the [`OAuth2SessionRepository::list`] and
399    /// [`OAuth2SessionRepository::count`] methods.
400    #[sqlx::test(migrator = "crate::MIGRATOR")]
401    async fn test_list_sessions(pool: PgPool) {
402        let mut rng = ChaChaRng::seed_from_u64(42);
403        let clock = MockClock::default();
404        let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
405
406        // Create two users and their corresponding browser sessions
407        let user1 = repo
408            .user()
409            .add(&mut rng, &clock, "alice".to_owned())
410            .await
411            .unwrap();
412        let user1_session = repo
413            .browser_session()
414            .add(&mut rng, &clock, &user1, None)
415            .await
416            .unwrap();
417
418        let user2 = repo
419            .user()
420            .add(&mut rng, &clock, "bob".to_owned())
421            .await
422            .unwrap();
423        let user2_session = repo
424            .browser_session()
425            .add(&mut rng, &clock, &user2, None)
426            .await
427            .unwrap();
428
429        // Create two clients
430        let client1 = repo
431            .oauth2_client()
432            .add(
433                &mut rng,
434                &clock,
435                vec!["https://first.example.com/redirect".parse().unwrap()],
436                None,
437                None,
438                vec![GrantType::AuthorizationCode],
439                Some("First client".to_owned()),
440                Some("https://first.example.com/logo.png".parse().unwrap()),
441                Some("https://first.example.com/".parse().unwrap()),
442                Some("https://first.example.com/policy".parse().unwrap()),
443                Some("https://first.example.com/tos".parse().unwrap()),
444                Some("https://first.example.com/jwks.json".parse().unwrap()),
445                None,
446                None,
447                None,
448                None,
449                None,
450                Some("https://first.example.com/login".parse().unwrap()),
451            )
452            .await
453            .unwrap();
454        let client2 = repo
455            .oauth2_client()
456            .add(
457                &mut rng,
458                &clock,
459                vec!["https://second.example.com/redirect".parse().unwrap()],
460                None,
461                None,
462                vec![GrantType::AuthorizationCode],
463                Some("Second client".to_owned()),
464                Some("https://second.example.com/logo.png".parse().unwrap()),
465                Some("https://second.example.com/".parse().unwrap()),
466                Some("https://second.example.com/policy".parse().unwrap()),
467                Some("https://second.example.com/tos".parse().unwrap()),
468                Some("https://second.example.com/jwks.json".parse().unwrap()),
469                None,
470                None,
471                None,
472                None,
473                None,
474                Some("https://second.example.com/login".parse().unwrap()),
475            )
476            .await
477            .unwrap();
478
479        let scope = Scope::from_iter([OPENID, EMAIL]);
480        let scope2 = Scope::from_iter([OPENID, PROFILE]);
481
482        // Create two sessions for each user, one with each client
483        // We're moving the clock forward by 1 minute between each session to ensure
484        // we're getting consistent ordering in lists.
485        let session11 = repo
486            .oauth2_session()
487            .add_from_browser_session(&mut rng, &clock, &client1, &user1_session, scope.clone())
488            .await
489            .unwrap();
490        clock.advance(Duration::try_minutes(1).unwrap());
491
492        let session12 = repo
493            .oauth2_session()
494            .add_from_browser_session(&mut rng, &clock, &client1, &user2_session, scope.clone())
495            .await
496            .unwrap();
497        clock.advance(Duration::try_minutes(1).unwrap());
498
499        let session21 = repo
500            .oauth2_session()
501            .add_from_browser_session(&mut rng, &clock, &client2, &user1_session, scope2.clone())
502            .await
503            .unwrap();
504        clock.advance(Duration::try_minutes(1).unwrap());
505
506        let session22 = repo
507            .oauth2_session()
508            .add_from_browser_session(&mut rng, &clock, &client2, &user2_session, scope2.clone())
509            .await
510            .unwrap();
511        clock.advance(Duration::try_minutes(1).unwrap());
512
513        // We're also finishing two of the sessions
514        let session11 = repo
515            .oauth2_session()
516            .finish(&clock, session11)
517            .await
518            .unwrap();
519        let session22 = repo
520            .oauth2_session()
521            .finish(&clock, session22)
522            .await
523            .unwrap();
524
525        let pagination = Pagination::first(10);
526
527        // First, list all the sessions
528        let filter = OAuth2SessionFilter::new().for_any_user();
529        let list = repo
530            .oauth2_session()
531            .list(filter, pagination)
532            .await
533            .unwrap();
534        assert!(!list.has_next_page);
535        assert_eq!(list.edges.len(), 4);
536        assert_eq!(list.edges[0], session11);
537        assert_eq!(list.edges[1], session12);
538        assert_eq!(list.edges[2], session21);
539        assert_eq!(list.edges[3], session22);
540
541        assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 4);
542
543        // Now filter for only one user
544        let filter = OAuth2SessionFilter::new().for_user(&user1);
545        let list = repo
546            .oauth2_session()
547            .list(filter, pagination)
548            .await
549            .unwrap();
550        assert!(!list.has_next_page);
551        assert_eq!(list.edges.len(), 2);
552        assert_eq!(list.edges[0], session11);
553        assert_eq!(list.edges[1], session21);
554
555        assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 2);
556
557        // Filter for only one client
558        let filter = OAuth2SessionFilter::new().for_client(&client1);
559        let list = repo
560            .oauth2_session()
561            .list(filter, pagination)
562            .await
563            .unwrap();
564        assert!(!list.has_next_page);
565        assert_eq!(list.edges.len(), 2);
566        assert_eq!(list.edges[0], session11);
567        assert_eq!(list.edges[1], session12);
568
569        assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 2);
570
571        // Filter for both a user and a client
572        let filter = OAuth2SessionFilter::new()
573            .for_user(&user2)
574            .for_client(&client2);
575        let list = repo
576            .oauth2_session()
577            .list(filter, pagination)
578            .await
579            .unwrap();
580        assert!(!list.has_next_page);
581        assert_eq!(list.edges.len(), 1);
582        assert_eq!(list.edges[0], session22);
583
584        assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 1);
585
586        // Filter for active sessions
587        let filter = OAuth2SessionFilter::new().active_only();
588        let list = repo
589            .oauth2_session()
590            .list(filter, pagination)
591            .await
592            .unwrap();
593        assert!(!list.has_next_page);
594        assert_eq!(list.edges.len(), 2);
595        assert_eq!(list.edges[0], session12);
596        assert_eq!(list.edges[1], session21);
597
598        assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 2);
599
600        // Filter for finished sessions
601        let filter = OAuth2SessionFilter::new().finished_only();
602        let list = repo
603            .oauth2_session()
604            .list(filter, pagination)
605            .await
606            .unwrap();
607        assert!(!list.has_next_page);
608        assert_eq!(list.edges.len(), 2);
609        assert_eq!(list.edges[0], session11);
610        assert_eq!(list.edges[1], session22);
611
612        assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 2);
613
614        // Combine the finished filter with the user filter
615        let filter = OAuth2SessionFilter::new().finished_only().for_user(&user2);
616        let list = repo
617            .oauth2_session()
618            .list(filter, pagination)
619            .await
620            .unwrap();
621        assert!(!list.has_next_page);
622        assert_eq!(list.edges.len(), 1);
623        assert_eq!(list.edges[0], session22);
624
625        assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 1);
626
627        // Combine the finished filter with the client filter
628        let filter = OAuth2SessionFilter::new()
629            .finished_only()
630            .for_client(&client2);
631        let list = repo
632            .oauth2_session()
633            .list(filter, pagination)
634            .await
635            .unwrap();
636        assert!(!list.has_next_page);
637        assert_eq!(list.edges.len(), 1);
638        assert_eq!(list.edges[0], session22);
639
640        assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 1);
641
642        // Combine the active filter with the user filter
643        let filter = OAuth2SessionFilter::new().active_only().for_user(&user2);
644        let list = repo
645            .oauth2_session()
646            .list(filter, pagination)
647            .await
648            .unwrap();
649        assert!(!list.has_next_page);
650        assert_eq!(list.edges.len(), 1);
651        assert_eq!(list.edges[0], session12);
652
653        assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 1);
654
655        // Combine the active filter with the client filter
656        let filter = OAuth2SessionFilter::new()
657            .active_only()
658            .for_client(&client2);
659        let list = repo
660            .oauth2_session()
661            .list(filter, pagination)
662            .await
663            .unwrap();
664        assert!(!list.has_next_page);
665        assert_eq!(list.edges.len(), 1);
666        assert_eq!(list.edges[0], session21);
667
668        assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 1);
669
670        // Try the scope filter. We should get all sessions with the "openid" scope
671        let scope = Scope::from_iter([OPENID]);
672        let filter = OAuth2SessionFilter::new().with_scope(&scope);
673        let list = repo
674            .oauth2_session()
675            .list(filter, pagination)
676            .await
677            .unwrap();
678        assert!(!list.has_next_page);
679        assert_eq!(list.edges.len(), 4);
680        assert_eq!(list.edges[0], session11);
681        assert_eq!(list.edges[1], session12);
682        assert_eq!(list.edges[2], session21);
683        assert_eq!(list.edges[3], session22);
684        assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 4);
685
686        // We should get all sessions with the "openid" and "email" scope
687        let scope = Scope::from_iter([OPENID, EMAIL]);
688        let filter = OAuth2SessionFilter::new().with_scope(&scope);
689        let list = repo
690            .oauth2_session()
691            .list(filter, pagination)
692            .await
693            .unwrap();
694        assert!(!list.has_next_page);
695        assert_eq!(list.edges.len(), 2);
696        assert_eq!(list.edges[0], session11);
697        assert_eq!(list.edges[1], session12);
698        assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 2);
699
700        // Try combining the scope filter with the user filter
701        let filter = OAuth2SessionFilter::new()
702            .with_scope(&scope)
703            .for_user(&user1);
704        let list = repo
705            .oauth2_session()
706            .list(filter, pagination)
707            .await
708            .unwrap();
709        assert_eq!(list.edges.len(), 1);
710        assert_eq!(list.edges[0], session11);
711        assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 1);
712
713        // Finish all sessions of a client in batch
714        let affected = repo
715            .oauth2_session()
716            .finish_bulk(
717                &clock,
718                OAuth2SessionFilter::new()
719                    .for_client(&client1)
720                    .active_only(),
721            )
722            .await
723            .unwrap();
724        assert_eq!(affected, 1);
725
726        // We should have 3 finished sessions
727        assert_eq!(
728            repo.oauth2_session()
729                .count(OAuth2SessionFilter::new().finished_only())
730                .await
731                .unwrap(),
732            3
733        );
734
735        // We should have 1 active sessions
736        assert_eq!(
737            repo.oauth2_session()
738                .count(OAuth2SessionFilter::new().active_only())
739                .await
740                .unwrap(),
741            1
742        );
743    }
744
745    /// Test the [`OAuth2DeviceCodeGrantRepository`] implementation
746    #[sqlx::test(migrator = "crate::MIGRATOR")]
747    async fn test_device_code_grant_repository(pool: PgPool) {
748        let mut rng = ChaChaRng::seed_from_u64(42);
749        let clock = MockClock::default();
750        let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
751
752        // Provision a client
753        let client = repo
754            .oauth2_client()
755            .add(
756                &mut rng,
757                &clock,
758                vec!["https://example.com/redirect".parse().unwrap()],
759                None,
760                None,
761                vec![GrantType::AuthorizationCode],
762                Some("Example".to_owned()),
763                Some("https://example.com/logo.png".parse().unwrap()),
764                Some("https://example.com/".parse().unwrap()),
765                Some("https://example.com/policy".parse().unwrap()),
766                Some("https://example.com/tos".parse().unwrap()),
767                Some("https://example.com/jwks.json".parse().unwrap()),
768                None,
769                None,
770                None,
771                None,
772                None,
773                Some("https://example.com/login".parse().unwrap()),
774            )
775            .await
776            .unwrap();
777
778        // Provision a user
779        let user = repo
780            .user()
781            .add(&mut rng, &clock, "john".to_owned())
782            .await
783            .unwrap();
784
785        // Provision a browser session
786        let browser_session = repo
787            .browser_session()
788            .add(&mut rng, &clock, &user, None)
789            .await
790            .unwrap();
791
792        let user_code = "usercode";
793        let device_code = "devicecode";
794        let scope = Scope::from_iter([OPENID, EMAIL]);
795
796        // Create a device code grant
797        let grant = repo
798            .oauth2_device_code_grant()
799            .add(
800                &mut rng,
801                &clock,
802                OAuth2DeviceCodeGrantParams {
803                    client: &client,
804                    scope: scope.clone(),
805                    device_code: device_code.to_owned(),
806                    user_code: user_code.to_owned(),
807                    expires_in: Duration::try_minutes(5).unwrap(),
808                    ip_address: None,
809                    user_agent: None,
810                },
811            )
812            .await
813            .unwrap();
814
815        assert!(grant.is_pending());
816
817        // Check that we can find the grant by ID
818        let id = grant.id;
819        let lookup = repo.oauth2_device_code_grant().lookup(id).await.unwrap();
820        assert_eq!(lookup.as_ref(), Some(&grant));
821
822        // Check that we can find the grant by device code
823        let lookup = repo
824            .oauth2_device_code_grant()
825            .find_by_device_code(device_code)
826            .await
827            .unwrap();
828        assert_eq!(lookup.as_ref(), Some(&grant));
829
830        // Check that we can find the grant by user code
831        let lookup = repo
832            .oauth2_device_code_grant()
833            .find_by_user_code(user_code)
834            .await
835            .unwrap();
836        assert_eq!(lookup.as_ref(), Some(&grant));
837
838        // Let's mark it as fulfilled
839        let grant = repo
840            .oauth2_device_code_grant()
841            .fulfill(&clock, grant, &browser_session)
842            .await
843            .unwrap();
844        assert!(!grant.is_pending());
845        assert!(grant.is_fulfilled());
846
847        // Check that we can't mark it as rejected now
848        let res = repo
849            .oauth2_device_code_grant()
850            .reject(&clock, grant, &browser_session)
851            .await;
852        assert!(res.is_err());
853
854        // Look it up again
855        let grant = repo
856            .oauth2_device_code_grant()
857            .lookup(id)
858            .await
859            .unwrap()
860            .unwrap();
861
862        // We can't mark it as fulfilled again
863        let res = repo
864            .oauth2_device_code_grant()
865            .fulfill(&clock, grant, &browser_session)
866            .await;
867        assert!(res.is_err());
868
869        // Look it up again
870        let grant = repo
871            .oauth2_device_code_grant()
872            .lookup(id)
873            .await
874            .unwrap()
875            .unwrap();
876
877        // Create an OAuth 2.0 session
878        let session = repo
879            .oauth2_session()
880            .add_from_browser_session(&mut rng, &clock, &client, &browser_session, scope.clone())
881            .await
882            .unwrap();
883
884        // We can mark it as exchanged
885        let grant = repo
886            .oauth2_device_code_grant()
887            .exchange(&clock, grant, &session)
888            .await
889            .unwrap();
890        assert!(!grant.is_pending());
891        assert!(!grant.is_fulfilled());
892        assert!(grant.is_exchanged());
893
894        // We can't mark it as exchanged again
895        let res = repo
896            .oauth2_device_code_grant()
897            .exchange(&clock, grant, &session)
898            .await;
899        assert!(res.is_err());
900
901        // Do a new grant to reject it
902        let grant = repo
903            .oauth2_device_code_grant()
904            .add(
905                &mut rng,
906                &clock,
907                OAuth2DeviceCodeGrantParams {
908                    client: &client,
909                    scope: scope.clone(),
910                    device_code: "second_devicecode".to_owned(),
911                    user_code: "second_usercode".to_owned(),
912                    expires_in: Duration::try_minutes(5).unwrap(),
913                    ip_address: None,
914                    user_agent: None,
915                },
916            )
917            .await
918            .unwrap();
919
920        let id = grant.id;
921
922        // We can mark it as rejected
923        let grant = repo
924            .oauth2_device_code_grant()
925            .reject(&clock, grant, &browser_session)
926            .await
927            .unwrap();
928        assert!(!grant.is_pending());
929        assert!(grant.is_rejected());
930
931        // We can't mark it as rejected again
932        let res = repo
933            .oauth2_device_code_grant()
934            .reject(&clock, grant, &browser_session)
935            .await;
936        assert!(res.is_err());
937
938        // Look it up again
939        let grant = repo
940            .oauth2_device_code_grant()
941            .lookup(id)
942            .await
943            .unwrap()
944            .unwrap();
945
946        // We can't mark it as fulfilled
947        let res = repo
948            .oauth2_device_code_grant()
949            .fulfill(&clock, grant, &browser_session)
950            .await;
951        assert!(res.is_err());
952
953        // Look it up again
954        let grant = repo
955            .oauth2_device_code_grant()
956            .lookup(id)
957            .await
958            .unwrap()
959            .unwrap();
960
961        // We can't mark it as exchanged
962        let res = repo
963            .oauth2_device_code_grant()
964            .exchange(&clock, grant, &session)
965            .await;
966        assert!(res.is_err());
967    }
968}