mas_storage/oauth2/client.rs
1// Copyright 2024 New Vector Ltd.
2// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
3//
4// SPDX-License-Identifier: AGPL-3.0-only
5// Please see LICENSE in the repository root for full details.
6
7use std::collections::{BTreeMap, BTreeSet};
8
9use async_trait::async_trait;
10use mas_data_model::{Client, User};
11use mas_iana::{jose::JsonWebSignatureAlg, oauth::OAuthClientAuthenticationMethod};
12use mas_jose::jwk::PublicJsonWebKeySet;
13use oauth2_types::{oidc::ApplicationType, requests::GrantType, scope::Scope};
14use rand_core::RngCore;
15use ulid::Ulid;
16use url::Url;
17
18use crate::{Clock, repository_impl};
19
20/// An [`OAuth2ClientRepository`] helps interacting with [`Client`] saved in the
21/// storage backend
22#[async_trait]
23pub trait OAuth2ClientRepository: Send + Sync {
24 /// The error type returned by the repository
25 type Error;
26
27 /// Lookup an OAuth2 client by its ID
28 ///
29 /// Returns `None` if the client does not exist
30 ///
31 /// # Parameters
32 ///
33 /// * `id`: The ID of the client to lookup
34 ///
35 /// # Errors
36 ///
37 /// Returns [`Self::Error`] if the underlying repository fails
38 async fn lookup(&mut self, id: Ulid) -> Result<Option<Client>, Self::Error>;
39
40 /// Find an OAuth2 client by its client ID
41 async fn find_by_client_id(&mut self, client_id: &str) -> Result<Option<Client>, Self::Error> {
42 let Ok(id) = client_id.parse() else {
43 return Ok(None);
44 };
45 self.lookup(id).await
46 }
47
48 /// Load a batch of OAuth2 clients by their IDs
49 ///
50 /// Returns a map of client IDs to clients. If a client does not exist, it
51 /// is not present in the map.
52 ///
53 /// # Parameters
54 ///
55 /// * `ids`: The IDs of the clients to load
56 ///
57 /// # Errors
58 ///
59 /// Returns [`Self::Error`] if the underlying repository fails
60 async fn load_batch(
61 &mut self,
62 ids: BTreeSet<Ulid>,
63 ) -> Result<BTreeMap<Ulid, Client>, Self::Error>;
64
65 /// Add a new OAuth2 client
66 ///
67 /// Returns the client that was added
68 ///
69 /// # Parameters
70 ///
71 /// * `rng`: The random number generator to use
72 /// * `clock`: The clock used to generate timestamps
73 /// * `redirect_uris`: The list of redirect URIs used by this client
74 /// * `encrypted_client_secret`: The encrypted client secret, if any
75 /// * `application_type`: The application type of this client
76 /// * `grant_types`: The list of grant types this client can use
77 /// * `client_name`: The human-readable name of this client, if given
78 /// * `logo_uri`: The URI of the logo of this client, if given
79 /// * `client_uri`: The URI of a website of this client, if given
80 /// * `policy_uri`: The URI of the privacy policy of this client, if given
81 /// * `tos_uri`: The URI of the terms of service of this client, if given
82 /// * `jwks_uri`: The URI of the JWKS of this client, if given
83 /// * `jwks`: The JWKS of this client, if given
84 /// * `id_token_signed_response_alg`: The algorithm used to sign the ID
85 /// token
86 /// * `userinfo_signed_response_alg`: The algorithm used to sign the user
87 /// info. If none, the user info endpoint will not sign the response
88 /// * `token_endpoint_auth_method`: The authentication method used by this
89 /// client when calling the token endpoint
90 /// * `token_endpoint_auth_signing_alg`: The algorithm used to sign the JWT
91 /// when using the `client_secret_jwt` or `private_key_jwt` authentication
92 /// methods
93 /// * `initiate_login_uri`: The URI used to initiate a login, if given
94 ///
95 /// # Errors
96 ///
97 /// Returns [`Self::Error`] if the underlying repository fails
98 #[allow(clippy::too_many_arguments)]
99 async fn add(
100 &mut self,
101 rng: &mut (dyn RngCore + Send),
102 clock: &dyn Clock,
103 redirect_uris: Vec<Url>,
104 encrypted_client_secret: Option<String>,
105 application_type: Option<ApplicationType>,
106 grant_types: Vec<GrantType>,
107 client_name: Option<String>,
108 logo_uri: Option<Url>,
109 client_uri: Option<Url>,
110 policy_uri: Option<Url>,
111 tos_uri: Option<Url>,
112 jwks_uri: Option<Url>,
113 jwks: Option<PublicJsonWebKeySet>,
114 id_token_signed_response_alg: Option<JsonWebSignatureAlg>,
115 userinfo_signed_response_alg: Option<JsonWebSignatureAlg>,
116 token_endpoint_auth_method: Option<OAuthClientAuthenticationMethod>,
117 token_endpoint_auth_signing_alg: Option<JsonWebSignatureAlg>,
118 initiate_login_uri: Option<Url>,
119 ) -> Result<Client, Self::Error>;
120
121 /// Add or replace a static client
122 ///
123 /// Returns the client that was added or replaced
124 ///
125 /// # Parameters
126 ///
127 /// * `client_id`: The client ID
128 /// * `client_auth_method`: The authentication method this client uses
129 /// * `encrypted_client_secret`: The encrypted client secret, if any
130 /// * `jwks`: The client JWKS, if any
131 /// * `jwks_uri`: The client JWKS URI, if any
132 /// * `redirect_uris`: The list of redirect URIs used by this client
133 ///
134 /// # Errors
135 ///
136 /// Returns [`Self::Error`] if the underlying repository fails
137 #[allow(clippy::too_many_arguments)]
138 async fn upsert_static(
139 &mut self,
140 client_id: Ulid,
141 client_auth_method: OAuthClientAuthenticationMethod,
142 encrypted_client_secret: Option<String>,
143 jwks: Option<PublicJsonWebKeySet>,
144 jwks_uri: Option<Url>,
145 redirect_uris: Vec<Url>,
146 ) -> Result<Client, Self::Error>;
147
148 /// List all static clients
149 ///
150 /// # Errors
151 ///
152 /// Returns [`Self::Error`] if the underlying repository fails
153 async fn all_static(&mut self) -> Result<Vec<Client>, Self::Error>;
154
155 /// Get the list of scopes that the user has given consent for the given
156 /// client
157 ///
158 /// # Parameters
159 ///
160 /// * `client`: The client to get the consent for
161 /// * `user`: The user to get the consent for
162 ///
163 /// # Errors
164 ///
165 /// Returns [`Self::Error`] if the underlying repository fails
166 async fn get_consent_for_user(
167 &mut self,
168 client: &Client,
169 user: &User,
170 ) -> Result<Scope, Self::Error>;
171
172 /// Give consent for a set of scopes for the given client and user
173 ///
174 /// # Parameters
175 ///
176 /// * `rng`: The random number generator to use
177 /// * `clock`: The clock used to generate timestamps
178 /// * `client`: The client to give the consent for
179 /// * `user`: The user to give the consent for
180 /// * `scope`: The scope to give consent for
181 ///
182 /// # Errors
183 ///
184 /// Returns [`Self::Error`] if the underlying repository fails
185 async fn give_consent_for_user(
186 &mut self,
187 rng: &mut (dyn RngCore + Send),
188 clock: &dyn Clock,
189 client: &Client,
190 user: &User,
191 scope: &Scope,
192 ) -> Result<(), Self::Error>;
193
194 /// Delete a client
195 ///
196 /// # Parameters
197 ///
198 /// * `client`: The client to delete
199 ///
200 /// # Errors
201 ///
202 /// Returns [`Self::Error`] if the underlying repository fails, or if the
203 /// client does not exist
204 async fn delete(&mut self, client: Client) -> Result<(), Self::Error> {
205 self.delete_by_id(client.id).await
206 }
207
208 /// Delete a client by ID
209 ///
210 /// # Parameters
211 ///
212 /// * `id`: The ID of the client to delete
213 ///
214 /// # Errors
215 ///
216 /// Returns [`Self::Error`] if the underlying repository fails, or if the
217 /// client does not exist
218 async fn delete_by_id(&mut self, id: Ulid) -> Result<(), Self::Error>;
219}
220
221repository_impl!(OAuth2ClientRepository:
222 async fn lookup(&mut self, id: Ulid) -> Result<Option<Client>, Self::Error>;
223
224 async fn load_batch(
225 &mut self,
226 ids: BTreeSet<Ulid>,
227 ) -> Result<BTreeMap<Ulid, Client>, Self::Error>;
228
229 async fn add(
230 &mut self,
231 rng: &mut (dyn RngCore + Send),
232 clock: &dyn Clock,
233 redirect_uris: Vec<Url>,
234 encrypted_client_secret: Option<String>,
235 application_type: Option<ApplicationType>,
236 grant_types: Vec<GrantType>,
237 client_name: Option<String>,
238 logo_uri: Option<Url>,
239 client_uri: Option<Url>,
240 policy_uri: Option<Url>,
241 tos_uri: Option<Url>,
242 jwks_uri: Option<Url>,
243 jwks: Option<PublicJsonWebKeySet>,
244 id_token_signed_response_alg: Option<JsonWebSignatureAlg>,
245 userinfo_signed_response_alg: Option<JsonWebSignatureAlg>,
246 token_endpoint_auth_method: Option<OAuthClientAuthenticationMethod>,
247 token_endpoint_auth_signing_alg: Option<JsonWebSignatureAlg>,
248 initiate_login_uri: Option<Url>,
249 ) -> Result<Client, Self::Error>;
250
251 async fn upsert_static(
252 &mut self,
253 client_id: Ulid,
254 client_auth_method: OAuthClientAuthenticationMethod,
255 encrypted_client_secret: Option<String>,
256 jwks: Option<PublicJsonWebKeySet>,
257 jwks_uri: Option<Url>,
258 redirect_uris: Vec<Url>,
259 ) -> Result<Client, Self::Error>;
260
261 async fn all_static(&mut self) -> Result<Vec<Client>, Self::Error>;
262
263 async fn delete(&mut self, client: Client) -> Result<(), Self::Error>;
264
265 async fn delete_by_id(&mut self, id: Ulid) -> Result<(), Self::Error>;
266
267 async fn get_consent_for_user(
268 &mut self,
269 client: &Client,
270 user: &User,
271 ) -> Result<Scope, Self::Error>;
272
273 async fn give_consent_for_user(
274 &mut self,
275 rng: &mut (dyn RngCore + Send),
276 clock: &dyn Clock,
277 client: &Client,
278 user: &User,
279 scope: &Scope,
280 ) -> Result<(), Self::Error>;
281);