mas_data_model/oauth2/
client.rs

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