1use 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 Jwks(PublicJsonWebKeySet),
29
30 JwksUri(Url),
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
35pub struct Client {
36 pub id: Ulid,
37
38 pub client_id: String,
40
41 pub metadata_digest: Option<String>,
43
44 pub encrypted_client_secret: Option<String>,
45
46 pub application_type: Option<ApplicationType>,
47
48 pub redirect_uris: Vec<Url>,
50
51 pub grant_types: Vec<GrantType>,
54
55 pub client_name: Option<String>, pub logo_uri: Option<Url>, pub client_uri: Option<Url>, pub policy_uri: Option<Url>, pub tos_uri: Option<Url>, pub jwks: Option<JwksOrJwksUri>,
73
74 pub id_token_signed_response_alg: Option<JsonWebSignatureAlg>,
77
78 pub userinfo_signed_response_alg: Option<JsonWebSignatureAlg>,
80
81 pub token_endpoint_auth_method: Option<OAuthClientAuthenticationMethod>,
83
84 pub token_endpoint_auth_signing_alg: Option<JsonWebSignatureAlg>,
88
89 pub initiate_login_uri: Option<Url>,
92
93 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 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 #[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 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 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
239const LOCAL_HOSTS: &[&str] = &["localhost", "127.0.0.1", "[::1]"];
241
242fn 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 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 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 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}