1use 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 Jwks(PublicJsonWebKeySet),
26
27 JwksUri(Url),
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
32pub struct Client {
33 pub id: Ulid,
34
35 pub client_id: String,
37
38 pub encrypted_client_secret: Option<String>,
39
40 pub application_type: Option<ApplicationType>,
41
42 pub redirect_uris: Vec<Url>,
44
45 pub grant_types: Vec<GrantType>,
48
49 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>,
67
68 pub id_token_signed_response_alg: Option<JsonWebSignatureAlg>,
71
72 pub userinfo_signed_response_alg: Option<JsonWebSignatureAlg>,
74
75 pub token_endpoint_auth_method: Option<OAuthClientAuthenticationMethod>,
77
78 pub token_endpoint_auth_signing_alg: Option<JsonWebSignatureAlg>,
82
83 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 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 #[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 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 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
225const LOCAL_HOSTS: &[&str] = &["localhost", "127.0.0.1", "[::1]"];
227
228fn 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 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 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 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}