1use super::{OAuthClientAssertion, OAuthClientAssertionContext, OAuthClientAssertionGetter};
3use base64::{
4 Engine,
5 alphabet::URL_SAFE,
6 engine::{
7 DecodePaddingMode, GeneralPurpose, GeneralPurposeConfig, general_purpose::URL_SAFE_NO_PAD,
8 },
9};
10use rsa::{
11 pkcs8::DecodePrivateKey,
12 signature::{RandomizedSigner, SignatureEncoding, Signer},
13 traits::PublicKeyParts,
14};
15use serde_json::{Value, json};
16use std::sync::Arc;
17
18const ALGORITHMS: &[&str] = &[
19 "RS256", "RS384", "RS512", "PS256", "PS384", "PS512", "ES256", "ES384", "ES512", "EdDSA",
20];
21
22#[derive(Clone, Default)]
26pub struct OAuthPrivateKeyJwtOptions {
27 pub private_key_jwk: Option<Value>,
28 pub private_key_pem: Option<String>,
29 pub algorithm: Option<String>,
30 pub kid: Option<String>,
31 pub expires_in: Option<f64>,
33}
34impl std::fmt::Debug for OAuthPrivateKeyJwtOptions {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 f.debug_struct("OAuthPrivateKeyJwtOptions")
37 .finish_non_exhaustive()
38 }
39}
40impl OAuthPrivateKeyJwtOptions {
41 pub fn into_assertion(self) -> Result<OAuthClientAssertion, String> {
46 _ = self.resolved_algorithm()?;
47 Ok(OAuthClientAssertion(Arc::new(self)))
48 }
49 fn resolved_algorithm(&self) -> Result<&str, String> {
50 if self.private_key_jwk.as_ref().is_none_or(Value::is_null)
51 && self.private_key_pem.as_deref().is_none_or(str::is_empty)
52 {
53 return Err("private_key_jwt requires either privateKeyJwk or privateKeyPem".into());
54 }
55 let embedded = self
56 .private_key_jwk
57 .as_ref()
58 .and_then(|k| k.get("alg"))
59 .and_then(Value::as_str);
60 for algorithm in [
61 self.algorithm.as_deref().filter(|s| !s.is_empty()),
62 embedded,
63 ]
64 .into_iter()
65 .flatten()
66 {
67 if !ALGORITHMS.contains(&algorithm) {
68 return Err("Unsupported private_key_jwt signing algorithm".into());
69 }
70 }
71 if let (Some(explicit), Some(embedded)) = (
72 self.algorithm.as_deref().filter(|s| !s.is_empty()),
73 embedded,
74 ) && explicit != embedded
75 {
76 return Err("JWK alg does not match configured algorithm".into());
77 }
78 Ok(self.algorithm.as_deref().or(embedded).unwrap_or("RS256"))
79 }
80 fn sign(&self, algorithm: &str, message: &[u8]) -> Result<Vec<u8>, String> {
81 let jwk = self.private_key_jwk.as_ref().filter(|v| !v.is_null());
82 let pem = self.private_key_pem.as_deref().unwrap_or_default();
83 if let Some(jwk) = jwk {
84 if jwk.get("ext").is_some_and(|value| !value.is_boolean()) {
85 return Err("JWK ext must be a boolean".into());
86 }
87 if let Some(ops) = jwk.get("key_ops")
88 && !ops.as_array().is_some_and(|ops| {
89 ops.iter().any(|op| op.as_str() == Some("sign"))
90 && ops.iter().enumerate().all(|(index, op)| {
91 op.is_string() && ops.iter().take(index).all(|previous| previous != op)
92 })
93 })
94 {
95 return Err("JWK does not permit signing".into());
96 }
97 let (kty, curve) = match algorithm {
98 "EdDSA" => ("OKP", Some("Ed25519")),
99 "ES256" => ("EC", Some("P-256")),
100 "ES384" => ("EC", Some("P-384")),
101 "ES512" => ("EC", Some("P-521")),
102 _ => ("RSA", None),
103 };
104 if jwk.get("kty").and_then(Value::as_str) != Some(kty)
105 || curve.is_some_and(|c| jwk.get("crv").and_then(Value::as_str) != Some(c))
106 {
107 return Err("JWK does not match signing algorithm".into());
108 }
109 }
110 if algorithm.starts_with("RS") || algorithm.starts_with("PS") {
111 let key = if let Some(jwk) = jwk {
112 for name in ["dp", "dq", "qi"] {
114 _ = field(jwk, name)?;
115 }
116 rsa::RsaPrivateKey::from_components(
117 rsa::BigUint::from_bytes_be(&field(jwk, "n")?),
118 rsa::BigUint::from_bytes_be(&field(jwk, "e")?),
119 rsa::BigUint::from_bytes_be(&field(jwk, "d")?),
120 vec![
121 rsa::BigUint::from_bytes_be(&field(jwk, "p")?),
122 rsa::BigUint::from_bytes_be(&field(jwk, "q")?),
123 ],
124 )
125 .map_err(key_error)?
126 } else {
127 rsa::RsaPrivateKey::from_pkcs8_pem(pem).map_err(key_error)?
128 };
129 if key.n().bits() < 2048 {
130 return Err("RSA modulus length must be at least 2048 bits".into());
131 }
132 macro_rules! rsa_sign {
133 ($digest:ty) => {
134 if algorithm.starts_with("RS") {
135 rsa::pkcs1v15::SigningKey::<$digest>::new(key)
136 .sign(message)
137 .to_vec()
138 } else {
139 rsa::pss::SigningKey::<$digest>::new(key)
140 .sign_with_rng(&mut rsa::rand_core::OsRng, message)
141 .to_vec()
142 }
143 };
144 }
145 return Ok(match algorithm {
146 "RS256" | "PS256" => rsa_sign!(rsa::sha2::Sha256),
147 "RS384" | "PS384" => rsa_sign!(rsa::sha2::Sha384),
148 _ => rsa_sign!(rsa::sha2::Sha512),
149 });
150 }
151 macro_rules! ec_sign {
152 ($curve:ident) => {{
153 use $curve::ecdsa::signature::RandomizedSigner as _;
154 use $curve::elliptic_curve::pkcs8::DecodePrivateKey as _;
155 use $curve::elliptic_curve::sec1::ToSec1Point as _;
156
157 let key = if let Some(jwk) = jwk {
158 $curve::SecretKey::from_slice(&field(jwk, "d")?).map_err(key_error)?
159 } else {
160 $curve::SecretKey::from_pkcs8_pem(pem).map_err(key_error)?
161 };
162 if let Some(jwk) = jwk {
163 let public = key.public_key().to_sec1_point(false);
164 if public.x().map(|x| x.as_slice()) != Some(field(jwk, "x")?.as_slice())
165 || public.y().map(|y| y.as_slice()) != Some(field(jwk, "y")?.as_slice())
166 {
167 return Err("EC private/public key mismatch".into());
168 }
169 }
170 let signer = $curve::ecdsa::SigningKey::from_slice(key.to_bytes().as_slice())
171 .map_err(key_error)?;
172 let signature: $curve::ecdsa::Signature = signer
173 .try_sign_with_rng(&mut rand::rng(), message)
174 .map_err(key_error)?;
175 signature.to_bytes().to_vec()
176 }};
177 }
178 Ok(match algorithm {
179 "ES256" => ec_sign!(p256),
180 "ES384" => ec_sign!(p384),
181 "ES512" => ec_sign!(p521),
182 "EdDSA" => {
183 use ed25519_dalek::Signer;
184 let key = if let Some(jwk) = jwk {
185 let seed: [u8; 32] = field(jwk, "d")?
186 .try_into()
187 .map_err(|_| "Invalid Ed25519 seed")?;
188 ed25519_dalek::SigningKey::from_bytes(&seed)
189 } else {
190 use ed25519_dalek::pkcs8::DecodePrivateKey as _;
191 ed25519_dalek::SigningKey::from_pkcs8_pem(pem).map_err(key_error)?
192 };
193 key.sign(message).to_bytes().to_vec()
194 }
195 _ => return Err("Unsupported private_key_jwt signing algorithm".into()),
196 })
197 }
198}
199#[async_trait::async_trait]
200impl OAuthClientAssertionGetter for OAuthPrivateKeyJwtOptions {
201 async fn get_client_assertion(
202 &self,
203 context: OAuthClientAssertionContext,
204 ) -> Result<String, String> {
205 let algorithm = self.resolved_algorithm()?;
206 let mut header = json!({"alg":algorithm,"typ":"JWT"});
207 let kid = self.kid.as_deref().map(str::to_owned).or_else(|| {
208 self.private_key_jwk
209 .as_ref()
210 .and_then(|k| k.get("kid"))
211 .and_then(Value::as_str)
212 .map(str::to_owned)
213 });
214 if let Some(kid) = kid.filter(|k| !k.is_empty())
215 && let Some(object) = header.as_object_mut()
216 {
217 _ = object.insert("kid".into(), Value::String(kid));
218 }
219 let now = chrono::Utc::now().timestamp();
220 #[expect(clippy::cast_precision_loss, reason = "JavaScript number arithmetic")]
221 let exp = now as f64 + self.expires_in.unwrap_or(120.0);
222 if !exp.is_finite() {
223 return Err("Invalid client assertion expiration time".into());
224 }
225 let claims = json!({"iss":context.client_id,"sub":context.client_id,"aud":context.token_endpoint,"iat":now,"exp":exp,"jti":uuid::Uuid::new_v4().to_string()});
226 let message = format!(
227 "{}.{}",
228 URL_SAFE_NO_PAD.encode(serde_json::to_vec(&header).map_err(key_error)?),
229 URL_SAFE_NO_PAD.encode(serde_json::to_vec(&claims).map_err(key_error)?)
230 );
231 Ok(format!(
232 "{message}.{}",
233 URL_SAFE_NO_PAD.encode(self.sign(algorithm, message.as_bytes())?)
234 ))
235 }
236}
237fn field(key: &Value, name: &str) -> Result<Vec<u8>, String> {
238 GeneralPurpose::new(
239 &URL_SAFE,
240 GeneralPurposeConfig::new()
241 .with_decode_padding_mode(DecodePaddingMode::Indifferent)
242 .with_decode_allow_trailing_bits(true),
243 )
244 .decode(
245 key.get(name)
246 .and_then(Value::as_str)
247 .ok_or("Missing JWK field")?,
248 )
249 .map_err(key_error)
250}
251fn key_error(_: impl std::fmt::Display) -> String {
252 "Client assertion key operation failed".into()
253}