assertion.rs 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. //! `private_key_jwt` client authentication (RFC 7521/7523, RFC 9700 §2.5).
  2. //!
  3. //! The assertion is verified against the client's inline `jwks`, or against
  4. //! `jwks_uri` fetched ONLY through the SSRF guard. Enforced: `alg`
  5. //! allowlist (`RS256`/`PS256`/`ES256`/`EdDSA`; never `none`/HMAC), `typ`,
  6. //! `iss` and `sub` equal to `client_id`, `aud` equal to the token endpoint,
  7. //! `exp` with clock-skew leeway and a bounded lifetime, `exp >= iat` (with
  8. //! skew) when `iat` is present, and single-use `jti` via the
  9. //! `oauth:client_assertion_jti:<hash>` replay store, with the marker TTL
  10. //! derived from the assertion's remaining validity so the marker always
  11. //! outlives the assertion.
  12. use std::time::Duration;
  13. use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode, decode_header};
  14. use serde::Deserialize;
  15. use time::OffsetDateTime;
  16. use super::{AuthContext, AuthStores, AuthenticatedClient};
  17. use crate::{
  18. db::{Client, TokenEndpointAuthMethod},
  19. error::AppError,
  20. protocol::ssrf,
  21. store::{self, dcr},
  22. };
  23. /// Allowed client-assertion signing algorithms (matches the metadata
  24. /// `token_endpoint_auth_signing_alg_values_supported` allowlist).
  25. pub(crate) const ALLOWED_ALGS: [Algorithm; 4] = [
  26. Algorithm::RS256,
  27. Algorithm::PS256,
  28. Algorithm::ES256,
  29. Algorithm::EdDSA,
  30. ];
  31. /// Clock-skew leeway applied to `exp`/`nbf`/`iat` (seconds).
  32. const CLOCK_SKEW_SECS: u64 = 30;
  33. /// Maximum assertion lifetime past `now` (seconds), before skew. The
  34. /// single-use `jti` marker TTL is derived from the remaining validity, so an
  35. /// assertion claiming to live longer than this cannot be covered by it.
  36. const MAX_ASSERTION_LIFETIME_SECS: u64 = 300;
  37. /// Maximum assertion compact-serialization length accepted.
  38. const MAX_ASSERTION_BYTES: usize = 16 * 1024;
  39. /// Maximum `jti` length accepted (unbounded ids become unbounded keys).
  40. const MAX_JTI_LEN: usize = 256;
  41. /// `aud` is either a single string or an array of strings (RFC 7523 §3).
  42. #[derive(Debug, Deserialize)]
  43. #[serde(untagged)]
  44. enum Aud {
  45. /// A single audience value.
  46. One(String),
  47. /// Several audience values; one must be the token endpoint.
  48. Many(Vec<String>),
  49. }
  50. impl Aud {
  51. /// Whether `token_endpoint` is among the audiences (exact match only).
  52. fn contains(&self, token_endpoint: &str) -> bool {
  53. match self {
  54. Self::One(one) => one == token_endpoint,
  55. Self::Many(many) => many.iter().any(|a| a == token_endpoint),
  56. }
  57. }
  58. }
  59. /// Registered claims of a `private_key_jwt` assertion (RFC 7523 §3).
  60. #[derive(Debug, Deserialize)]
  61. struct AssertionClaims {
  62. /// Issuer: must equal `client_id`.
  63. iss: String,
  64. /// Subject: must equal `client_id`.
  65. sub: String,
  66. /// Audience: must contain the token endpoint.
  67. aud: Aud,
  68. /// Unique id: single-use via the replay store.
  69. jti: String,
  70. /// Expiry (Unix seconds, enforced with leeway).
  71. exp: i64,
  72. /// Issued-at (Unix seconds, optional; when present must be sane).
  73. iat: Option<i64>,
  74. }
  75. /// A verified assertion: the `jti` hash reserved for the replay claim.
  76. #[derive(Debug)]
  77. pub struct ValidatedAssertion {
  78. /// Hex hash of the `jti`, the replay-store key suffix.
  79. pub jti_hash: String,
  80. /// Lifetime for the `jti` replay marker: the assertion's remaining validity
  81. /// (`exp + skew - now`), clamped to `[1s, 300s + 2*skew]`.
  82. pub jti_ttl: Duration,
  83. }
  84. /// Read `iss` from an assertion WITHOUT verifying the signature.
  85. ///
  86. /// Used only to locate the candidate client row; every security decision
  87. /// happens in [`validate_assertion`] after signature verification.
  88. #[must_use]
  89. pub fn unverified_iss(assertion: &str) -> Option<String> {
  90. use base64::Engine as _;
  91. let payload = assertion.split('.').nth(1)?;
  92. let bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
  93. .decode(payload)
  94. .ok()?;
  95. let value: serde_json::Value = serde_json::from_slice(&bytes).ok()?;
  96. value
  97. .get("iss")
  98. .and_then(|iss| iss.as_str())
  99. .map(str::to_owned)
  100. }
  101. /// Resolve the client's JWKS document: inline `jwks` preferred, else fetch
  102. /// `jwks_uri` through the SSRF guard. A client with neither is rejected.
  103. async fn resolve_jwks(client: &Client) -> Result<serde_json::Value, AppError> {
  104. if let Some(inline) = client.jwks.clone() {
  105. return Ok(inline);
  106. }
  107. if let Some(uri) = client.jwks_uri.as_deref() {
  108. return ssrf::fetch_guarded_json(uri)
  109. .await
  110. .map_err(|_| AppError::invalid_client_non_basic());
  111. }
  112. Err(AppError::invalid_client_non_basic())
  113. }
  114. /// Verify the assertion signature and claims (no I/O).
  115. ///
  116. /// Every failure maps to `invalid_client` (400: assertions travel in the
  117. /// body, never via Basic).
  118. pub fn validate_assertion(
  119. assertion: &str,
  120. jwks_doc: &serde_json::Value,
  121. client_id: &str,
  122. token_endpoint: &str,
  123. now: OffsetDateTime,
  124. ) -> Result<ValidatedAssertion, AppError> {
  125. let invalid = AppError::invalid_client_non_basic;
  126. if assertion.len() > MAX_ASSERTION_BYTES {
  127. return Err(invalid());
  128. }
  129. let header = decode_header(assertion).map_err(|_| invalid())?;
  130. if !ALLOWED_ALGS.contains(&header.alg) {
  131. return Err(invalid());
  132. }
  133. if header.typ.as_deref() != Some("JWT") {
  134. return Err(invalid());
  135. }
  136. // `jku`/`x5u` are fetch directives: never honored (SSRF bypass), so a
  137. // token carrying them is rejected outright.
  138. if header.jku.is_some() || header.x5u.is_some() {
  139. return Err(invalid());
  140. }
  141. let set: jsonwebtoken::jwk::JwkSet =
  142. serde_json::from_value(jwks_doc.clone()).map_err(|_| invalid())?;
  143. if set.keys.is_empty() {
  144. return Err(invalid());
  145. }
  146. let wanted = header.kid.clone().unwrap_or_default();
  147. let mut candidates: Vec<DecodingKey> = Vec::new();
  148. for key in &set.keys {
  149. if (wanted.is_empty() || key.common.key_id.as_deref() == Some(wanted.as_str()))
  150. && let Ok(decoding) = DecodingKey::try_from(key)
  151. {
  152. candidates.push(decoding);
  153. }
  154. }
  155. // Without a `kid` the set must hold exactly one usable key; otherwise the
  156. // assertion does not name its key.
  157. if candidates.len() != 1 {
  158. return Err(invalid());
  159. }
  160. let Some(decoding) = candidates.pop() else {
  161. return Err(invalid());
  162. };
  163. let mut validation = Validation::new(header.alg);
  164. validation.leeway = CLOCK_SKEW_SECS;
  165. validation.validate_exp = true;
  166. validation.validate_nbf = true;
  167. validation.set_required_spec_claims(&["exp"]);
  168. validation.validate_aud = false;
  169. let claims = decode::<AssertionClaims>(assertion, &decoding, &validation)
  170. .map_err(|_| invalid())?
  171. .claims;
  172. if claims.iss != client_id || claims.sub != client_id {
  173. return Err(invalid());
  174. }
  175. if !claims.aud.contains(token_endpoint) {
  176. return Err(invalid());
  177. }
  178. if claims.jti.is_empty() || claims.jti.len() > MAX_JTI_LEN {
  179. return Err(invalid());
  180. }
  181. let skew = i64::try_from(CLOCK_SKEW_SECS).unwrap_or(i64::MAX);
  182. let now_secs = now.unix_timestamp();
  183. if claims.exp.saturating_add(skew) < now_secs {
  184. return Err(invalid());
  185. }
  186. let max_lifetime = i64::try_from(MAX_ASSERTION_LIFETIME_SECS).unwrap_or(i64::MAX);
  187. if claims.exp > now_secs.saturating_add(max_lifetime.saturating_add(skew)) {
  188. return Err(invalid());
  189. }
  190. if let Some(iat) = claims.iat {
  191. if iat > now_secs.saturating_add(skew) {
  192. return Err(invalid());
  193. }
  194. if claims.exp.saturating_add(skew) < iat {
  195. return Err(invalid());
  196. }
  197. }
  198. let max_ttl = i64::try_from(MAX_ASSERTION_LIFETIME_SECS.saturating_add(2 * CLOCK_SKEW_SECS))
  199. .unwrap_or(i64::MAX);
  200. let remaining_secs = claims
  201. .exp
  202. .saturating_add(skew)
  203. .saturating_sub(now_secs)
  204. .clamp(1, max_ttl);
  205. Ok(ValidatedAssertion {
  206. jti_hash: store::sha256_hex(claims.jti.as_bytes()),
  207. jti_ttl: Duration::from_secs(u64::try_from(remaining_secs).unwrap_or(1)),
  208. })
  209. }
  210. /// Authenticate a client by `private_key_jwt`: resolve JWKS, verify the
  211. /// assertion, then single-use claim its `jti` (replay → `invalid_client`).
  212. ///
  213. /// # Errors
  214. ///
  215. /// Returns `invalid_client` for any verification, fetch-guard, or replay
  216. /// failure; internal errors only surface when the replay store itself fails
  217. /// in a non-client-attributable way (mapped by the store layer).
  218. pub async fn authenticate(
  219. stores: &mut AuthStores<'_>,
  220. client: &Client,
  221. assertion_str: &str,
  222. ctx: &AuthContext<'_>,
  223. ) -> Result<AuthenticatedClient, AppError> {
  224. let jwks_doc = resolve_jwks(client).await?;
  225. let validated = validate_assertion(
  226. assertion_str,
  227. &jwks_doc,
  228. &client.client_id,
  229. ctx.token_endpoint,
  230. ctx.now,
  231. )?;
  232. let claimed = dcr::claim_client_assertion_jti(
  233. stores.conn,
  234. stores.store,
  235. &validated.jti_hash,
  236. validated.jti_ttl,
  237. )
  238. .await?;
  239. if !claimed {
  240. return Err(AppError::invalid_client_non_basic());
  241. }
  242. Ok(AuthenticatedClient {
  243. client: client.clone(),
  244. method: TokenEndpointAuthMethod::PrivateKeyJwt,
  245. })
  246. }