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