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                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        // Lookup the same client by id
90        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        // Find the same client by client id
99        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        // Lookup a non-existing grant
108        let grant = repo
109            .oauth2_authorization_grant()
110            .lookup(Ulid::nil())
111            .await
112            .unwrap();
113        assert_eq!(grant, None);
114
115        // Find a non-existing grant by code
116        let grant = repo
117            .oauth2_authorization_grant()
118            .find_by_code("code")
119            .await
120            .unwrap();
121        assert_eq!(grant, None);
122
123        // Create an authorization grant
124        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        // Lookup the same grant by id
147        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        // Find the same grant by code
156        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        // Create a user and a start a user session
165        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        // Lookup a non-existing session
177        let session = repo.oauth2_session().lookup(Ulid::nil()).await.unwrap();
178        assert_eq!(session, None);
179
180        // Create an OAuth session
181        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        // Mark the grant as fulfilled
194        let grant = repo
195            .oauth2_authorization_grant()
196            .fulfill(&clock, &session, grant)
197            .await
198            .unwrap();
199        assert!(grant.is_fulfilled());
200
201        // Lookup the same session by id
202        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        // Mark the grant as exchanged
211        let grant = repo
212            .oauth2_authorization_grant()
213            .exchange(&clock, grant)
214            .await
215            .unwrap();
216        assert!(grant.is_exchanged());
217
218        // Lookup a non-existing token
219        let token = repo
220            .oauth2_access_token()
221            .lookup(Ulid::nil())
222            .await
223            .unwrap();
224        assert_eq!(token, None);
225
226        // Find a non-existing token
227        let token = repo
228            .oauth2_access_token()
229            .find_by_token("aabbcc")
230            .await
231            .unwrap();
232        assert_eq!(token, None);
233
234        // Create an access token
235        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        // Lookup the same token by id
248        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        // Find the same token by token
257        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        // Lookup a non-existing refresh token
266        let refresh_token = repo
267            .oauth2_refresh_token()
268            .lookup(Ulid::nil())
269            .await
270            .unwrap();
271        assert_eq!(refresh_token, None);
272
273        // Find a non-existing refresh token
274        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        // Create a refresh token
282        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        // Lookup the same refresh token by id
295        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        // Find the same refresh token by token
304        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        // XXX: we might want to create a new access token
317        clock.advance(Duration::try_minutes(-6).unwrap()); // Go back in time
318        assert!(access_token.is_valid(clock.now()));
319
320        // Create a new refresh token to be able to consume the old one
321        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        // Mark the access token as revoked
334        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        // Mark the refresh token as consumed
342        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        // Record the user-agent on the session
351        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        // Reload the session and check the user-agent
360        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        // Mark the session as finished
369        assert!(session.is_valid());
370        let session = repo.oauth2_session().finish(&clock, session).await.unwrap();
371        assert!(!session.is_valid());
372    }
373
374    /// Test the [`OAuth2SessionRepository::list`] and
375    /// [`OAuth2SessionRepository::count`] methods.
376    #[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        // Create two users and their corresponding browser sessions
383        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        // Create two clients
406        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        // Create two sessions for each user, one with each client
461        // We're moving the clock forward by 1 minute between each session to ensure
462        // we're getting consistent ordering in lists.
463        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        // We're also finishing two of the sessions
492        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        // First, list all the sessions
506        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        // Now filter for only one user
522        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        // Filter for only one client
536        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        // Filter for both a user and a client
550        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        // Filter for active sessions
565        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        // Filter for finished sessions
579        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        // Combine the finished filter with the user filter
593        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        // Combine the finished filter with the client filter
606        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        // Combine the active filter with the user filter
621        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        // Combine the active filter with the client filter
634        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        // Try the scope filter. We should get all sessions with the "openid" scope
649        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        // We should get all sessions with the "openid" and "email" scope
665        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        // Try combining the scope filter with the user filter
679        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        // Finish all sessions of a client in batch
692        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        // We should have 3 finished sessions
705        assert_eq!(
706            repo.oauth2_session()
707                .count(OAuth2SessionFilter::new().finished_only())
708                .await
709                .unwrap(),
710            3
711        );
712
713        // We should have 1 active sessions
714        assert_eq!(
715            repo.oauth2_session()
716                .count(OAuth2SessionFilter::new().active_only())
717                .await
718                .unwrap(),
719            1
720        );
721    }
722
723    /// Test the [`OAuth2DeviceCodeGrantRepository`] implementation
724    #[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        // Provision a client
731        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        // Provision a user
758        let user = repo
759            .user()
760            .add(&mut rng, &clock, "john".to_owned())
761            .await
762            .unwrap();
763
764        // Provision a browser session
765        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        // Create a device code grant
776        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        // Check that we can find the grant by ID
797        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        // Check that we can find the grant by device code
802        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        // Check that we can find the grant by user code
810        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's mark it as fulfilled
818        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        // Check that we can't mark it as rejected now
827        let res = repo
828            .oauth2_device_code_grant()
829            .reject(&clock, grant, &browser_session)
830            .await;
831        assert!(res.is_err());
832
833        // Look it up again
834        let grant = repo
835            .oauth2_device_code_grant()
836            .lookup(id)
837            .await
838            .unwrap()
839            .unwrap();
840
841        // We can't mark it as fulfilled again
842        let res = repo
843            .oauth2_device_code_grant()
844            .fulfill(&clock, grant, &browser_session)
845            .await;
846        assert!(res.is_err());
847
848        // Look it up again
849        let grant = repo
850            .oauth2_device_code_grant()
851            .lookup(id)
852            .await
853            .unwrap()
854            .unwrap();
855
856        // Create an OAuth 2.0 session
857        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        // We can mark it as exchanged
864        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        // We can't mark it as exchanged again
874        let res = repo
875            .oauth2_device_code_grant()
876            .exchange(&clock, grant, &session)
877            .await;
878        assert!(res.is_err());
879
880        // Do a new grant to reject it
881        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        // We can mark it as rejected
902        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        // We can't mark it as rejected again
911        let res = repo
912            .oauth2_device_code_grant()
913            .reject(&clock, grant, &browser_session)
914            .await;
915        assert!(res.is_err());
916
917        // Look it up again
918        let grant = repo
919            .oauth2_device_code_grant()
920            .lookup(id)
921            .await
922            .unwrap()
923            .unwrap();
924
925        // We can't mark it as fulfilled
926        let res = repo
927            .oauth2_device_code_grant()
928            .fulfill(&clock, grant, &browser_session)
929            .await;
930        assert!(res.is_err());
931
932        // Look it up again
933        let grant = repo
934            .oauth2_device_code_grant()
935            .lookup(id)
936            .await
937            .unwrap()
938            .unwrap();
939
940        // We can't mark it as exchanged
941        let res = repo
942            .oauth2_device_code_grant()
943            .exchange(&clock, grant, &session)
944            .await;
945        assert!(res.is_err());
946    }
947}