Skip to main content

alibi_api/plugins/oauth/
client_assertion.rs

1//! Published RFC 7523 client assertions for application-owned token endpoints.
2use 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};
14use serde_json::{Value, json};
15use std::sync::Arc;
16
17const ALGORITHMS: &[&str] = &[
18    "RS256", "RS384", "RS512", "PS256", "PS384", "PS512", "ES256", "ES384", "ES512", "EdDSA",
19];
20
21/// Key material stays private; Debug deliberately never renders it.
22/// JWK takes precedence over PKCS#8 PEM. Algorithm defaults to JWK `alg`, then
23/// RS256; explicit algorithm and JWK `alg` must agree. `kid` overrides JWK `kid`.
24#[derive(Clone, Default)]
25pub struct OAuthPrivateKeyJwtOptions {
26    pub private_key_jwk: Option<Value>,
27    pub private_key_pem: Option<String>,
28    pub algorithm: Option<String>,
29    pub kid: Option<String>,
30    /// Seconds, default 120. Negative lifetimes produce expired assertions.
31    pub expires_in: Option<f64>,
32}
33impl std::fmt::Debug for OAuthPrivateKeyJwtOptions {
34    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35        f.debug_struct("OAuthPrivateKeyJwtOptions")
36            .finish_non_exhaustive()
37    }
38}
39impl OAuthPrivateKeyJwtOptions {
40    /// Validate configuration eagerly; key import/signing occurs for each grant,
41    /// matching the published getter's construction versus request boundaries.
42    /// # Errors
43    /// Rejects missing key material, unsupported algorithms, and algorithm conflicts.
44    pub fn into_assertion(self) -> Result<OAuthClientAssertion, String> {
45        let _algorithm = self.resolved_algorithm()?;
46        Ok(OAuthClientAssertion(Arc::new(self)))
47    }
48    fn resolved_algorithm(&self) -> Result<&str, String> {
49        if self.private_key_jwk.as_ref().is_none_or(Value::is_null)
50            && self.private_key_pem.as_deref().is_none_or(str::is_empty)
51        {
52            return Err("private_key_jwt requires either privateKeyJwk or privateKeyPem".into());
53        }
54        let embedded = self
55            .private_key_jwk
56            .as_ref()
57            .and_then(|k| k.get("alg"))
58            .and_then(Value::as_str);
59        for algorithm in [
60            self.algorithm.as_deref().filter(|s| !s.is_empty()),
61            embedded,
62        ]
63        .into_iter()
64        .flatten()
65        {
66            if !ALGORITHMS.contains(&algorithm) {
67                return Err("Unsupported private_key_jwt signing algorithm".into());
68            }
69        }
70        if let (Some(explicit), Some(embedded)) = (
71            self.algorithm.as_deref().filter(|s| !s.is_empty()),
72            embedded,
73        ) && explicit != embedded
74        {
75            return Err("JWK alg does not match configured algorithm".into());
76        }
77        Ok(self.algorithm.as_deref().or(embedded).unwrap_or("RS256"))
78    }
79    fn sign(&self, algorithm: &str, message: &[u8]) -> Result<Vec<u8>, String> {
80        let jwk = self.private_key_jwk.as_ref().filter(|v| !v.is_null());
81        let pem = self.private_key_pem.as_deref().unwrap_or_default();
82        if let Some(jwk) = jwk {
83            if jwk.get("ext").is_some_and(|value| !value.is_boolean()) {
84                return Err("JWK ext must be a boolean".into());
85            }
86            if let Some(ops) = jwk.get("key_ops")
87                && !ops.as_array().is_some_and(|ops| {
88                    ops.iter().any(|op| op.as_str() == Some("sign"))
89                        && ops.iter().enumerate().all(|(index, op)| {
90                            op.is_string() && ops.iter().take(index).all(|previous| previous != op)
91                        })
92                })
93            {
94                return Err("JWK does not permit signing".into());
95            }
96            let (kty, curve) = match algorithm {
97                "EdDSA" => ("OKP", Some("Ed25519")),
98                "ES256" => ("EC", Some("P-256")),
99                "ES384" => ("EC", Some("P-384")),
100                "ES512" => ("EC", Some("P-521")),
101                _ => ("RSA", None),
102            };
103            if jwk.get("kty").and_then(Value::as_str) != Some(kty)
104                || curve.is_some_and(|c| jwk.get("crv").and_then(Value::as_str) != Some(c))
105            {
106                return Err("JWK does not match signing algorithm".into());
107            }
108        }
109        if algorithm.starts_with("RS") || algorithm.starts_with("PS") {
110            let key = if let Some(jwk) = jwk {
111                // WebCrypto imports the complete two-prime private JWK.
112                for name in ["dp", "dq", "qi"] {
113                    let _component = field(jwk, name)?;
114                }
115                rsa::RsaPrivateKey::from_components(
116                    rsa::BigUint::from_bytes_be(&field(jwk, "n")?),
117                    rsa::BigUint::from_bytes_be(&field(jwk, "e")?),
118                    rsa::BigUint::from_bytes_be(&field(jwk, "d")?),
119                    vec![
120                        rsa::BigUint::from_bytes_be(&field(jwk, "p")?),
121                        rsa::BigUint::from_bytes_be(&field(jwk, "q")?),
122                    ],
123                )
124                .map_err(key_error)?
125            } else {
126                rsa::RsaPrivateKey::from_pkcs8_pem(pem).map_err(key_error)?
127            };
128            use rsa::traits::PublicKeyParts;
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            drop(object.insert("kid".into(), Value::String(kid)));
218        }
219        let now = chrono::Utc::now().timestamp();
220        let exp = now as f64 + self.expires_in.unwrap_or(120.0);
221        if !exp.is_finite() {
222            return Err("Invalid client assertion expiration time".into());
223        }
224        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()});
225        let message = format!(
226            "{}.{}",
227            URL_SAFE_NO_PAD.encode(serde_json::to_vec(&header).map_err(key_error)?),
228            URL_SAFE_NO_PAD.encode(serde_json::to_vec(&claims).map_err(key_error)?)
229        );
230        Ok(format!(
231            "{message}.{}",
232            URL_SAFE_NO_PAD.encode(self.sign(algorithm, message.as_bytes())?)
233        ))
234    }
235}
236fn field(key: &Value, name: &str) -> Result<Vec<u8>, String> {
237    GeneralPurpose::new(
238        &URL_SAFE,
239        GeneralPurposeConfig::new()
240            .with_decode_padding_mode(DecodePaddingMode::Indifferent)
241            .with_decode_allow_trailing_bits(true),
242    )
243    .decode(
244        key.get(name)
245            .and_then(Value::as_str)
246            .ok_or("Missing JWK field")?,
247    )
248    .map_err(key_error)
249}
250fn key_error(_: impl std::fmt::Display) -> String {
251    "Client assertion key operation failed".into()
252}