Skip to main content

mas_data_model/oauth2/
client.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
8use chrono::{DateTime, Utc};
9use mas_iana::{jose::JsonWebSignatureAlg, oauth::OAuthClientAuthenticationMethod};
10use mas_jose::jwk::PublicJsonWebKeySet;
11use oauth2_types::{
12    oidc::ApplicationType,
13    registration::{ClientMetadata, Localized},
14    requests::GrantType,
15};
16use rand::RngCore;
17use serde::Serialize;
18use thiserror::Error;
19use ulid::Ulid;
20use url::Url;
21
22use crate::UlidExt as _;
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
25#[serde(rename_all = "snake_case")]
26pub enum JwksOrJwksUri {
27    /// Client's JSON Web Key Set document, passed by value.
28    Jwks(PublicJsonWebKeySet),
29
30    /// URL for the Client's JSON Web Key Set document.
31    JwksUri(Url),
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
35pub struct Client {
36    pub id: Ulid,
37
38    /// Client identifier
39    pub client_id: String,
40
41    /// Hash of the client metadata
42    pub metadata_digest: Option<String>,
43
44    pub encrypted_client_secret: Option<String>,
45
46    pub application_type: Option<ApplicationType>,
47
48    /// Array of Redirection URI values used by the Client
49    pub redirect_uris: Vec<Url>,
50
51    /// Array containing a list of the OAuth 2.0 Grant Types that the Client is
52    /// declaring that it will restrict itself to using.
53    pub grant_types: Vec<GrantType>,
54
55    /// Name of the Client to be presented to the End-User
56    pub client_name: Option<String>, // TODO: translations
57
58    /// URL that references a logo for the Client application
59    pub logo_uri: Option<Url>, // TODO: translations
60
61    /// URL of the home page of the Client
62    pub client_uri: Option<Url>, // TODO: translations
63
64    /// URL that the Relying Party Client provides to the End-User to read about
65    /// the how the profile data will be used
66    pub policy_uri: Option<Url>, // TODO: translations
67
68    /// URL that the Relying Party Client provides to the End-User to read about
69    /// the Relying Party's terms of service
70    pub tos_uri: Option<Url>, // TODO: translations
71
72    pub jwks: Option<JwksOrJwksUri>,
73
74    /// JWS alg algorithm REQUIRED for signing the ID Token issued to this
75    /// Client
76    pub id_token_signed_response_alg: Option<JsonWebSignatureAlg>,
77
78    /// JWS alg algorithm REQUIRED for signing `UserInfo` Responses.
79    pub userinfo_signed_response_alg: Option<JsonWebSignatureAlg>,
80
81    /// Requested authentication method for the token endpoint
82    pub token_endpoint_auth_method: Option<OAuthClientAuthenticationMethod>,
83
84    /// JWS alg algorithm that MUST be used for signing the JWT used to
85    /// authenticate the Client at the Token Endpoint for the `private_key_jwt`
86    /// and `client_secret_jwt` authentication methods
87    pub token_endpoint_auth_signing_alg: Option<JsonWebSignatureAlg>,
88
89    /// URI using the https scheme that a third party can use to initiate a
90    /// login by the RP
91    pub initiate_login_uri: Option<Url>,
92
93    /// Whether this client is statically configured (defined in the
94    /// configuration file) rather than dynamically registered.
95    pub is_static: bool,
96}
97
98#[derive(Debug, Error)]
99pub enum InvalidRedirectUriError {
100    #[error("redirect_uri is not allowed for this client")]
101    NotAllowed,
102
103    #[error("multiple redirect_uris registered for this client")]
104    MultipleRegistered,
105
106    #[error("client has no redirect_uri registered")]
107    NoneRegistered,
108}
109
110impl Client {
111    /// Determine which redirect URI to use for the given request.
112    ///
113    /// # Errors
114    ///
115    /// Returns an error if:
116    ///
117    ///  - no URL was given but multiple redirect URIs are registered,
118    ///  - no URL was registered, or
119    ///  - the given URL is not registered
120    pub fn resolve_redirect_uri<'a>(
121        &'a self,
122        redirect_uri: &'a Option<Url>,
123    ) -> Result<&'a Url, InvalidRedirectUriError> {
124        match (&self.redirect_uris[..], redirect_uri) {
125            ([], _) => Err(InvalidRedirectUriError::NoneRegistered),
126            ([one], None) => Ok(one),
127            (_, None) => Err(InvalidRedirectUriError::MultipleRegistered),
128            (uris, Some(uri)) if uri_matches_one_of(uri, uris) => Ok(uri),
129            _ => Err(InvalidRedirectUriError::NotAllowed),
130        }
131    }
132
133    /// Create a client metadata object for this client
134    #[must_use]
135    pub fn into_metadata(self) -> ClientMetadata {
136        let (jwks, jwks_uri) = match self.jwks {
137            Some(JwksOrJwksUri::Jwks(jwks)) => (Some(jwks), None),
138            Some(JwksOrJwksUri::JwksUri(jwks_uri)) => (None, Some(jwks_uri)),
139            _ => (None, None),
140        };
141        ClientMetadata {
142            redirect_uris: Some(self.redirect_uris.clone()),
143            response_types: None,
144            grant_types: Some(self.grant_types.clone()),
145            application_type: self.application_type.clone(),
146            client_name: self.client_name.map(|n| Localized::new(n, [])),
147            logo_uri: self.logo_uri.map(|n| Localized::new(n, [])),
148            client_uri: self.client_uri.map(|n| Localized::new(n, [])),
149            policy_uri: self.policy_uri.map(|n| Localized::new(n, [])),
150            tos_uri: self.tos_uri.map(|n| Localized::new(n, [])),
151            jwks_uri,
152            jwks,
153            id_token_signed_response_alg: self.id_token_signed_response_alg,
154            userinfo_signed_response_alg: self.userinfo_signed_response_alg,
155            token_endpoint_auth_method: self.token_endpoint_auth_method,
156            token_endpoint_auth_signing_alg: self.token_endpoint_auth_signing_alg,
157            initiate_login_uri: self.initiate_login_uri,
158            contacts: None,
159            software_id: None,
160            software_version: None,
161            sector_identifier_uri: None,
162            subject_type: None,
163            id_token_encrypted_response_alg: None,
164            id_token_encrypted_response_enc: None,
165            userinfo_encrypted_response_alg: None,
166            userinfo_encrypted_response_enc: None,
167            request_object_signing_alg: None,
168            request_object_encryption_alg: None,
169            request_object_encryption_enc: None,
170            default_max_age: None,
171            require_auth_time: None,
172            default_acr_values: None,
173            request_uris: None,
174            require_signed_request_object: None,
175            require_pushed_authorization_requests: None,
176            introspection_signed_response_alg: None,
177            introspection_encrypted_response_alg: None,
178            introspection_encrypted_response_enc: None,
179            post_logout_redirect_uris: None,
180        }
181    }
182
183    #[doc(hidden)]
184    pub fn samples(now: DateTime<Utc>, rng: &mut impl RngCore) -> Vec<Client> {
185        vec![
186            // A client with all the URIs set
187            Self {
188                id: Ulid::from_datetime_with_rng(now, rng),
189                client_id: "client1".to_owned(),
190                metadata_digest: None,
191                encrypted_client_secret: None,
192                application_type: Some(ApplicationType::Web),
193                redirect_uris: vec![
194                    Url::parse("https://client1.example.com/redirect").unwrap(),
195                    Url::parse("https://client1.example.com/redirect2").unwrap(),
196                ],
197                grant_types: vec![GrantType::AuthorizationCode, GrantType::RefreshToken],
198                client_name: Some("Client 1".to_owned()),
199                client_uri: Some(Url::parse("https://client1.example.com").unwrap()),
200                logo_uri: Some(Url::parse("https://client1.example.com/logo.png").unwrap()),
201                tos_uri: Some(Url::parse("https://client1.example.com/tos").unwrap()),
202                policy_uri: Some(Url::parse("https://client1.example.com/policy").unwrap()),
203                initiate_login_uri: Some(
204                    Url::parse("https://client1.example.com/initiate-login").unwrap(),
205                ),
206                token_endpoint_auth_method: Some(OAuthClientAuthenticationMethod::None),
207                token_endpoint_auth_signing_alg: None,
208                id_token_signed_response_alg: None,
209                userinfo_signed_response_alg: None,
210                jwks: None,
211                is_static: false,
212            },
213            // Another client without any URIs set
214            Self {
215                id: Ulid::from_datetime_with_rng(now, rng),
216                client_id: "client2".to_owned(),
217                metadata_digest: None,
218                encrypted_client_secret: None,
219                application_type: Some(ApplicationType::Native),
220                redirect_uris: vec![Url::parse("https://client2.example.com/redirect").unwrap()],
221                grant_types: vec![GrantType::AuthorizationCode, GrantType::RefreshToken],
222                client_name: None,
223                client_uri: None,
224                logo_uri: None,
225                tos_uri: None,
226                policy_uri: None,
227                initiate_login_uri: None,
228                token_endpoint_auth_method: None,
229                token_endpoint_auth_signing_alg: None,
230                id_token_signed_response_alg: None,
231                userinfo_signed_response_alg: None,
232                jwks: None,
233                is_static: false,
234            },
235        ]
236    }
237}
238
239/// The hosts that match the loopback interface.
240const LOCAL_HOSTS: &[&str] = &["localhost", "127.0.0.1", "[::1]"];
241
242/// Whether the given URI matches one of the registered URIs.
243///
244/// If the URI host is one if `localhost`, `127.0.0.1` or `[::1]`, any port is
245/// accepted.
246fn uri_matches_one_of(uri: &Url, registered_uris: &[Url]) -> bool {
247    if LOCAL_HOSTS.contains(&uri.host_str().unwrap_or_default()) {
248        let mut uri = uri.clone();
249        // Try matching without the port first
250        if uri.set_port(None).is_ok() && registered_uris.contains(&uri) {
251            return true;
252        }
253    }
254
255    registered_uris.contains(uri)
256}
257
258#[cfg(test)]
259mod tests {
260    use url::Url;
261
262    use super::*;
263
264    #[test]
265    fn test_uri_matches_one_of() {
266        let registered_uris = &[
267            Url::parse("http://127.0.0.1").unwrap(),
268            Url::parse("https://example.org").unwrap(),
269        ];
270
271        // Non-loopback interface URIs.
272        assert!(uri_matches_one_of(
273            &Url::parse("https://example.org").unwrap(),
274            registered_uris
275        ));
276        assert!(!uri_matches_one_of(
277            &Url::parse("https://example.org:8080").unwrap(),
278            registered_uris
279        ));
280
281        // Loopback interface URIS.
282        assert!(uri_matches_one_of(
283            &Url::parse("http://127.0.0.1").unwrap(),
284            registered_uris
285        ));
286        assert!(uri_matches_one_of(
287            &Url::parse("http://127.0.0.1:8080").unwrap(),
288            registered_uris
289        ));
290        assert!(!uri_matches_one_of(
291            &Url::parse("http://localhost").unwrap(),
292            registered_uris
293        ));
294    }
295}