Skip to main content

mas_storage/oauth2/
client.rs

1// Copyright 2025, 2026 Element Creations Ltd.
2// Copyright 2024, 2025 New Vector Ltd.
3// Copyright 2022-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
8use std::collections::{BTreeMap, BTreeSet};
9
10use async_trait::async_trait;
11use mas_data_model::{Client, Clock};
12use mas_iana::{jose::JsonWebSignatureAlg, oauth::OAuthClientAuthenticationMethod};
13use mas_jose::jwk::PublicJsonWebKeySet;
14use oauth2_types::{oidc::ApplicationType, requests::GrantType};
15use rand_core::Rng;
16use ulid::Ulid;
17use url::Url;
18
19use crate::{Page, Pagination, repository_impl};
20
21/// The kind of OAuth 2.0 client, used by [`OAuth2ClientFilter`]
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23pub enum OAuth2ClientKind {
24    /// Static clients, declared in the configuration file
25    Static,
26
27    /// Dynamic clients, registered through the dynamic client registration
28    /// endpoint
29    Dynamic,
30}
31
32/// Filter parameters for listing OAuth 2.0 clients
33#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
34pub struct OAuth2ClientFilter<'a> {
35    kind: Option<OAuth2ClientKind>,
36    client_name: Option<&'a str>,
37    client_uri: Option<&'a str>,
38    grant_type: Option<&'a GrantType>,
39    has_active_sessions: Option<bool>,
40}
41
42impl<'a> OAuth2ClientFilter<'a> {
43    /// Create a new [`OAuth2ClientFilter`] with default values
44    #[must_use]
45    pub fn new() -> Self {
46        Self::default()
47    }
48
49    /// Only return static clients (those declared in the configuration)
50    #[must_use]
51    pub fn only_static_clients(mut self) -> Self {
52        self.kind = Some(OAuth2ClientKind::Static);
53        self
54    }
55
56    /// Only return dynamic clients (those registered via the
57    /// dynamic-client-registration endpoint)
58    #[must_use]
59    pub fn only_dynamic_clients(mut self) -> Self {
60        self.kind = Some(OAuth2ClientKind::Dynamic);
61        self
62    }
63
64    /// Get the client kind filter
65    ///
66    /// Returns [`None`] if no client kind filter was set
67    #[must_use]
68    pub fn kind(&self) -> Option<OAuth2ClientKind> {
69        self.kind
70    }
71
72    /// Only return clients whose `client_name` matches the given substring
73    /// (case-insensitive)
74    #[must_use]
75    pub fn matching_client_name(mut self, client_name: &'a str) -> Self {
76        self.client_name = Some(client_name);
77        self
78    }
79
80    /// Get the client name filter
81    ///
82    /// Returns [`None`] if no client name filter was set
83    #[must_use]
84    pub fn client_name(&self) -> Option<&'a str> {
85        self.client_name
86    }
87
88    /// Only return clients whose `client_uri` matches the given substring
89    /// (case-insensitive)
90    #[must_use]
91    pub fn matching_client_uri(mut self, client_uri: &'a str) -> Self {
92        self.client_uri = Some(client_uri);
93        self
94    }
95
96    /// Get the client URI filter
97    ///
98    /// Returns [`None`] if no client URI filter was set
99    #[must_use]
100    pub fn client_uri(&self) -> Option<&'a str> {
101        self.client_uri
102    }
103
104    /// Only return clients which support the given grant type
105    #[must_use]
106    pub fn with_grant_type(mut self, grant_type: &'a GrantType) -> Self {
107        self.grant_type = Some(grant_type);
108        self
109    }
110
111    /// Get the grant type filter
112    ///
113    /// Returns [`None`] if no grant type filter was set
114    #[must_use]
115    pub fn grant_type(&self) -> Option<&'a GrantType> {
116        self.grant_type
117    }
118
119    /// Only return clients which have (or don't have) at least one active
120    /// (non-finished) `OAuth2` session
121    #[must_use]
122    pub fn with_active_sessions(mut self, has_active_sessions: bool) -> Self {
123        self.has_active_sessions = Some(has_active_sessions);
124        self
125    }
126
127    /// Get the active-sessions filter
128    ///
129    /// Returns [`None`] if no active-sessions filter was set
130    #[must_use]
131    pub fn has_active_sessions(&self) -> Option<bool> {
132        self.has_active_sessions
133    }
134}
135
136/// An [`OAuth2ClientRepository`] helps interacting with [`Client`] saved in the
137/// storage backend
138#[async_trait]
139pub trait OAuth2ClientRepository: Send + Sync {
140    /// The error type returned by the repository
141    type Error;
142
143    /// Lookup an OAuth client by its ID
144    ///
145    /// Returns `None` if the client does not exist
146    ///
147    /// # Parameters
148    ///
149    /// * `id`: The ID of the client to lookup
150    ///
151    /// # Errors
152    ///
153    /// Returns [`Self::Error`] if the underlying repository fails
154    async fn lookup(&mut self, id: Ulid) -> Result<Option<Client>, Self::Error>;
155
156    /// Find an OAuth client by its client ID
157    async fn find_by_client_id(&mut self, client_id: &str) -> Result<Option<Client>, Self::Error> {
158        let Ok(id) = client_id.parse() else {
159            return Ok(None);
160        };
161        self.lookup(id).await
162    }
163
164    /// Load a batch of OAuth clients by their IDs
165    ///
166    /// Returns a map of client IDs to clients. If a client does not exist, it
167    /// is not present in the map.
168    ///
169    /// # Parameters
170    ///
171    /// * `ids`: The IDs of the clients to load
172    ///
173    /// # Errors
174    ///
175    /// Returns [`Self::Error`] if the underlying repository fails
176    async fn load_batch(
177        &mut self,
178        ids: BTreeSet<Ulid>,
179    ) -> Result<BTreeMap<Ulid, Client>, Self::Error>;
180
181    /// Add a new OAuth client, or reuse the existing one with the same metadata
182    /// digest
183    ///
184    /// Returns the client and whether it was inserted. If `metadata_digest` is
185    /// set and a client with the same digest already exists, that client is
186    /// returned unchanged instead.
187    ///
188    /// # Parameters
189    ///
190    /// * `rng`: The random number generator to use
191    /// * `clock`: The clock used to generate timestamps
192    /// * `redirect_uris`: The list of redirect URIs used by this client
193    /// * `metadata_digest`: The hash of the client metadata, if computed
194    /// * `encrypted_client_secret`: The encrypted client secret, if any
195    /// * `application_type`: The application type of this client
196    /// * `grant_types`: The list of grant types this client can use
197    /// * `client_name`: The human-readable name of this client, if given
198    /// * `logo_uri`: The URI of the logo of this client, if given
199    /// * `client_uri`: The URI of a website of this client, if given
200    /// * `policy_uri`: The URI of the privacy policy of this client, if given
201    /// * `tos_uri`: The URI of the terms of service of this client, if given
202    /// * `jwks_uri`: The URI of the JWKS of this client, if given
203    /// * `jwks`: The JWKS of this client, if given
204    /// * `id_token_signed_response_alg`: The algorithm used to sign the ID
205    ///   token
206    /// * `userinfo_signed_response_alg`: The algorithm used to sign the user
207    ///   info. If none, the user info endpoint will not sign the response
208    /// * `token_endpoint_auth_method`: The authentication method used by this
209    ///   client when calling the token endpoint
210    /// * `token_endpoint_auth_signing_alg`: The algorithm used to sign the JWT
211    ///   when using the `client_secret_jwt` or `private_key_jwt` authentication
212    ///   methods
213    /// * `initiate_login_uri`: The URI used to initiate a login, if given
214    ///
215    /// # Errors
216    ///
217    /// Returns [`Self::Error`] if the underlying repository fails
218    #[expect(clippy::too_many_arguments)]
219    async fn add_or_reuse(
220        &mut self,
221        rng: &mut (dyn Rng + Send),
222        clock: &dyn Clock,
223        redirect_uris: Vec<Url>,
224        metadata_digest: Option<String>,
225        encrypted_client_secret: Option<String>,
226        application_type: Option<ApplicationType>,
227        grant_types: Vec<GrantType>,
228        client_name: Option<String>,
229        logo_uri: Option<Url>,
230        client_uri: Option<Url>,
231        policy_uri: Option<Url>,
232        tos_uri: Option<Url>,
233        jwks_uri: Option<Url>,
234        jwks: Option<PublicJsonWebKeySet>,
235        id_token_signed_response_alg: Option<JsonWebSignatureAlg>,
236        userinfo_signed_response_alg: Option<JsonWebSignatureAlg>,
237        token_endpoint_auth_method: Option<OAuthClientAuthenticationMethod>,
238        token_endpoint_auth_signing_alg: Option<JsonWebSignatureAlg>,
239        initiate_login_uri: Option<Url>,
240    ) -> Result<(Client, bool), Self::Error>;
241
242    /// Add or replace a static client
243    ///
244    /// Returns the client that was added or replaced
245    ///
246    /// # Parameters
247    ///
248    /// * `client_id`: The client ID
249    /// * `client_name`: Name of the `OAuth2` client
250    /// * `client_uri`: User-facing information about the client
251    /// * `client_auth_method`: The authentication method this client uses
252    /// * `encrypted_client_secret`: The encrypted client secret, if any
253    /// * `jwks`: The client JWKS, if any
254    /// * `jwks_uri`: The client JWKS URI, if any
255    /// * `redirect_uris`: The list of redirect URIs used by this client
256    ///
257    /// # Errors
258    ///
259    /// Returns [`Self::Error`] if the underlying repository fails
260    #[expect(clippy::too_many_arguments)]
261    async fn upsert_static(
262        &mut self,
263        client_id: Ulid,
264        client_name: Option<String>,
265        client_uri: Option<Url>,
266        client_auth_method: OAuthClientAuthenticationMethod,
267        encrypted_client_secret: Option<String>,
268        jwks: Option<PublicJsonWebKeySet>,
269        jwks_uri: Option<Url>,
270        redirect_uris: Vec<Url>,
271    ) -> Result<Client, Self::Error>;
272
273    /// List all static clients
274    ///
275    /// # Errors
276    ///
277    /// Returns [`Self::Error`] if the underlying repository fails
278    async fn all_static(&mut self) -> Result<Vec<Client>, Self::Error>;
279
280    /// Delete a client
281    ///
282    /// # Parameters
283    ///
284    /// * `client`: The client to delete
285    ///
286    /// # Errors
287    ///
288    /// Returns [`Self::Error`] if the underlying repository fails, or if the
289    /// client does not exist
290    async fn delete(&mut self, client: Client) -> Result<(), Self::Error> {
291        self.delete_by_id(client.id).await
292    }
293
294    /// Delete a client by ID
295    ///
296    /// # Parameters
297    ///
298    /// * `id`: The ID of the client to delete
299    ///
300    /// # Errors
301    ///
302    /// Returns [`Self::Error`] if the underlying repository fails, or if the
303    /// client does not exist
304    async fn delete_by_id(&mut self, id: Ulid) -> Result<(), Self::Error>;
305
306    /// List [`Client`] with the given filter and pagination
307    ///
308    /// # Parameters
309    ///
310    /// * `filter`: The filter parameters
311    /// * `pagination`: The pagination parameters
312    ///
313    /// # Errors
314    ///
315    /// Returns [`Self::Error`] if the underlying repository fails
316    async fn list(
317        &mut self,
318        filter: OAuth2ClientFilter<'_>,
319        pagination: Pagination,
320    ) -> Result<Page<Client>, Self::Error>;
321
322    /// Count the [`Client`] with the given filter
323    ///
324    /// # Parameters
325    ///
326    /// * `filter`: The filter parameters
327    ///
328    /// # Errors
329    ///
330    /// Returns [`Self::Error`] if the underlying repository fails
331    async fn count(&mut self, filter: OAuth2ClientFilter<'_>) -> Result<usize, Self::Error>;
332}
333
334repository_impl!(OAuth2ClientRepository:
335    async fn lookup(&mut self, id: Ulid) -> Result<Option<Client>, Self::Error>;
336
337    async fn load_batch(
338        &mut self,
339        ids: BTreeSet<Ulid>,
340    ) -> Result<BTreeMap<Ulid, Client>, Self::Error>;
341
342    async fn add_or_reuse(
343        &mut self,
344        rng: &mut (dyn Rng + Send),
345        clock: &dyn Clock,
346        redirect_uris: Vec<Url>,
347        metadata_digest: Option<String>,
348        encrypted_client_secret: Option<String>,
349        application_type: Option<ApplicationType>,
350        grant_types: Vec<GrantType>,
351        client_name: Option<String>,
352        logo_uri: Option<Url>,
353        client_uri: Option<Url>,
354        policy_uri: Option<Url>,
355        tos_uri: Option<Url>,
356        jwks_uri: Option<Url>,
357        jwks: Option<PublicJsonWebKeySet>,
358        id_token_signed_response_alg: Option<JsonWebSignatureAlg>,
359        userinfo_signed_response_alg: Option<JsonWebSignatureAlg>,
360        token_endpoint_auth_method: Option<OAuthClientAuthenticationMethod>,
361        token_endpoint_auth_signing_alg: Option<JsonWebSignatureAlg>,
362        initiate_login_uri: Option<Url>,
363    ) -> Result<(Client, bool), Self::Error>;
364
365    async fn upsert_static(
366        &mut self,
367        client_id: Ulid,
368        client_name: Option<String>,
369        client_uri: Option<Url>,
370        client_auth_method: OAuthClientAuthenticationMethod,
371        encrypted_client_secret: Option<String>,
372        jwks: Option<PublicJsonWebKeySet>,
373        jwks_uri: Option<Url>,
374        redirect_uris: Vec<Url>,
375    ) -> Result<Client, Self::Error>;
376
377    async fn all_static(&mut self) -> Result<Vec<Client>, Self::Error>;
378
379    async fn delete(&mut self, client: Client) -> Result<(), Self::Error>;
380
381    async fn delete_by_id(&mut self, id: Ulid) -> Result<(), Self::Error>;
382
383    async fn list(
384        &mut self,
385        filter: OAuth2ClientFilter<'_>,
386        pagination: Pagination,
387    ) -> Result<Page<Client>, Self::Error>;
388
389    async fn count(&mut self, filter: OAuth2ClientFilter<'_>) -> Result<usize, Self::Error>;
390);