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, UlidExt as _, 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, with a locale captured from the browser
1059        let grant = repo
1060            .oauth2_device_code_grant()
1061            .fulfill(&clock, grant, &browser_session, Some("en".to_owned()))
1062            .await
1063            .unwrap();
1064        assert!(!grant.is_pending());
1065        assert!(grant.is_fulfilled());
1066        assert_eq!(grant.locale.as_deref(), Some("en"));
1067
1068        // Check that we can't mark it as rejected now
1069        let res = repo
1070            .oauth2_device_code_grant()
1071            .reject(&clock, grant, &browser_session)
1072            .await;
1073        assert!(res.is_err());
1074
1075        // Look it up again
1076        let grant = repo
1077            .oauth2_device_code_grant()
1078            .lookup(id)
1079            .await
1080            .unwrap()
1081            .unwrap();
1082
1083        // The locale was persisted
1084        assert_eq!(grant.locale.as_deref(), Some("en"));
1085
1086        // We can't mark it as fulfilled again
1087        let res = repo
1088            .oauth2_device_code_grant()
1089            .fulfill(&clock, grant, &browser_session, None)
1090            .await;
1091        assert!(res.is_err());
1092
1093        // Look it up again
1094        let grant = repo
1095            .oauth2_device_code_grant()
1096            .lookup(id)
1097            .await
1098            .unwrap()
1099            .unwrap();
1100
1101        // Create an OAuth 2.0 session
1102        let session = repo
1103            .oauth2_session()
1104            .add_from_browser_session(&mut rng, &clock, &client, &browser_session, scope.clone())
1105            .await
1106            .unwrap();
1107
1108        // We can mark it as exchanged
1109        let grant = repo
1110            .oauth2_device_code_grant()
1111            .exchange(&clock, grant, &session)
1112            .await
1113            .unwrap();
1114        assert!(!grant.is_pending());
1115        assert!(!grant.is_fulfilled());
1116        assert!(grant.is_exchanged());
1117
1118        // We can't mark it as exchanged again
1119        let res = repo
1120            .oauth2_device_code_grant()
1121            .exchange(&clock, grant, &session)
1122            .await;
1123        assert!(res.is_err());
1124
1125        // Do a new grant to reject it
1126        let grant = repo
1127            .oauth2_device_code_grant()
1128            .add(
1129                &mut rng,
1130                &clock,
1131                OAuth2DeviceCodeGrantParams {
1132                    client: &client,
1133                    scope: scope.clone(),
1134                    device_code: "second_devicecode".to_owned(),
1135                    user_code: "second_usercode".to_owned(),
1136                    expires_in: Duration::try_minutes(5).unwrap(),
1137                    ip_address: None,
1138                    user_agent: None,
1139                },
1140            )
1141            .await
1142            .unwrap();
1143
1144        let id = grant.id;
1145
1146        // We can mark it as rejected
1147        let grant = repo
1148            .oauth2_device_code_grant()
1149            .reject(&clock, grant, &browser_session)
1150            .await
1151            .unwrap();
1152        assert!(!grant.is_pending());
1153        assert!(grant.is_rejected());
1154
1155        // We can't mark it as rejected again
1156        let res = repo
1157            .oauth2_device_code_grant()
1158            .reject(&clock, grant, &browser_session)
1159            .await;
1160        assert!(res.is_err());
1161
1162        // Look it up again
1163        let grant = repo
1164            .oauth2_device_code_grant()
1165            .lookup(id)
1166            .await
1167            .unwrap()
1168            .unwrap();
1169
1170        // We can't mark it as fulfilled
1171        let res = repo
1172            .oauth2_device_code_grant()
1173            .fulfill(&clock, grant, &browser_session, None)
1174            .await;
1175        assert!(res.is_err());
1176
1177        // Look it up again
1178        let grant = repo
1179            .oauth2_device_code_grant()
1180            .lookup(id)
1181            .await
1182            .unwrap()
1183            .unwrap();
1184
1185        // We can't mark it as exchanged
1186        let res = repo
1187            .oauth2_device_code_grant()
1188            .exchange(&clock, grant, &session)
1189            .await;
1190        assert!(res.is_err());
1191    }
1192
1193    /// Test the [`OAuth2ClientRepository::list`] and
1194    /// [`OAuth2ClientRepository::count`] methods.
1195    #[sqlx::test(migrator = "crate::MIGRATOR")]
1196    async fn test_list_clients(pool: PgPool) {
1197        let mut rng = ChaChaRng::seed_from_u64(42);
1198        let clock = MockClock::default();
1199        let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
1200
1201        // Empty initially
1202        let filter = OAuth2ClientFilter::new();
1203        assert_eq!(repo.oauth2_client().count(filter).await.unwrap(), 0);
1204
1205        let page = repo
1206            .oauth2_client()
1207            .list(filter, Pagination::first(10))
1208            .await
1209            .unwrap();
1210        assert!(page.edges.is_empty());
1211        assert!(!page.has_next_page);
1212
1213        // Add a couple of clients
1214        let client1 = repo
1215            .oauth2_client()
1216            .add(
1217                &mut rng,
1218                &clock,
1219                vec!["https://first.example.com/redirect".parse().unwrap()],
1220                None,
1221                None,
1222                None,
1223                vec![GrantType::AuthorizationCode],
1224                Some("First client".to_owned()),
1225                None,
1226                Some("https://first.example.com/".parse().unwrap()),
1227                None,
1228                None,
1229                None,
1230                None,
1231                None,
1232                None,
1233                None,
1234                None,
1235                None,
1236            )
1237            .await
1238            .unwrap();
1239        clock.advance(Duration::try_minutes(1).unwrap());
1240
1241        let client2 = repo
1242            .oauth2_client()
1243            .add(
1244                &mut rng,
1245                &clock,
1246                vec!["https://second.example.com/redirect".parse().unwrap()],
1247                None,
1248                None,
1249                None,
1250                vec![GrantType::AuthorizationCode],
1251                Some("Second client".to_owned()),
1252                None,
1253                Some("https://second.example.com/".parse().unwrap()),
1254                None,
1255                None,
1256                None,
1257                None,
1258                None,
1259                None,
1260                None,
1261                None,
1262                None,
1263            )
1264            .await
1265            .unwrap();
1266
1267        assert_eq!(repo.oauth2_client().count(filter).await.unwrap(), 2);
1268
1269        let page = repo
1270            .oauth2_client()
1271            .list(filter, Pagination::first(10))
1272            .await
1273            .unwrap();
1274        assert!(!page.has_next_page);
1275        assert_eq!(page.edges.len(), 2);
1276        assert_eq!(page.edges[0].node, client1);
1277        assert_eq!(page.edges[1].node, client2);
1278
1279        // Add a static client
1280        let static_id = Ulid::from_datetime_with_rng(clock.now(), &mut rng);
1281        repo.oauth2_client()
1282            .upsert_static(
1283                static_id,
1284                Some("Static client".to_owned()),
1285                OAuthClientAuthenticationMethod::None,
1286                None,
1287                None,
1288                None,
1289                vec!["https://static.example.com/redirect".parse().unwrap()],
1290            )
1291            .await
1292            .unwrap();
1293        // Re-read via lookup so we have the canonical representation
1294        let static_client = repo
1295            .oauth2_client()
1296            .lookup(static_id)
1297            .await
1298            .unwrap()
1299            .expect("static client just inserted");
1300
1301        assert_eq!(repo.oauth2_client().count(filter).await.unwrap(), 3);
1302
1303        // Only static clients
1304        let filter = OAuth2ClientFilter::new().only_static_clients();
1305        assert_eq!(repo.oauth2_client().count(filter).await.unwrap(), 1);
1306        let page = repo
1307            .oauth2_client()
1308            .list(filter, Pagination::first(10))
1309            .await
1310            .unwrap();
1311        assert_eq!(page.edges.len(), 1);
1312        assert_eq!(page.edges[0].node, static_client);
1313
1314        // Only dynamic clients
1315        let filter = OAuth2ClientFilter::new().only_dynamic_clients();
1316        assert_eq!(repo.oauth2_client().count(filter).await.unwrap(), 2);
1317        let page = repo
1318            .oauth2_client()
1319            .list(filter, Pagination::first(10))
1320            .await
1321            .unwrap();
1322        assert_eq!(page.edges.len(), 2);
1323        assert_eq!(page.edges[0].node, client1);
1324        assert_eq!(page.edges[1].node, client2);
1325
1326        // Substring match on client_name
1327        let filter = OAuth2ClientFilter::new().matching_client_name("first");
1328        assert_eq!(repo.oauth2_client().count(filter).await.unwrap(), 1);
1329        let page = repo
1330            .oauth2_client()
1331            .list(filter, Pagination::first(10))
1332            .await
1333            .unwrap();
1334        assert_eq!(page.edges.len(), 1);
1335        assert_eq!(page.edges[0].node, client1);
1336
1337        // Case-insensitive match on client_name
1338        let filter = OAuth2ClientFilter::new().matching_client_name("CLIENT");
1339        assert_eq!(repo.oauth2_client().count(filter).await.unwrap(), 3);
1340
1341        // Substring match on client_uri
1342        let filter = OAuth2ClientFilter::new().matching_client_uri("second");
1343        assert_eq!(repo.oauth2_client().count(filter).await.unwrap(), 1);
1344        let page = repo
1345            .oauth2_client()
1346            .list(filter, Pagination::first(10))
1347            .await
1348            .unwrap();
1349        assert_eq!(page.edges.len(), 1);
1350        assert_eq!(page.edges[0].node, client2);
1351
1352        // Case-insensitive match on client_uri
1353        let filter = OAuth2ClientFilter::new().matching_client_uri("EXAMPLE.COM");
1354        assert_eq!(repo.oauth2_client().count(filter).await.unwrap(), 2);
1355    }
1356
1357    /// Test the grant-type filter on [`OAuth2ClientFilter`].
1358    #[sqlx::test(migrator = "crate::MIGRATOR")]
1359    async fn test_list_clients_by_grant_type(pool: PgPool) {
1360        let mut rng = ChaChaRng::seed_from_u64(42);
1361        let clock = MockClock::default();
1362        let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
1363
1364        // A client supporting authorization_code (+ refresh_token)
1365        let auth_code_client = repo
1366            .oauth2_client()
1367            .add(
1368                &mut rng,
1369                &clock,
1370                vec!["https://code.example.com/redirect".parse().unwrap()],
1371                None,
1372                None,
1373                None,
1374                vec![GrantType::AuthorizationCode, GrantType::RefreshToken],
1375                Some("Authorization code client".to_owned()),
1376                None,
1377                None,
1378                None,
1379                None,
1380                None,
1381                None,
1382                None,
1383                None,
1384                None,
1385                None,
1386                None,
1387            )
1388            .await
1389            .unwrap();
1390
1391        // A client supporting only client_credentials
1392        let client_credentials_client = repo
1393            .oauth2_client()
1394            .add(
1395                &mut rng,
1396                &clock,
1397                vec![],
1398                None,
1399                None,
1400                None,
1401                vec![GrantType::ClientCredentials],
1402                Some("Client credentials client".to_owned()),
1403                None,
1404                None,
1405                None,
1406                None,
1407                None,
1408                None,
1409                None,
1410                None,
1411                None,
1412                None,
1413                None,
1414            )
1415            .await
1416            .unwrap();
1417
1418        // authorization_code: only the first client
1419        let filter = OAuth2ClientFilter::new().with_grant_type(&GrantType::AuthorizationCode);
1420        assert_eq!(repo.oauth2_client().count(filter).await.unwrap(), 1);
1421        let page = repo
1422            .oauth2_client()
1423            .list(filter, Pagination::first(10))
1424            .await
1425            .unwrap();
1426        assert_eq!(page.edges.len(), 1);
1427        assert_eq!(page.edges[0].node, auth_code_client);
1428
1429        // client_credentials: only the second client
1430        let filter = OAuth2ClientFilter::new().with_grant_type(&GrantType::ClientCredentials);
1431        assert_eq!(repo.oauth2_client().count(filter).await.unwrap(), 1);
1432        let page = repo
1433            .oauth2_client()
1434            .list(filter, Pagination::first(10))
1435            .await
1436            .unwrap();
1437        assert_eq!(page.edges.len(), 1);
1438        assert_eq!(page.edges[0].node, client_credentials_client);
1439
1440        // refresh_token: only the first client
1441        let filter = OAuth2ClientFilter::new().with_grant_type(&GrantType::RefreshToken);
1442        assert_eq!(repo.oauth2_client().count(filter).await.unwrap(), 1);
1443
1444        // device_code: no client supports it
1445        let filter = OAuth2ClientFilter::new().with_grant_type(&GrantType::DeviceCode);
1446        assert_eq!(repo.oauth2_client().count(filter).await.unwrap(), 0);
1447
1448        // A grant type without a dedicated column matches nothing
1449        let filter = OAuth2ClientFilter::new().with_grant_type(&GrantType::Implicit);
1450        assert_eq!(repo.oauth2_client().count(filter).await.unwrap(), 0);
1451    }
1452
1453    /// Test the active-sessions filter on [`OAuth2ClientFilter`].
1454    #[sqlx::test(migrator = "crate::MIGRATOR")]
1455    async fn test_list_clients_by_active_sessions(pool: PgPool) {
1456        let mut rng = ChaChaRng::seed_from_u64(42);
1457        let clock = MockClock::default();
1458        let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
1459
1460        // A client that will have an active session
1461        let with_session = repo
1462            .oauth2_client()
1463            .add(
1464                &mut rng,
1465                &clock,
1466                vec![],
1467                None,
1468                None,
1469                None,
1470                vec![GrantType::ClientCredentials],
1471                Some("Client with session".to_owned()),
1472                None,
1473                None,
1474                None,
1475                None,
1476                None,
1477                None,
1478                None,
1479                None,
1480                None,
1481                None,
1482                None,
1483            )
1484            .await
1485            .unwrap();
1486
1487        // A client without any session
1488        let without_session = repo
1489            .oauth2_client()
1490            .add(
1491                &mut rng,
1492                &clock,
1493                vec![],
1494                None,
1495                None,
1496                None,
1497                vec![GrantType::ClientCredentials],
1498                Some("Client without session".to_owned()),
1499                None,
1500                None,
1501                None,
1502                None,
1503                None,
1504                None,
1505                None,
1506                None,
1507                None,
1508                None,
1509                None,
1510            )
1511            .await
1512            .unwrap();
1513
1514        let session = repo
1515            .oauth2_session()
1516            .add_from_client_credentials(
1517                &mut rng,
1518                &clock,
1519                &with_session,
1520                Scope::from_iter([OPENID]),
1521            )
1522            .await
1523            .unwrap();
1524
1525        // Has an active session: only the first client
1526        let filter = OAuth2ClientFilter::new().with_active_sessions(true);
1527        assert_eq!(repo.oauth2_client().count(filter).await.unwrap(), 1);
1528        let page = repo
1529            .oauth2_client()
1530            .list(filter, Pagination::first(10))
1531            .await
1532            .unwrap();
1533        assert_eq!(page.edges.len(), 1);
1534        assert_eq!(page.edges[0].node, with_session);
1535
1536        // Has no active session: only the second client
1537        let filter = OAuth2ClientFilter::new().with_active_sessions(false);
1538        assert_eq!(repo.oauth2_client().count(filter).await.unwrap(), 1);
1539        let page = repo
1540            .oauth2_client()
1541            .list(filter, Pagination::first(10))
1542            .await
1543            .unwrap();
1544        assert_eq!(page.edges.len(), 1);
1545        assert_eq!(page.edges[0].node, without_session);
1546
1547        // Once the session is finished, the first client no longer has one
1548        repo.oauth2_session().finish(&clock, session).await.unwrap();
1549
1550        let filter = OAuth2ClientFilter::new().with_active_sessions(true);
1551        assert_eq!(repo.oauth2_client().count(filter).await.unwrap(), 0);
1552        let filter = OAuth2ClientFilter::new().with_active_sessions(false);
1553        assert_eq!(repo.oauth2_client().count(filter).await.unwrap(), 2);
1554    }
1555}