1use std::sync::Mutex;
21
22use didbot_pds::oauth::ClientAuthKey;
23use jsonwebtoken::jwk::{
24 AlgorithmParameters, EllipticCurve, Jwk, KeyAlgorithm, PublicKeyUse, ThumbprintHash,
25};
26use jsonwebtoken::{Algorithm, DecodingKey, Validation};
27use serde::{Deserialize, Serialize};
28use sha2::{Digest, Sha256};
29use time::{Duration, OffsetDateTime};
30
31use crate::oauth::client_metadata::ClientMetadataDocument;
32use crate::oauth::error::OAuthErrorCode;
33use crate::replay::{Spent, SpentKeys, MAX_JTI_BYTES, REPLAY_CAPACITY};
34
35pub const ASSERTION_TYPE: &str = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
37
38pub const NONE: &str = "none";
40
41pub const PRIVATE_KEY_JWT: &str = "private_key_jwt";
43
44pub const SIGNING_ALG: &str = "ES256";
48
49const ALGORITHM: Algorithm = Algorithm::ES256;
51
52pub const ASSERTION_MAX_AGE: Duration = Duration::minutes(1);
58
59pub const ASSERTION_CLOCK_SKEW: Duration = Duration::minutes(1);
62
63fn window() -> Duration {
66 ASSERTION_MAX_AGE + ASSERTION_CLOCK_SKEW
67}
68
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
72pub struct ClientAuthentication {
73 pub token_endpoint_auth_method: String,
76 #[serde(default, skip_serializing_if = "Option::is_none")]
78 pub token_endpoint_auth_signing_alg: Option<String>,
79 #[serde(default, skip_serializing_if = "Option::is_none")]
81 pub jwks: Option<Jwks>,
82 #[serde(default, skip_serializing_if = "Option::is_none")]
84 pub jwks_uri: Option<String>,
85 #[serde(skip)]
90 pub(crate) jwks_uri_keys: Option<Jwks>,
91}
92
93impl ClientAuthentication {
94 pub fn public() -> Self {
96 Self {
97 token_endpoint_auth_method: NONE.to_owned(),
98 token_endpoint_auth_signing_alg: None,
99 jwks: None,
100 jwks_uri: None,
101 jwks_uri_keys: None,
102 }
103 }
104
105 pub fn is_confidential(&self) -> bool {
107 self.token_endpoint_auth_method == PRIVATE_KEY_JWT
108 }
109
110 pub fn keys(&self) -> Option<&Jwks> {
112 self.jwks.as_ref().or(self.jwks_uri_keys.as_ref())
113 }
114}
115
116#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
124pub struct Jwks {
125 keys: Vec<Jwk>,
126}
127
128impl<'de> Deserialize<'de> for Jwks {
129 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
130 #[derive(Deserialize)]
131 struct Published {
132 keys: Vec<serde_json::Value>,
133 }
134 let published = Published::deserialize(deserializer)?;
135 Ok(Self {
136 keys: published.keys.into_iter().filter_map(usable).collect(),
137 })
138 }
139}
140
141fn usable(published: serde_json::Value) -> Option<Jwk> {
143 if published.get("d").is_some() {
144 return None;
145 }
146 let key: Jwk = serde_json::from_value(published).ok()?;
147 let named = key
148 .common
149 .key_id
150 .as_deref()
151 .is_some_and(|kid| !kid.is_empty());
152 let p256 = matches!(
153 &key.algorithm,
154 AlgorithmParameters::EllipticCurve(ec) if ec.curve == EllipticCurve::P256
155 );
156 let signs = key
157 .common
158 .public_key_use
159 .as_ref()
160 .is_none_or(|used| *used == PublicKeyUse::Signature);
161 let es256 = key
162 .common
163 .key_algorithm
164 .is_none_or(|alg| alg == KeyAlgorithm::ES256);
165 (named && p256 && signs && es256).then_some(key)
166}
167
168impl Jwks {
169 pub fn is_empty(&self) -> bool {
171 self.keys.is_empty()
172 }
173
174 fn find(&self, kid: &str) -> Option<&Jwk> {
176 self.keys
177 .iter()
178 .find(|key| key.common.key_id.as_deref() == Some(kid))
179 }
180}
181
182#[derive(Debug, Clone, Default, PartialEq, Eq)]
185pub struct ClientCredentials {
186 pub assertion_type: Option<String>,
188 pub assertion: Option<String>,
190}
191
192impl ClientCredentials {
193 pub fn is_empty(&self) -> bool {
195 self.assertion_type.is_none() && self.assertion.is_none()
196 }
197
198 fn assertion(&self) -> Result<Option<&str>, ClientAuthError> {
201 match (self.assertion_type.as_deref(), self.assertion.as_deref()) {
202 (None, None) => Ok(None),
203 (Some(ASSERTION_TYPE), Some(assertion)) => Ok(Some(assertion)),
204 (Some(_), Some(_)) => Err(ClientAuthError::UnsupportedAssertionType),
205 _ => Err(ClientAuthError::Incomplete),
206 }
207 }
208}
209
210#[derive(Debug, Clone, PartialEq, Eq)]
213pub struct AuthenticatedClient {
214 pub client_id: String,
216 pub key: ClientAuthKey,
218}
219
220#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
222pub enum ClientAuthError {
223 #[error("client_assertion and client_assertion_type go together")]
225 Incomplete,
226 #[error("client_assertion_type must be {ASSERTION_TYPE}")]
229 UnsupportedAssertionType,
230 #[error("this client authenticates with private_key_jwt, and the request carries no client_assertion")]
232 AssertionRequired,
233 #[error("this client's metadata names no client authentication, and the request carries a client_assertion")]
235 AssertionRefused,
236 #[error("no key this client publishes has the kid {0:?}")]
238 UnknownKey(String),
239 #[error("the client_assertion has expired")]
242 Expired,
243 #[error("the client_assertion has already been used")]
245 Replayed,
246 #[error("this server is holding as many client assertions as it will; retry shortly")]
250 WindowFull,
251 #[error("the client_assertion was issued before this server started; sign a fresh one")]
255 PredatesStart,
256 #[error("the client_assertion is not valid: {0}")]
258 Invalid(String),
259}
260
261impl ClientAuthError {
262 pub fn code(&self) -> OAuthErrorCode {
266 match self {
267 ClientAuthError::Incomplete => OAuthErrorCode::InvalidRequest,
268 _ => OAuthErrorCode::InvalidClient,
269 }
270 }
271}
272
273fn invalid(what: impl Into<String>) -> ClientAuthError {
275 ClientAuthError::Invalid(what.into())
276}
277
278fn refusal(error: jsonwebtoken::errors::Error) -> ClientAuthError {
280 use jsonwebtoken::errors::ErrorKind;
281 match error.kind() {
282 ErrorKind::ExpiredSignature => ClientAuthError::Expired,
283 ErrorKind::InvalidSignature => {
284 invalid("its signature does not verify against the key its kid names")
285 }
286 ErrorKind::InvalidAlgorithm => invalid(format!("it must be signed with {SIGNING_ALG}")),
287 ErrorKind::InvalidAudience => invalid("its aud is not this server's issuer"),
288 ErrorKind::InvalidIssuer => invalid("its iss is not the client_id"),
289 ErrorKind::InvalidSubject => invalid("its sub is not the client_id"),
290 ErrorKind::ImmatureSignature => invalid("its nbf has not arrived"),
291 ErrorKind::MissingRequiredClaim(claim) => invalid(format!("it carries no {claim}")),
292 _ => invalid(error.to_string()),
293 }
294}
295
296#[derive(Deserialize)]
299struct AssertionClaims {
300 iat: Option<i64>,
301 jti: Option<String>,
302}
303
304fn replay_key(client_id: &str, jti: &str) -> String {
308 hex::encode(Sha256::digest(format!("{client_id}\n{jti}")))
309}
310
311pub struct ClientAssertionVerifier {
314 started: OffsetDateTime,
318 spent: Mutex<SpentKeys>,
320}
321
322impl Default for ClientAssertionVerifier {
323 fn default() -> Self {
324 Self::new()
325 }
326}
327
328impl ClientAssertionVerifier {
329 pub fn new() -> Self {
331 Self::started_at(OffsetDateTime::now_utc())
332 }
333
334 fn started_at(started: OffsetDateTime) -> Self {
337 let started = OffsetDateTime::from_unix_timestamp(started.unix_timestamp())
338 .unwrap_or(OffsetDateTime::UNIX_EPOCH);
339 Self {
340 started,
341 spent: Mutex::new(SpentKeys::new(REPLAY_CAPACITY, started)),
342 }
343 }
344
345 pub fn authenticate(
352 &self,
353 document: &ClientMetadataDocument,
354 credentials: &ClientCredentials,
355 issuer: &str,
356 ) -> Result<Option<ClientAuthKey>, ClientAuthError> {
357 self.authenticate_at(document, credentials, issuer, OffsetDateTime::now_utc())
358 }
359
360 fn authenticate_at(
361 &self,
362 document: &ClientMetadataDocument,
363 credentials: &ClientCredentials,
364 issuer: &str,
365 now: OffsetDateTime,
366 ) -> Result<Option<ClientAuthKey>, ClientAuthError> {
367 let assertion = credentials.assertion()?;
368 if !document.authentication.is_confidential() {
369 return match assertion {
370 Some(_) => Err(ClientAuthError::AssertionRefused),
371 None => Ok(None),
372 };
373 }
374 let assertion = assertion.ok_or(ClientAuthError::AssertionRequired)?;
375 self.verify(document, assertion, issuer, now).map(Some)
376 }
377
378 fn verify(
382 &self,
383 document: &ClientMetadataDocument,
384 assertion: &str,
385 issuer: &str,
386 now: OffsetDateTime,
387 ) -> Result<ClientAuthKey, ClientAuthError> {
388 let client_id = document.client_id.as_str();
389 if issuer.is_empty() {
390 return Err(invalid("this server has no issuer yet for its aud to name"));
391 }
392 let header = jsonwebtoken::decode_header(assertion)
393 .map_err(|_| invalid(format!("its header does not decode as {SIGNING_ALG}")))?;
394 let kid = header
395 .kid
396 .ok_or_else(|| invalid("its header names no kid"))?;
397 let jwk = document
398 .authentication
399 .keys()
400 .and_then(|keys| keys.find(&kid))
401 .ok_or_else(|| ClientAuthError::UnknownKey(kid.clone()))?;
402 let key = DecodingKey::from_jwk(jwk)
403 .map_err(|_| invalid("the key its kid names does not decode"))?;
404
405 let mut validation = Validation::new(ALGORITHM);
406 validation.leeway = ASSERTION_CLOCK_SKEW.whole_seconds().unsigned_abs();
407 validation.validate_nbf = true;
408 validation.set_required_spec_claims(&["iss", "sub", "aud"]);
409 validation.set_issuer(&[client_id]);
410 validation.sub = Some(client_id.to_owned());
411 validation.set_audience(&[issuer]);
412 let claims = jsonwebtoken::decode::<AssertionClaims>(assertion, &key, &validation)
413 .map_err(refusal)?
414 .claims;
415
416 let iat = claims
417 .iat
418 .and_then(|iat| OffsetDateTime::from_unix_timestamp(iat).ok())
419 .ok_or_else(|| invalid("it carries no iat"))?;
420 if iat > now + ASSERTION_CLOCK_SKEW {
421 return Err(invalid("its iat is in the future"));
422 }
423 if now - iat > window() {
424 return Err(ClientAuthError::Expired);
425 }
426 if now < self.started + window() && iat < self.started {
430 return Err(ClientAuthError::PredatesStart);
431 }
432 let jti = claims
433 .jti
434 .filter(|jti| !jti.is_empty())
435 .ok_or_else(|| invalid("it carries no jti"))?;
436 if jti.len() > MAX_JTI_BYTES {
437 return Err(invalid(format!(
438 "its jti is longer than the {MAX_JTI_BYTES} bytes this server keeps"
439 )));
440 }
441 let jkt = jwk
442 .thumbprint(ThumbprintHash::SHA256)
443 .map_err(|_| invalid("the key its kid names has no thumbprint"))?;
444
445 let mut spent = self.spent.lock().unwrap_or_else(|p| p.into_inner());
446 spent.sweep_if_due(now, window());
447 spent
448 .spend(replay_key(client_id, &jti), iat + window(), now)
449 .map_err(|refused| match refused {
450 Spent::Replayed => ClientAuthError::Replayed,
451 Spent::WindowFull => ClientAuthError::WindowFull,
452 })?;
453 Ok(ClientAuthKey {
454 kid,
455 alg: SIGNING_ALG.to_owned(),
456 jkt,
457 })
458 }
459}
460
461#[cfg(test)]
462mod tests {
463 use jacquard_oauth::jose::jwt::{RegisteredClaims, RegisteredClaimsAud};
464 use jacquard_oauth::keyset::Keyset;
465 use serde_json::json;
466
467 use super::*;
468
469 const CLIENT_ID: &str = "https://app.client.example/oauth-client-metadata.json";
470 const ISSUER: &str = "https://pds.example";
471
472 fn document(keyset: &Keyset) -> ClientMetadataDocument {
474 serde_json::from_value(json!({
475 "client_id": CLIENT_ID,
476 "redirect_uris": ["https://app.client.example/callback"],
477 "grant_types": ["authorization_code", "refresh_token"],
478 "response_types": ["code"],
479 "token_endpoint_auth_method": PRIVATE_KEY_JWT,
480 "token_endpoint_auth_signing_alg": SIGNING_ALG,
481 "jwks": keyset.public_jwks(),
482 "dpop_bound_access_tokens": true,
483 }))
484 .expect("a confidential client's document reads")
485 }
486
487 fn assertion(keyset: &Keyset, iat: OffsetDateTime, jti: &str) -> ClientCredentials {
489 let es256 = jacquard_oauth::keyset::parse_signing_alg(SIGNING_ALG).expect("ES256");
490 let iat = iat.unix_timestamp();
491 let signed = keyset
492 .create_jwt(
493 &[es256],
494 RegisteredClaims {
495 iss: Some(CLIENT_ID),
496 sub: Some(CLIENT_ID),
497 aud: Some(RegisteredClaimsAud::Single(ISSUER)),
498 exp: Some(iat + 60),
499 iat: Some(iat),
500 jti: Some(jti.into()),
501 ..Default::default()
502 }
503 .into(),
504 )
505 .expect("jacquard signs the assertion");
506 ClientCredentials {
507 assertion_type: Some(ASSERTION_TYPE.to_owned()),
508 assertion: Some(signed.to_string()),
509 }
510 }
511
512 #[test]
517 fn an_assertion_stamped_before_a_restart_is_refused_for_one_window() {
518 let keyset = Keyset::generate_es256("client-key-1").expect("a key");
519 let document = document(&keyset);
520 let started = OffsetDateTime::now_utc() - Duration::seconds(30);
521 let verifier = ClientAssertionVerifier::started_at(started);
522
523 let captured = assertion(&keyset, started - Duration::seconds(10), "captured");
524 assert_eq!(
525 verifier.authenticate(&document, &captured, ISSUER),
526 Err(ClientAuthError::PredatesStart)
527 );
528
529 let fresh = assertion(&keyset, OffsetDateTime::now_utc(), "fresh");
530 let bound = verifier
531 .authenticate(&document, &fresh, ISSUER)
532 .expect("an assertion stamped after the start is accepted")
533 .expect("a confidential client authenticates with a key");
534 assert_eq!(bound.kid, "client-key-1");
535 assert_eq!(bound.alg, SIGNING_ALG);
536 }
537
538 #[test]
542 fn a_key_set_keeps_only_public_p256_signing_keys_with_a_kid() {
543 let public =
544 serde_json::to_value(Keyset::generate_es256("kept").expect("a key").public_jwks())
545 .expect("a key set serializes");
546 let kept = public["keys"][0].clone();
547 let with = |field: &str, value: serde_json::Value| {
548 let mut key = kept.clone();
549 key[field] = value;
550 key
551 };
552 let private = serde_json::to_value(Keyset::generate_es256("private").expect("a key"))
553 .expect("a private key set serializes")[0]
554 .clone();
555 let mut unnamed = kept.clone();
556 unnamed
557 .as_object_mut()
558 .expect("a key is an object")
559 .remove("kid");
560
561 let set: Jwks = serde_json::from_value(json!({
564 "keys": [
565 private,
566 unnamed,
567 with("use", json!("enc")),
568 with("alg", json!("ES384")),
569 with("crv", json!("P-384")),
570 { "kty": "RSA", "kid": "rsa", "n": "sXch", "e": "AQAB" },
571 kept.clone(),
572 ]
573 }))
574 .expect("a key set with keys this server cannot use still reads");
575 assert_eq!(set.keys.len(), 1, "{set:?}");
576 let survivor = set.find("kept").expect("the unbent key is kept");
577 assert_eq!(survivor.common.public_key_use, None);
578 assert_eq!(survivor.common.key_algorithm, None);
579 assert!(set.find("private").is_none());
580 }
581}