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
22#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
23#[serde(rename_all = "snake_case")]
24pub enum JwksOrJwksUri {
25 Jwks(PublicJsonWebKeySet),
27
28 JwksUri(Url),
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
33pub struct Client {
34 pub id: Ulid,
35
36 pub client_id: String,
38
39 pub metadata_digest: Option<String>,
41
42 pub encrypted_client_secret: Option<String>,
43
44 pub application_type: Option<ApplicationType>,
45
46 pub redirect_uris: Vec<Url>,
48
49 pub grant_types: Vec<GrantType>,
52
53 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>,
71
72 pub id_token_signed_response_alg: Option<JsonWebSignatureAlg>,
75
76 pub userinfo_signed_response_alg: Option<JsonWebSignatureAlg>,
78
79 pub token_endpoint_auth_method: Option<OAuthClientAuthenticationMethod>,
81
82 pub token_endpoint_auth_signing_alg: Option<JsonWebSignatureAlg>,
86
87 pub initiate_login_uri: Option<Url>,
90
91 pub is_static: bool,
94}
95
96#[derive(Debug, Error)]
97pub enum InvalidRedirectUriError {
98 #[error("redirect_uri is not allowed for this client")]
99 NotAllowed,
100
101 #[error("multiple redirect_uris registered for this client")]
102 MultipleRegistered,
103
104 #[error("client has no redirect_uri registered")]
105 NoneRegistered,
106}
107
108impl Client {
109 pub fn resolve_redirect_uri<'a>(
119 &'a self,
120 redirect_uri: &'a Option<Url>,
121 ) -> Result<&'a Url, InvalidRedirectUriError> {
122 match (&self.redirect_uris[..], redirect_uri) {
123 ([], _) => Err(InvalidRedirectUriError::NoneRegistered),
124 ([one], None) => Ok(one),
125 (_, None) => Err(InvalidRedirectUriError::MultipleRegistered),
126 (uris, Some(uri)) if uri_matches_one_of(uri, uris) => Ok(uri),
127 _ => Err(InvalidRedirectUriError::NotAllowed),
128 }
129 }
130
131 #[must_use]
133 pub fn into_metadata(self) -> ClientMetadata {
134 let (jwks, jwks_uri) = match self.jwks {
135 Some(JwksOrJwksUri::Jwks(jwks)) => (Some(jwks), None),
136 Some(JwksOrJwksUri::JwksUri(jwks_uri)) => (None, Some(jwks_uri)),
137 _ => (None, None),
138 };
139 ClientMetadata {
140 redirect_uris: Some(self.redirect_uris.clone()),
141 response_types: None,
142 grant_types: Some(self.grant_types.clone()),
143 application_type: self.application_type.clone(),
144 client_name: self.client_name.map(|n| Localized::new(n, [])),
145 logo_uri: self.logo_uri.map(|n| Localized::new(n, [])),
146 client_uri: self.client_uri.map(|n| Localized::new(n, [])),
147 policy_uri: self.policy_uri.map(|n| Localized::new(n, [])),
148 tos_uri: self.tos_uri.map(|n| Localized::new(n, [])),
149 jwks_uri,
150 jwks,
151 id_token_signed_response_alg: self.id_token_signed_response_alg,
152 userinfo_signed_response_alg: self.userinfo_signed_response_alg,
153 token_endpoint_auth_method: self.token_endpoint_auth_method,
154 token_endpoint_auth_signing_alg: self.token_endpoint_auth_signing_alg,
155 initiate_login_uri: self.initiate_login_uri,
156 contacts: None,
157 software_id: None,
158 software_version: None,
159 sector_identifier_uri: None,
160 subject_type: None,
161 id_token_encrypted_response_alg: None,
162 id_token_encrypted_response_enc: None,
163 userinfo_encrypted_response_alg: None,
164 userinfo_encrypted_response_enc: None,
165 request_object_signing_alg: None,
166 request_object_encryption_alg: None,
167 request_object_encryption_enc: None,
168 default_max_age: None,
169 require_auth_time: None,
170 default_acr_values: None,
171 request_uris: None,
172 require_signed_request_object: None,
173 require_pushed_authorization_requests: None,
174 introspection_signed_response_alg: None,
175 introspection_encrypted_response_alg: None,
176 introspection_encrypted_response_enc: None,
177 post_logout_redirect_uris: None,
178 }
179 }
180
181 #[doc(hidden)]
182 pub fn samples(now: DateTime<Utc>, rng: &mut impl RngCore) -> Vec<Client> {
183 vec![
184 Self {
186 id: Ulid::from_datetime_with_source(now.into(), rng),
187 client_id: "client1".to_owned(),
188 metadata_digest: None,
189 encrypted_client_secret: None,
190 application_type: Some(ApplicationType::Web),
191 redirect_uris: vec![
192 Url::parse("https://client1.example.com/redirect").unwrap(),
193 Url::parse("https://client1.example.com/redirect2").unwrap(),
194 ],
195 grant_types: vec![GrantType::AuthorizationCode, GrantType::RefreshToken],
196 client_name: Some("Client 1".to_owned()),
197 client_uri: Some(Url::parse("https://client1.example.com").unwrap()),
198 logo_uri: Some(Url::parse("https://client1.example.com/logo.png").unwrap()),
199 tos_uri: Some(Url::parse("https://client1.example.com/tos").unwrap()),
200 policy_uri: Some(Url::parse("https://client1.example.com/policy").unwrap()),
201 initiate_login_uri: Some(
202 Url::parse("https://client1.example.com/initiate-login").unwrap(),
203 ),
204 token_endpoint_auth_method: Some(OAuthClientAuthenticationMethod::None),
205 token_endpoint_auth_signing_alg: None,
206 id_token_signed_response_alg: None,
207 userinfo_signed_response_alg: None,
208 jwks: None,
209 is_static: false,
210 },
211 Self {
213 id: Ulid::from_datetime_with_source(now.into(), rng),
214 client_id: "client2".to_owned(),
215 metadata_digest: None,
216 encrypted_client_secret: None,
217 application_type: Some(ApplicationType::Native),
218 redirect_uris: vec![Url::parse("https://client2.example.com/redirect").unwrap()],
219 grant_types: vec![GrantType::AuthorizationCode, GrantType::RefreshToken],
220 client_name: None,
221 client_uri: None,
222 logo_uri: None,
223 tos_uri: None,
224 policy_uri: None,
225 initiate_login_uri: None,
226 token_endpoint_auth_method: None,
227 token_endpoint_auth_signing_alg: None,
228 id_token_signed_response_alg: None,
229 userinfo_signed_response_alg: None,
230 jwks: None,
231 is_static: false,
232 },
233 ]
234 }
235}
236
237const LOCAL_HOSTS: &[&str] = &["localhost", "127.0.0.1", "[::1]"];
239
240fn uri_matches_one_of(uri: &Url, registered_uris: &[Url]) -> bool {
245 if LOCAL_HOSTS.contains(&uri.host_str().unwrap_or_default()) {
246 let mut uri = uri.clone();
247 if uri.set_port(None).is_ok() && registered_uris.contains(&uri) {
249 return true;
250 }
251 }
252
253 registered_uris.contains(uri)
254}
255
256#[cfg(test)]
257mod tests {
258 use url::Url;
259
260 use super::*;
261
262 #[test]
263 fn test_uri_matches_one_of() {
264 let registered_uris = &[
265 Url::parse("http://127.0.0.1").unwrap(),
266 Url::parse("https://example.org").unwrap(),
267 ];
268
269 assert!(uri_matches_one_of(
271 &Url::parse("https://example.org").unwrap(),
272 registered_uris
273 ));
274 assert!(!uri_matches_one_of(
275 &Url::parse("https://example.org:8080").unwrap(),
276 registered_uris
277 ));
278
279 assert!(uri_matches_one_of(
281 &Url::parse("http://127.0.0.1").unwrap(),
282 registered_uris
283 ));
284 assert!(uri_matches_one_of(
285 &Url::parse("http://127.0.0.1:8080").unwrap(),
286 registered_uris
287 ));
288 assert!(!uri_matches_one_of(
289 &Url::parse("http://localhost").unwrap(),
290 registered_uris
291 ));
292 }
293}