Skip to main content

mas_storage_pg/oauth2/
mod.rs

1// Copyright 2025, 2026 Element Creations Ltd.
2// Copyright 2024, 2025 New Vector Ltd.
3// Copyright 2021-2024 The Matrix.org Foundation C.I.C.
4//
5// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
6// Please see LICENSE files in the repository root for full details.
7
8//! A module containing the PostgreSQL implementations of the OAuth2-related
9//! repositories
10
11mod access_token;
12mod authorization_grant;
13mod client;
14mod device_code_grant;
15mod refresh_token;
16mod session;
17
18pub use self::{
19    access_token::PgOAuth2AccessTokenRepository,
20    authorization_grant::PgOAuth2AuthorizationGrantRepository, client::PgOAuth2ClientRepository,
21    device_code_grant::PgOAuth2DeviceCodeGrantRepository,
22    refresh_token::PgOAuth2RefreshTokenRepository, session::PgOAuth2SessionRepository,
23};
24
25#[cfg(test)]
26mod tests {
27    use chrono::Duration;
28    use mas_data_model::{AuthorizationCode, Clock, clock::MockClock};
29    use mas_iana::oauth::OAuthClientAuthenticationMethod;
30    use mas_storage::{
31        Pagination,
32        oauth2::{
33            OAuth2ClientFilter, OAuth2DeviceCodeGrantParams, OAuth2SessionFilter,
34            OAuth2SessionRepository,
35        },
36    };
37    use oauth2_types::{
38        requests::{GrantType, ResponseMode},
39        scope::{EMAIL, OPENID, PROFILE, Scope},
40    };
41    use rand::SeedableRng;
42    use rand_chacha::ChaChaRng;
43    use sqlx::PgPool;
44    use ulid::Ulid;
45
46    use crate::PgRepository;
47
48    #[sqlx::test(migrator = "crate::MIGRATOR")]
49    async fn test_repositories(pool: PgPool) {
50        let mut rng = ChaChaRng::seed_from_u64(42);
51        let clock = MockClock::default();
52        let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
53
54        // Lookup a non-existing client
55        let client = repo.oauth2_client().lookup(Ulid::nil()).await.unwrap();
56        assert_eq!(client, None);
57
58        // Find a non-existing client by client id
59        let client = repo
60            .oauth2_client()
61            .find_by_client_id("some-client-id")
62            .await
63            .unwrap();
64        assert_eq!(client, None);
65
66        // Create a client
67        let client = repo
68            .oauth2_client()
69            .add(
70                &mut rng,
71                &clock,
72                vec!["https://example.com/redirect".parse().unwrap()],
73                None,
74                None,
75                None,
76                vec![GrantType::AuthorizationCode],
77                Some("Test client".to_owned()),
78                Some("https://example.com/logo.png".parse().unwrap()),
79                Some("https://example.com/".parse().unwrap()),
80                Some("https://example.com/policy".parse().unwrap()),
81                Some("https://example.com/tos".parse().unwrap()),
82                Some("https://example.com/jwks.json".parse().unwrap()),
83                None,
84                None,
85                None,
86                None,
87                None,
88                Some("https://example.com/login".parse().unwrap()),
89            )
90            .await
91            .unwrap();
92
93        // Lookup the same client by id
94        let client_lookup = repo
95            .oauth2_client()
96            .lookup(client.id)
97            .await
98            .unwrap()
99            .expect("client not found");
100        assert_eq!(client, client_lookup);
101
102        // Find the same client by client id
103        let client_lookup = repo
104            .oauth2_client()
105            .find_by_client_id(&client.client_id)
106            .await
107            .unwrap()
108            .expect("client not found");
109        assert_eq!(client, client_lookup);
110
111        // Lookup a non-existing grant
112        let grant = repo
113            .oauth2_authorization_grant()
114            .lookup(Ulid::nil())
115            .await
116            .unwrap();
117        assert_eq!(grant, None);
118
119        // Find a non-existing grant by code
120        let grant = repo
121            .oauth2_authorization_grant()
122            .find_by_code("code")
123            .await
124            .unwrap();
125        assert_eq!(grant, None);
126
127        // Create an authorization grant
128        let raw_parameters = std::collections::BTreeMap::from([
129            ("client_id".to_owned(), "client".to_owned()),
130            ("foo".to_owned(), "bar".to_owned()),
131        ]);
132        let grant = repo
133            .oauth2_authorization_grant()
134            .add(
135                &mut rng,
136                &clock,
137                &client,
138                "https://example.com/redirect".parse().unwrap(),
139                Scope::from_iter([OPENID]),
140                Some(AuthorizationCode {
141                    code: "code".to_owned(),
142                    pkce: None,
143                }),
144                Some("state".to_owned()),
145                Some("nonce".to_owned()),
146                ResponseMode::Query,
147                true,
148                None,
149                None,
150                raw_parameters.clone(),
151            )
152            .await
153            .unwrap();
154        assert!(grant.is_pending());
155        assert_eq!(grant.raw_parameters, raw_parameters);
156
157        // Lookup the same grant by id
158        let grant_lookup = repo
159            .oauth2_authorization_grant()
160            .lookup(grant.id)
161            .await
162            .unwrap()
163            .expect("grant not found");
164        assert_eq!(grant, grant_lookup);
165
166        // Find the same grant by code
167        let grant_lookup = repo
168            .oauth2_authorization_grant()
169            .find_by_code("code")
170            .await
171            .unwrap()
172            .expect("grant not found");
173        assert_eq!(grant, grant_lookup);
174
175        // Create a user and a start a user session
176        let user = repo
177            .user()
178            .add(&mut rng, &clock, "john".to_owned())
179            .await
180            .unwrap();
181        let user_session = repo
182            .browser_session()
183            .add(&mut rng, &clock, &user, None)
184            .await
185            .unwrap();
186
187        // Lookup a non-existing session
188        let session = repo.oauth2_session().lookup(Ulid::nil()).await.unwrap();
189        assert_eq!(session, None);
190
191        // Create an OAuth session
192        let session = repo
193            .oauth2_session()
194            .add_from_browser_session(
195                &mut rng,
196                &clock,
197                &client,
198                &user_session,
199                grant.scope.clone(),
200            )
201            .await
202            .unwrap();
203
204        // Mark the grant as fulfilled
205        let grant = repo
206            .oauth2_authorization_grant()
207            .fulfill(&clock, &session, grant)
208            .await
209            .unwrap();
210        assert!(grant.is_fulfilled());
211
212        // Lookup the same session by id
213        let session_lookup = repo
214            .oauth2_session()
215            .lookup(session.id)
216            .await
217            .unwrap()
218            .expect("session not found");
219        assert_eq!(session, session_lookup);
220
221        // Mark the grant as exchanged
222        let grant = repo
223            .oauth2_authorization_grant()
224            .exchange(&clock, grant)
225            .await
226            .unwrap();
227        assert!(grant.is_exchanged());
228
229        // Lookup a non-existing token
230        let token = repo
231            .oauth2_access_token()
232            .lookup(Ulid::nil())
233            .await
234            .unwrap();
235        assert_eq!(token, None);
236
237        // Find a non-existing token
238        let token = repo
239            .oauth2_access_token()
240            .find_by_token("aabbcc")
241            .await
242            .unwrap();
243        assert_eq!(token, None);
244
245        // Create an access token
246        let access_token = repo
247            .oauth2_access_token()
248            .add(
249                &mut rng,
250                &clock,
251                &session,
252                "aabbcc".to_owned(),
253                Some(Duration::try_minutes(5).unwrap()),
254            )
255            .await
256            .unwrap();
257
258        // Lookup the same token by id
259        let access_token_lookup = repo
260            .oauth2_access_token()
261            .lookup(access_token.id)
262            .await
263            .unwrap()
264            .expect("token not found");
265        assert_eq!(access_token, access_token_lookup);
266
267        // Find the same token by token
268        let access_token_lookup = repo
269            .oauth2_access_token()
270            .find_by_token("aabbcc")
271            .await
272            .unwrap()
273            .expect("token not found");
274        assert_eq!(access_token, access_token_lookup);
275
276        // Lookup a non-existing refresh token
277        let refresh_token = repo
278            .oauth2_refresh_token()
279            .lookup(Ulid::nil())
280            .await
281            .unwrap();
282        assert_eq!(refresh_token, None);
283
284        // Find a non-existing refresh token
285        let refresh_token = repo
286            .oauth2_refresh_token()
287            .find_by_token("aabbcc")
288            .await
289            .unwrap();
290        assert_eq!(refresh_token, None);
291
292        // Create a refresh token
293        let refresh_token = repo
294            .oauth2_refresh_token()
295            .add(
296                &mut rng,
297                &clock,
298                &session,
299                &access_token,
300                "aabbcc".to_owned(),
301            )
302            .await
303            .unwrap();
304
305        // Lookup the same refresh token by id
306        let refresh_token_lookup = repo
307            .oauth2_refresh_token()
308            .lookup(refresh_token.id)
309            .await
310            .unwrap()
311            .expect("refresh token not found");
312        assert_eq!(refresh_token, refresh_token_lookup);
313
314        // Find the same refresh token by token
315        let refresh_token_lookup = repo
316            .oauth2_refresh_token()
317            .find_by_token("aabbcc")
318            .await
319            .unwrap()
320            .expect("refresh token not found");
321        assert_eq!(refresh_token, refresh_token_lookup);
322
323        assert!(access_token.is_valid(clock.now()));
324        clock.advance(Duration::try_minutes(6).unwrap());
325        assert!(!access_token.is_valid(clock.now()));
326
327        // XXX: we might want to create a new access token
328        clock.advance(Duration::try_minutes(-6).unwrap()); // Go back in time
329        assert!(access_token.is_valid(clock.now()));
330
331        // Create a new refresh token to be able to consume the old one
332        let new_refresh_token = repo
333            .oauth2_refresh_token()
334            .add(
335                &mut rng,
336                &clock,
337                &session,
338                &access_token,
339                "ddeeff".to_owned(),
340            )
341            .await
342            .unwrap();
343
344        // Mark the access token as revoked
345        let access_token = repo
346            .oauth2_access_token()
347            .revoke(&clock, access_token)
348            .await
349            .unwrap();
350        assert!(!access_token.is_valid(clock.now()));
351
352        // Mark the refresh token as consumed
353        assert!(refresh_token.is_valid());
354        let refresh_token = repo
355            .oauth2_refresh_token()
356            .consume(&clock, refresh_token, &new_refresh_token)
357            .await
358            .unwrap();
359        assert!(!refresh_token.is_valid());
360
361        // Record the user-agent on the session
362        assert!(session.user_agent.is_none());
363        let session = repo
364            .oauth2_session()
365            .record_user_agent(session, "Mozilla/5.0".to_owned())
366            .await
367            .unwrap();
368        assert_eq!(session.user_agent.as_deref(), Some("Mozilla/5.0"));
369
370        // Reload the session and check the user-agent
371        let session = repo
372            .oauth2_session()
373            .lookup(session.id)
374            .await
375            .unwrap()
376            .expect("session not found");
377        assert_eq!(session.user_agent.as_deref(), Some("Mozilla/5.0"));
378
379        // Mark the session as finished
380        assert!(session.is_valid());
381        let session = repo.oauth2_session().finish(&clock, session).await.unwrap();
382        assert!(!session.is_valid());
383    }
384
385    /// Test the [`OAuth2SessionRepository::list`] and
386    /// [`OAuth2SessionRepository::count`] methods.
387    #[sqlx::test(migrator = "crate::MIGRATOR")]
388    async fn test_list_sessions(pool: PgPool) {
389        let mut rng = ChaChaRng::seed_from_u64(42);
390        let clock = MockClock::default();
391        let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
392
393        // Create two users and their corresponding browser sessions
394        let user1 = repo
395            .user()
396            .add(&mut rng, &clock, "alice".to_owned())
397            .await
398            .unwrap();
399        let user1_session = repo
400            .browser_session()
401            .add(&mut rng, &clock, &user1, None)
402            .await
403            .unwrap();
404
405        let user2 = repo
406            .user()
407            .add(&mut rng, &clock, "bob".to_owned())
408            .await
409            .unwrap();
410        let user2_session = repo
411            .browser_session()
412            .add(&mut rng, &clock, &user2, None)
413            .await
414            .unwrap();
415
416        // Create two clients
417        let client1 = repo
418            .oauth2_client()
419            .add(
420                &mut rng,
421                &clock,
422                vec!["https://first.example.com/redirect".parse().unwrap()],
423                None,
424                None,
425                None,
426                vec![GrantType::AuthorizationCode],
427                Some("First client".to_owned()),
428                Some("https://first.example.com/logo.png".parse().unwrap()),
429                Some("https://first.example.com/".parse().unwrap()),
430                Some("https://first.example.com/policy".parse().unwrap()),
431                Some("https://first.example.com/tos".parse().unwrap()),
432                Some("https://first.example.com/jwks.json".parse().unwrap()),
433                None,
434                None,
435                None,
436                None,
437                None,
438                Some("https://first.example.com/login".parse().unwrap()),
439            )
440            .await
441            .unwrap();
442        let client2 = repo
443            .oauth2_client()
444            .add(
445                &mut rng,
446                &clock,
447                vec!["https://second.example.com/redirect".parse().unwrap()],
448                None,
449                None,
450                None,
451                vec![GrantType::AuthorizationCode],
452                Some("Second client".to_owned()),
453                Some("https://second.example.com/logo.png".parse().unwrap()),
454                Some("https://second.example.com/".parse().unwrap()),
455                Some("https://second.example.com/policy".parse().unwrap()),
456                Some("https://second.example.com/tos".parse().unwrap()),
457                Some("https://second.example.com/jwks.json".parse().unwrap()),
458                None,
459                None,
460                None,
461                None,
462                None,
463                Some("https://second.example.com/login".parse().unwrap()),
464            )
465            .await
466            .unwrap();
467
468        let scope = Scope::from_iter([OPENID, EMAIL]);
469        let scope2 = Scope::from_iter([OPENID, PROFILE]);
470
471        // Create two sessions for each user, one with each client
472        // We're moving the clock forward by 1 minute between each session to ensure
473        // we're getting consistent ordering in lists.
474        let session11 = repo
475            .oauth2_session()
476            .add_from_browser_session(&mut rng, &clock, &client1, &user1_session, scope.clone())
477            .await
478            .unwrap();
479        clock.advance(Duration::try_minutes(1).unwrap());
480
481        let session12 = repo
482            .oauth2_session()
483            .add_from_browser_session(&mut rng, &clock, &client1, &user2_session, scope.clone())
484            .await
485            .unwrap();
486        clock.advance(Duration::try_minutes(1).unwrap());
487
488        let session21 = repo
489            .oauth2_session()
490            .add_from_browser_session(&mut rng, &clock, &client2, &user1_session, scope2.clone())
491            .await
492            .unwrap();
493        clock.advance(Duration::try_minutes(1).unwrap());
494
495        let session22 = repo
496            .oauth2_session()
497            .add_from_browser_session(&mut rng, &clock, &client2, &user2_session, scope2.clone())
498            .await
499            .unwrap();
500        clock.advance(Duration::try_minutes(1).unwrap());
501
502        // We're also finishing two of the sessions
503        let session11 = repo
504            .oauth2_session()
505            .finish(&clock, session11)
506            .await
507            .unwrap();
508        let session22 = repo
509            .oauth2_session()
510            .finish(&clock, session22)
511            .await
512            .unwrap();
513
514        let pagination = Pagination::first(10);
515
516        // First, list all the sessions
517        let filter = OAuth2SessionFilter::new().for_any_user();
518        let list = repo
519            .oauth2_session()
520            .list(filter, pagination)
521            .await
522            .unwrap();
523        assert!(!list.has_next_page);
524        assert_eq!(list.edges.len(), 4);
525        assert_eq!(list.edges[0].node, session11);
526        assert_eq!(list.edges[1].node, session12);
527        assert_eq!(list.edges[2].node, session21);
528        assert_eq!(list.edges[3].node, session22);
529
530        assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 4);
531
532        // Now filter for only one user
533        let filter = OAuth2SessionFilter::new().for_user(&user1);
534        let list = repo
535            .oauth2_session()
536            .list(filter, pagination)
537            .await
538            .unwrap();
539        assert!(!list.has_next_page);
540        assert_eq!(list.edges.len(), 2);
541        assert_eq!(list.edges[0].node, session11);
542        assert_eq!(list.edges[1].node, session21);
543
544        assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 2);
545
546        // Filter for only one client
547        let filter = OAuth2SessionFilter::new().for_client(&client1);
548        let list = repo
549            .oauth2_session()
550            .list(filter, pagination)
551            .await
552            .unwrap();
553        assert!(!list.has_next_page);
554        assert_eq!(list.edges.len(), 2);
555        assert_eq!(list.edges[0].node, session11);
556        assert_eq!(list.edges[1].node, session12);
557
558        assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 2);
559
560        // Filter for both a user and a client
561        let filter = OAuth2SessionFilter::new()
562            .for_user(&user2)
563            .for_client(&client2);
564        let list = repo
565            .oauth2_session()
566            .list(filter, pagination)
567            .await
568            .unwrap();
569        assert!(!list.has_next_page);
570        assert_eq!(list.edges.len(), 1);
571        assert_eq!(list.edges[0].node, session22);
572
573        assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 1);
574
575        // Filter for active sessions
576        let filter = OAuth2SessionFilter::new().active_only();
577        let list = repo
578            .oauth2_session()
579            .list(filter, pagination)
580            .await
581            .unwrap();
582        assert!(!list.has_next_page);
583        assert_eq!(list.edges.len(), 2);
584        assert_eq!(list.edges[0].node, session12);
585        assert_eq!(list.edges[1].node, session21);
586
587        assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 2);
588
589        // Filter for finished sessions
590        let filter = OAuth2SessionFilter::new().finished_only();
591        let list = repo
592            .oauth2_session()
593            .list(filter, pagination)
594            .await
595            .unwrap();
596        assert!(!list.has_next_page);
597        assert_eq!(list.edges.len(), 2);
598        assert_eq!(list.edges[0].node, session11);
599        assert_eq!(list.edges[1].node, session22);
600
601        assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 2);
602
603        // Combine the finished filter with the user filter
604        let filter = OAuth2SessionFilter::new().finished_only().for_user(&user2);
605        let list = repo
606            .oauth2_session()
607            .list(filter, pagination)
608            .await
609            .unwrap();
610        assert!(!list.has_next_page);
611        assert_eq!(list.edges.len(), 1);
612        assert_eq!(list.edges[0].node, session22);
613
614        assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 1);
615
616        // Combine the finished filter with the client filter
617        let filter = OAuth2SessionFilter::new()
618            .finished_only()
619            .for_client(&client2);
620        let list = repo
621            .oauth2_session()
622            .list(filter, pagination)
623            .await
624            .unwrap();
625        assert!(!list.has_next_page);
626        assert_eq!(list.edges.len(), 1);
627        assert_eq!(list.edges[0].node, session22);
628
629        assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 1);
630
631        // Combine the active filter with the user filter
632        let filter = OAuth2SessionFilter::new().active_only().for_user(&user2);
633        let list = repo
634            .oauth2_session()
635            .list(filter, pagination)
636            .await
637            .unwrap();
638        assert!(!list.has_next_page);
639        assert_eq!(list.edges.len(), 1);
640        assert_eq!(list.edges[0].node, session12);
641
642        assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 1);
643
644        // Combine the active filter with the client filter
645        let filter = OAuth2SessionFilter::new()
646            .active_only()
647            .for_client(&client2);
648        let list = repo
649            .oauth2_session()
650            .list(filter, pagination)
651            .await
652            .unwrap();
653        assert!(!list.has_next_page);
654        assert_eq!(list.edges.len(), 1);
655        assert_eq!(list.edges[0].node, session21);
656
657        assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 1);
658
659        // Try the scope filter. We should get all sessions with the "openid" scope
660        let scope = Scope::from_iter([OPENID]);
661        let filter = OAuth2SessionFilter::new().with_scope(&scope);
662        let list = repo
663            .oauth2_session()
664            .list(filter, pagination)
665            .await
666            .unwrap();
667        assert!(!list.has_next_page);
668        assert_eq!(list.edges.len(), 4);
669        assert_eq!(list.edges[0].node, session11);
670        assert_eq!(list.edges[1].node, session12);
671        assert_eq!(list.edges[2].node, session21);
672        assert_eq!(list.edges[3].node, session22);
673        assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 4);
674
675        // We should get all sessions with the "openid" and "email" scope
676        let scope = Scope::from_iter([OPENID, EMAIL]);
677        let filter = OAuth2SessionFilter::new().with_scope(&scope);
678        let list = repo
679            .oauth2_session()
680            .list(filter, pagination)
681            .await
682            .unwrap();
683        assert!(!list.has_next_page);
684        assert_eq!(list.edges.len(), 2);
685        assert_eq!(list.edges[0].node, session11);
686        assert_eq!(list.edges[1].node, session12);
687        assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 2);
688
689        // Try combining the scope filter with the user filter
690        let filter = OAuth2SessionFilter::new()
691            .with_scope(&scope)
692            .for_user(&user1);
693        let list = repo
694            .oauth2_session()
695            .list(filter, pagination)
696            .await
697            .unwrap();
698        assert_eq!(list.edges.len(), 1);
699        assert_eq!(list.edges[0].node, session11);
700        assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 1);
701
702        // Finish all sessions of a client in batch
703        let affected = repo
704            .oauth2_session()
705            .finish_bulk(
706                &clock,
707                OAuth2SessionFilter::new()
708                    .for_client(&client1)
709                    .active_only(),
710            )
711            .await
712            .unwrap();
713        assert_eq!(affected, 1);
714
715        // We should have 3 finished sessions
716        assert_eq!(
717            repo.oauth2_session()
718                .count(OAuth2SessionFilter::new().finished_only())
719                .await
720                .unwrap(),
721            3
722        );
723
724        // We should have 1 active sessions
725        assert_eq!(
726            repo.oauth2_session()
727                .count(OAuth2SessionFilter::new().active_only())
728                .await
729                .unwrap(),
730            1
731        );
732    }
733
734    /// Test the created-at filters on [`OAuth2SessionFilter`].
735    #[sqlx::test(migrator = "crate::MIGRATOR")]
736    async fn test_list_sessions_by_created_at(pool: PgPool) {
737        let mut rng = ChaChaRng::seed_from_u64(42);
738        let clock = MockClock::default();
739        let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
740
741        let user = repo
742            .user()
743            .add(&mut rng, &clock, "alice".to_owned())
744            .await
745            .unwrap();
746        let user_session = repo
747            .browser_session()
748            .add(&mut rng, &clock, &user, None)
749            .await
750            .unwrap();
751        let client = repo
752            .oauth2_client()
753            .add(
754                &mut rng,
755                &clock,
756                vec!["https://example.com/redirect".parse().unwrap()],
757                None,
758                None,
759                None,
760                vec![GrantType::AuthorizationCode],
761                Some("Test client".to_owned()),
762                Some("https://example.com/logo.png".parse().unwrap()),
763                Some("https://example.com/".parse().unwrap()),
764                Some("https://example.com/policy".parse().unwrap()),
765                Some("https://example.com/tos".parse().unwrap()),
766                Some("https://example.com/jwks.json".parse().unwrap()),
767                None,
768                None,
769                None,
770                None,
771                None,
772                Some("https://example.com/login".parse().unwrap()),
773            )
774            .await
775            .unwrap();
776
777        let scope = Scope::from_iter([OPENID]);
778
779        // Create three sessions, one per minute, capturing the cutoff timestamp
780        // between the second and the third.
781        let session1 = repo
782            .oauth2_session()
783            .add_from_browser_session(&mut rng, &clock, &client, &user_session, scope.clone())
784            .await
785            .unwrap();
786        clock.advance(Duration::try_minutes(1).unwrap());
787
788        let session2 = repo
789            .oauth2_session()
790            .add_from_browser_session(&mut rng, &clock, &client, &user_session, scope.clone())
791            .await
792            .unwrap();
793        clock.advance(Duration::try_minutes(1).unwrap());
794
795        let cutoff = clock.now();
796
797        clock.advance(Duration::try_minutes(1).unwrap());
798        let session3 = repo
799            .oauth2_session()
800            .add_from_browser_session(&mut rng, &clock, &client, &user_session, scope.clone())
801            .await
802            .unwrap();
803
804        let pagination = Pagination::first(10);
805
806        // Sessions created before the cutoff
807        let filter = OAuth2SessionFilter::new().with_created_before(cutoff);
808        let list = repo
809            .oauth2_session()
810            .list(filter, pagination)
811            .await
812            .unwrap();
813        assert_eq!(list.edges.len(), 2);
814        assert_eq!(list.edges[0].node, session1);
815        assert_eq!(list.edges[1].node, session2);
816        assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 2);
817
818        // Sessions created after the cutoff
819        let filter = OAuth2SessionFilter::new().with_created_after(cutoff);
820        let list = repo
821            .oauth2_session()
822            .list(filter, pagination)
823            .await
824            .unwrap();
825        assert_eq!(list.edges.len(), 1);
826        assert_eq!(list.edges[0].node, session3);
827        assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 1);
828    }
829
830    /// Test the multi-client filter on [`OAuth2SessionFilter`].
831    #[sqlx::test(migrator = "crate::MIGRATOR")]
832    async fn test_list_sessions_for_clients(pool: PgPool) {
833        let mut rng = ChaChaRng::seed_from_u64(42);
834        let clock = MockClock::default();
835        let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
836
837        // Provision a user + browser session to attach the OAuth2 sessions to
838        let user = repo
839            .user()
840            .add(&mut rng, &clock, "alice".to_owned())
841            .await
842            .unwrap();
843        let user_session = repo
844            .browser_session()
845            .add(&mut rng, &clock, &user, None)
846            .await
847            .unwrap();
848
849        // Provision three clients
850        let mut clients = Vec::new();
851        for label in ["first", "second", "third"] {
852            let client = repo
853                .oauth2_client()
854                .add(
855                    &mut rng,
856                    &clock,
857                    vec![
858                        format!("https://{label}.example.com/redirect")
859                            .parse()
860                            .unwrap(),
861                    ],
862                    None,
863                    None,
864                    None,
865                    vec![GrantType::AuthorizationCode],
866                    Some(format!("{label} client")),
867                    Some(
868                        format!("https://{label}.example.com/logo.png")
869                            .parse()
870                            .unwrap(),
871                    ),
872                    Some(format!("https://{label}.example.com/").parse().unwrap()),
873                    Some(
874                        format!("https://{label}.example.com/policy")
875                            .parse()
876                            .unwrap(),
877                    ),
878                    Some(format!("https://{label}.example.com/tos").parse().unwrap()),
879                    Some(
880                        format!("https://{label}.example.com/jwks.json")
881                            .parse()
882                            .unwrap(),
883                    ),
884                    None,
885                    None,
886                    None,
887                    None,
888                    None,
889                    Some(
890                        format!("https://{label}.example.com/login")
891                            .parse()
892                            .unwrap(),
893                    ),
894                )
895                .await
896                .unwrap();
897            clients.push(client);
898        }
899        let [client1, client2, client3] = <[_; 3]>::try_from(clients).ok().unwrap();
900
901        let scope = Scope::from_iter([OPENID]);
902
903        // One session per client
904        let session1 = repo
905            .oauth2_session()
906            .add_from_browser_session(&mut rng, &clock, &client1, &user_session, scope.clone())
907            .await
908            .unwrap();
909        clock.advance(Duration::try_minutes(1).unwrap());
910
911        let session2 = repo
912            .oauth2_session()
913            .add_from_browser_session(&mut rng, &clock, &client2, &user_session, scope.clone())
914            .await
915            .unwrap();
916        clock.advance(Duration::try_minutes(1).unwrap());
917
918        let _session3 = repo
919            .oauth2_session()
920            .add_from_browser_session(&mut rng, &clock, &client3, &user_session, scope.clone())
921            .await
922            .unwrap();
923
924        let pagination = Pagination::first(10);
925
926        // Filter on two of the three clients returns the matching sessions
927        let two_clients = [&client1, &client2];
928        let filter = OAuth2SessionFilter::new().for_clients(&two_clients);
929        let list = repo
930            .oauth2_session()
931            .list(filter, pagination)
932            .await
933            .unwrap();
934        assert!(!list.has_next_page);
935        assert_eq!(list.edges.len(), 2);
936        assert_eq!(list.edges[0].node, session1);
937        assert_eq!(list.edges[1].node, session2);
938        assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 2);
939
940        // A single-element list behaves like for_client
941        let one_client = [&client2];
942        let filter = OAuth2SessionFilter::new().for_clients(&one_client);
943        let list = repo
944            .oauth2_session()
945            .list(filter, pagination)
946            .await
947            .unwrap();
948        assert_eq!(list.edges.len(), 1);
949        assert_eq!(list.edges[0].node, session2);
950        assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 1);
951
952        // An empty list matches no sessions (sea-query emits `1 = 2` for IN ())
953        let no_clients: [&mas_data_model::Client; 0] = [];
954        let filter = OAuth2SessionFilter::new().for_clients(&no_clients);
955        let list = repo
956            .oauth2_session()
957            .list(filter, pagination)
958            .await
959            .unwrap();
960        assert!(list.edges.is_empty());
961        assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 0);
962    }
963
964    /// Test the [`OAuth2DeviceCodeGrantRepository`] implementation
965    #[sqlx::test(migrator = "crate::MIGRATOR")]
966    async fn test_device_code_grant_repository(pool: PgPool) {
967        let mut rng = ChaChaRng::seed_from_u64(42);
968        let clock = MockClock::default();
969        let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
970
971        // Provision a client
972        let client = repo
973            .oauth2_client()
974            .add(
975                &mut rng,
976                &clock,
977                vec!["https://example.com/redirect".parse().unwrap()],
978                None,
979                None,
980                None,
981                vec![GrantType::AuthorizationCode],
982                Some("Example".to_owned()),
983                Some("https://example.com/logo.png".parse().unwrap()),
984                Some("https://example.com/".parse().unwrap()),
985                Some("https://example.com/policy".parse().unwrap()),
986                Some("https://example.com/tos".parse().unwrap()),
987                Some("https://example.com/jwks.json".parse().unwrap()),
988                None,
989                None,
990                None,
991                None,
992                None,
993                Some("https://example.com/login".parse().unwrap()),
994            )
995            .await
996            .unwrap();
997
998        // Provision a user
999        let user = repo
1000            .user()
1001            .add(&mut rng, &clock, "john".to_owned())
1002            .await
1003            .unwrap();
1004
1005        // Provision a browser session
1006        let browser_session = repo
1007            .browser_session()
1008            .add(&mut rng, &clock, &user, None)
1009            .await
1010            .unwrap();
1011
1012        let user_code = "usercode";
1013        let device_code = "devicecode";
1014        let scope = Scope::from_iter([OPENID, EMAIL]);
1015
1016        // Create a device code grant
1017        let grant = repo
1018            .oauth2_device_code_grant()
1019            .add(
1020                &mut rng,
1021                &clock,
1022                OAuth2DeviceCodeGrantParams {
1023                    client: &client,
1024                    scope: scope.clone(),
1025                    device_code: device_code.to_owned(),
1026                    user_code: user_code.to_owned(),
1027                    expires_in: Duration::try_minutes(5).unwrap(),
1028                    ip_address: None,
1029                    user_agent: None,
1030                },
1031            )
1032            .await
1033            .unwrap();
1034
1035        assert!(grant.is_pending());
1036
1037        // Check that we can find the grant by ID
1038        let id = grant.id;
1039        let lookup = repo.oauth2_device_code_grant().lookup(id).await.unwrap();
1040        assert_eq!(lookup.as_ref(), Some(&grant));
1041
1042        // Check that we can find the grant by device code
1043        let lookup = repo
1044            .oauth2_device_code_grant()
1045            .find_by_device_code(device_code)
1046            .await
1047            .unwrap();
1048        assert_eq!(lookup.as_ref(), Some(&grant));
1049
1050        // Check that we can find the grant by user code
1051        let lookup = repo
1052            .oauth2_device_code_grant()
1053            .find_by_user_code(user_code)
1054            .await
1055            .unwrap();
1056        assert_eq!(lookup.as_ref(), Some(&grant));
1057
1058        // Let's mark it as fulfilled
1059        let grant = repo
1060            .oauth2_device_code_grant()
1061            .fulfill(&clock, grant, &browser_session)
1062            .await
1063            .unwrap();
1064        assert!(!grant.is_pending());
1065        assert!(grant.is_fulfilled());
1066
1067        // Check that we can't mark it as rejected now
1068        let res = repo
1069            .oauth2_device_code_grant()
1070            .reject(&clock, grant, &browser_session)
1071            .await;
1072        assert!(res.is_err());
1073
1074        // Look it up again
1075        let grant = repo
1076            .oauth2_device_code_grant()
1077            .lookup(id)
1078            .await
1079            .unwrap()
1080            .unwrap();
1081
1082        // We can't mark it as fulfilled again
1083        let res = repo
1084            .oauth2_device_code_grant()
1085            .fulfill(&clock, grant, &browser_session)
1086            .await;
1087        assert!(res.is_err());
1088
1089        // Look it up again
1090        let grant = repo
1091            .oauth2_device_code_grant()
1092            .lookup(id)
1093            .await
1094            .unwrap()
1095            .unwrap();
1096
1097        // Create an OAuth 2.0 session
1098        let session = repo
1099            .oauth2_session()
1100            .add_from_browser_session(&mut rng, &clock, &client, &browser_session, scope.clone())
1101            .await
1102            .unwrap();
1103
1104        // We can mark it as exchanged
1105        let grant = repo
1106            .oauth2_device_code_grant()
1107            .exchange(&clock, grant, &session)
1108            .await
1109            .unwrap();
1110        assert!(!grant.is_pending());
1111        assert!(!grant.is_fulfilled());
1112        assert!(grant.is_exchanged());
1113
1114        // We can't mark it as exchanged again
1115        let res = repo
1116            .oauth2_device_code_grant()
1117            .exchange(&clock, grant, &session)
1118            .await;
1119        assert!(res.is_err());
1120
1121        // Do a new grant to reject it
1122        let grant = repo
1123            .oauth2_device_code_grant()
1124            .add(
1125                &mut rng,
1126                &clock,
1127                OAuth2DeviceCodeGrantParams {
1128                    client: &client,
1129                    scope: scope.clone(),
1130                    device_code: "second_devicecode".to_owned(),
1131                    user_code: "second_usercode".to_owned(),
1132                    expires_in: Duration::try_minutes(5).unwrap(),
1133                    ip_address: None,
1134                    user_agent: None,
1135                },
1136            )
1137            .await
1138            .unwrap();
1139
1140        let id = grant.id;
1141
1142        // We can mark it as rejected
1143        let grant = repo
1144            .oauth2_device_code_grant()
1145            .reject(&clock, grant, &browser_session)
1146            .await
1147            .unwrap();
1148        assert!(!grant.is_pending());
1149        assert!(grant.is_rejected());
1150
1151        // We can't mark it as rejected again
1152        let res = repo
1153            .oauth2_device_code_grant()
1154            .reject(&clock, grant, &browser_session)
1155            .await;
1156        assert!(res.is_err());
1157
1158        // Look it up again
1159        let grant = repo
1160            .oauth2_device_code_grant()
1161            .lookup(id)
1162            .await
1163            .unwrap()
1164            .unwrap();
1165
1166        // We can't mark it as fulfilled
1167        let res = repo
1168            .oauth2_device_code_grant()
1169            .fulfill(&clock, grant, &browser_session)
1170            .await;
1171        assert!(res.is_err());
1172
1173        // Look it up again
1174        let grant = repo
1175            .oauth2_device_code_grant()
1176            .lookup(id)
1177            .await
1178            .unwrap()
1179            .unwrap();
1180
1181        // We can't mark it as exchanged
1182        let res = repo
1183            .oauth2_device_code_grant()
1184            .exchange(&clock, grant, &session)
1185            .await;
1186        assert!(res.is_err());
1187    }
1188
1189    /// Test the [`OAuth2ClientRepository::list`] and
1190    /// [`OAuth2ClientRepository::count`] methods.
1191    #[sqlx::test(migrator = "crate::MIGRATOR")]
1192    async fn test_list_clients(pool: PgPool) {
1193        let mut rng = ChaChaRng::seed_from_u64(42);
1194        let clock = MockClock::default();
1195        let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
1196
1197        // Empty initially
1198        let filter = OAuth2ClientFilter::new();
1199        assert_eq!(repo.oauth2_client().count(filter).await.unwrap(), 0);
1200
1201        let page = repo
1202            .oauth2_client()
1203            .list(filter, Pagination::first(10))
1204            .await
1205            .unwrap();
1206        assert!(page.edges.is_empty());
1207        assert!(!page.has_next_page);
1208
1209        // Add a couple of clients
1210        let client1 = repo
1211            .oauth2_client()
1212            .add(
1213                &mut rng,
1214                &clock,
1215                vec!["https://first.example.com/redirect".parse().unwrap()],
1216                None,
1217                None,
1218                None,
1219                vec![GrantType::AuthorizationCode],
1220                Some("First client".to_owned()),
1221                None,
1222                Some("https://first.example.com/".parse().unwrap()),
1223                None,
1224                None,
1225                None,
1226                None,
1227                None,
1228                None,
1229                None,
1230                None,
1231                None,
1232            )
1233            .await
1234            .unwrap();
1235        clock.advance(Duration::try_minutes(1).unwrap());
1236
1237        let client2 = repo
1238            .oauth2_client()
1239            .add(
1240                &mut rng,
1241                &clock,
1242                vec!["https://second.example.com/redirect".parse().unwrap()],
1243                None,
1244                None,
1245                None,
1246                vec![GrantType::AuthorizationCode],
1247                Some("Second client".to_owned()),
1248                None,
1249                Some("https://second.example.com/".parse().unwrap()),
1250                None,
1251                None,
1252                None,
1253                None,
1254                None,
1255                None,
1256                None,
1257                None,
1258                None,
1259            )
1260            .await
1261            .unwrap();
1262
1263        assert_eq!(repo.oauth2_client().count(filter).await.unwrap(), 2);
1264
1265        let page = repo
1266            .oauth2_client()
1267            .list(filter, Pagination::first(10))
1268            .await
1269            .unwrap();
1270        assert!(!page.has_next_page);
1271        assert_eq!(page.edges.len(), 2);
1272        assert_eq!(page.edges[0].node, client1);
1273        assert_eq!(page.edges[1].node, client2);
1274
1275        // Add a static client
1276        let static_id = Ulid::from_datetime_with_source(clock.now().into(), &mut rng);
1277        repo.oauth2_client()
1278            .upsert_static(
1279                static_id,
1280                Some("Static client".to_owned()),
1281                OAuthClientAuthenticationMethod::None,
1282                None,
1283                None,
1284                None,
1285                vec!["https://static.example.com/redirect".parse().unwrap()],
1286            )
1287            .await
1288            .unwrap();
1289        // Re-read via lookup so we have the canonical representation
1290        let static_client = repo
1291            .oauth2_client()
1292            .lookup(static_id)
1293            .await
1294            .unwrap()
1295            .expect("static client just inserted");
1296
1297        assert_eq!(repo.oauth2_client().count(filter).await.unwrap(), 3);
1298
1299        // Only static clients
1300        let filter = OAuth2ClientFilter::new().only_static_clients();
1301        assert_eq!(repo.oauth2_client().count(filter).await.unwrap(), 1);
1302        let page = repo
1303            .oauth2_client()
1304            .list(filter, Pagination::first(10))
1305            .await
1306            .unwrap();
1307        assert_eq!(page.edges.len(), 1);
1308        assert_eq!(page.edges[0].node, static_client);
1309
1310        // Only dynamic clients
1311        let filter = OAuth2ClientFilter::new().only_dynamic_clients();
1312        assert_eq!(repo.oauth2_client().count(filter).await.unwrap(), 2);
1313        let page = repo
1314            .oauth2_client()
1315            .list(filter, Pagination::first(10))
1316            .await
1317            .unwrap();
1318        assert_eq!(page.edges.len(), 2);
1319        assert_eq!(page.edges[0].node, client1);
1320        assert_eq!(page.edges[1].node, client2);
1321
1322        // Substring match on client_name
1323        let filter = OAuth2ClientFilter::new().matching_client_name("first");
1324        assert_eq!(repo.oauth2_client().count(filter).await.unwrap(), 1);
1325        let page = repo
1326            .oauth2_client()
1327            .list(filter, Pagination::first(10))
1328            .await
1329            .unwrap();
1330        assert_eq!(page.edges.len(), 1);
1331        assert_eq!(page.edges[0].node, client1);
1332
1333        // Case-insensitive match on client_name
1334        let filter = OAuth2ClientFilter::new().matching_client_name("CLIENT");
1335        assert_eq!(repo.oauth2_client().count(filter).await.unwrap(), 3);
1336
1337        // Substring match on client_uri
1338        let filter = OAuth2ClientFilter::new().matching_client_uri("second");
1339        assert_eq!(repo.oauth2_client().count(filter).await.unwrap(), 1);
1340        let page = repo
1341            .oauth2_client()
1342            .list(filter, Pagination::first(10))
1343            .await
1344            .unwrap();
1345        assert_eq!(page.edges.len(), 1);
1346        assert_eq!(page.edges[0].node, client2);
1347
1348        // Case-insensitive match on client_uri
1349        let filter = OAuth2ClientFilter::new().matching_client_uri("EXAMPLE.COM");
1350        assert_eq!(repo.oauth2_client().count(filter).await.unwrap(), 2);
1351    }
1352
1353    /// Test the grant-type filter on [`OAuth2ClientFilter`].
1354    #[sqlx::test(migrator = "crate::MIGRATOR")]
1355    async fn test_list_clients_by_grant_type(pool: PgPool) {
1356        let mut rng = ChaChaRng::seed_from_u64(42);
1357        let clock = MockClock::default();
1358        let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
1359
1360        // A client supporting authorization_code (+ refresh_token)
1361        let auth_code_client = repo
1362            .oauth2_client()
1363            .add(
1364                &mut rng,
1365                &clock,
1366                vec!["https://code.example.com/redirect".parse().unwrap()],
1367                None,
1368                None,
1369                None,
1370                vec![GrantType::AuthorizationCode, GrantType::RefreshToken],
1371                Some("Authorization code client".to_owned()),
1372                None,
1373                None,
1374                None,
1375                None,
1376                None,
1377                None,
1378                None,
1379                None,
1380                None,
1381                None,
1382                None,
1383            )
1384            .await
1385            .unwrap();
1386
1387        // A client supporting only client_credentials
1388        let client_credentials_client = repo
1389            .oauth2_client()
1390            .add(
1391                &mut rng,
1392                &clock,
1393                vec![],
1394                None,
1395                None,
1396                None,
1397                vec![GrantType::ClientCredentials],
1398                Some("Client credentials client".to_owned()),
1399                None,
1400                None,
1401                None,
1402                None,
1403                None,
1404                None,
1405                None,
1406                None,
1407                None,
1408                None,
1409                None,
1410            )
1411            .await
1412            .unwrap();
1413
1414        // authorization_code: only the first client
1415        let filter = OAuth2ClientFilter::new().with_grant_type(&GrantType::AuthorizationCode);
1416        assert_eq!(repo.oauth2_client().count(filter).await.unwrap(), 1);
1417        let page = repo
1418            .oauth2_client()
1419            .list(filter, Pagination::first(10))
1420            .await
1421            .unwrap();
1422        assert_eq!(page.edges.len(), 1);
1423        assert_eq!(page.edges[0].node, auth_code_client);
1424
1425        // client_credentials: only the second client
1426        let filter = OAuth2ClientFilter::new().with_grant_type(&GrantType::ClientCredentials);
1427        assert_eq!(repo.oauth2_client().count(filter).await.unwrap(), 1);
1428        let page = repo
1429            .oauth2_client()
1430            .list(filter, Pagination::first(10))
1431            .await
1432            .unwrap();
1433        assert_eq!(page.edges.len(), 1);
1434        assert_eq!(page.edges[0].node, client_credentials_client);
1435
1436        // refresh_token: only the first client
1437        let filter = OAuth2ClientFilter::new().with_grant_type(&GrantType::RefreshToken);
1438        assert_eq!(repo.oauth2_client().count(filter).await.unwrap(), 1);
1439
1440        // device_code: no client supports it
1441        let filter = OAuth2ClientFilter::new().with_grant_type(&GrantType::DeviceCode);
1442        assert_eq!(repo.oauth2_client().count(filter).await.unwrap(), 0);
1443
1444        // A grant type without a dedicated column matches nothing
1445        let filter = OAuth2ClientFilter::new().with_grant_type(&GrantType::Implicit);
1446        assert_eq!(repo.oauth2_client().count(filter).await.unwrap(), 0);
1447    }
1448
1449    /// Test the active-sessions filter on [`OAuth2ClientFilter`].
1450    #[sqlx::test(migrator = "crate::MIGRATOR")]
1451    async fn test_list_clients_by_active_sessions(pool: PgPool) {
1452        let mut rng = ChaChaRng::seed_from_u64(42);
1453        let clock = MockClock::default();
1454        let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
1455
1456        // A client that will have an active session
1457        let with_session = repo
1458            .oauth2_client()
1459            .add(
1460                &mut rng,
1461                &clock,
1462                vec![],
1463                None,
1464                None,
1465                None,
1466                vec![GrantType::ClientCredentials],
1467                Some("Client with session".to_owned()),
1468                None,
1469                None,
1470                None,
1471                None,
1472                None,
1473                None,
1474                None,
1475                None,
1476                None,
1477                None,
1478                None,
1479            )
1480            .await
1481            .unwrap();
1482
1483        // A client without any session
1484        let without_session = repo
1485            .oauth2_client()
1486            .add(
1487                &mut rng,
1488                &clock,
1489                vec![],
1490                None,
1491                None,
1492                None,
1493                vec![GrantType::ClientCredentials],
1494                Some("Client without session".to_owned()),
1495                None,
1496                None,
1497                None,
1498                None,
1499                None,
1500                None,
1501                None,
1502                None,
1503                None,
1504                None,
1505                None,
1506            )
1507            .await
1508            .unwrap();
1509
1510        let session = repo
1511            .oauth2_session()
1512            .add_from_client_credentials(
1513                &mut rng,
1514                &clock,
1515                &with_session,
1516                Scope::from_iter([OPENID]),
1517            )
1518            .await
1519            .unwrap();
1520
1521        // Has an active session: only the first client
1522        let filter = OAuth2ClientFilter::new().with_active_sessions(true);
1523        assert_eq!(repo.oauth2_client().count(filter).await.unwrap(), 1);
1524        let page = repo
1525            .oauth2_client()
1526            .list(filter, Pagination::first(10))
1527            .await
1528            .unwrap();
1529        assert_eq!(page.edges.len(), 1);
1530        assert_eq!(page.edges[0].node, with_session);
1531
1532        // Has no active session: only the second client
1533        let filter = OAuth2ClientFilter::new().with_active_sessions(false);
1534        assert_eq!(repo.oauth2_client().count(filter).await.unwrap(), 1);
1535        let page = repo
1536            .oauth2_client()
1537            .list(filter, Pagination::first(10))
1538            .await
1539            .unwrap();
1540        assert_eq!(page.edges.len(), 1);
1541        assert_eq!(page.edges[0].node, without_session);
1542
1543        // Once the session is finished, the first client no longer has one
1544        repo.oauth2_session().finish(&clock, session).await.unwrap();
1545
1546        let filter = OAuth2ClientFilter::new().with_active_sessions(true);
1547        assert_eq!(repo.oauth2_client().count(filter).await.unwrap(), 0);
1548        let filter = OAuth2ClientFilter::new().with_active_sessions(false);
1549        assert_eq!(repo.oauth2_client().count(filter).await.unwrap(), 2);
1550    }
1551}