Skip to main content

webgates_codecs/jwt/
authority.rs

1//! Sign ES384 JWTs and publish the matching JWKS document.
2//!
3//! [`JwtAuthority`] is the main entry point when you need to:
4//! - sign JWTs with an ES384 private key
5//! - publish the corresponding public key as a JWKS document
6//!
7//! It composes [`JsonWebToken`] and [`JwksProvider`] from a single pair of
8//! PEM-encoded key bytes, ensuring the `kid` is always consistent between the
9//! signing header and the published JWKS document.
10//!
11//! # Example
12//!
13//! ```rust
14//! use webgates_codecs::jwt::authority::JwtAuthority;
15//! use webgates_codecs::jwt::JwtClaims;
16//! use webgates_codecs::jsonwebtoken::crypto::rust_crypto::DEFAULT_PROVIDER as JWT_CRYPTO_PROVIDER;
17//!
18//! # const PRIVATE_PEM: &[u8] = br#"-----BEGIN PRIVATE KEY-----
19//! # MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDCFT7MfRqWZfNgVX/cH
20//! # bxFTlPkBeCKqjsLkZXD/J3ZYHV1EtQksdrKtOzTr2hMs6pmhZANiAASyND9eQ5Qk
21//! # 7ZteSEPMpExbVJenRWwyobExJMb62mmp3eA7Fszy8uBbLj8HRB16y3QbLcTxCBoo
22//! # ldBXfNFzM133OuTV2bBWXq5h34l+A0h4gU/odZ678LfAgnrRYMG4ZjU=
23//! # -----END PRIVATE KEY-----
24//! # "#;
25//! # const PUBLIC_PEM: &[u8] = br#"-----BEGIN PUBLIC KEY-----
26//! # MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEsjQ/XkOUJO2bXkhDzKRMW1SXp0VsMqGx
27//! # MSTG+tppqd3gOxbM8vLgWy4/B0Qdest0Gy3E8QgaKJXQV3zRczNd9zrk1dmwVl6u
28//! # Yd+JfgNIeIFP6HWeu/C3wIJ60WDBuGY1
29//! # -----END PUBLIC KEY-----
30//! # "#;
31//! let _ = JWT_CRYPTO_PROVIDER.install_default();
32//! let authority = JwtAuthority::<JwtClaims<()>>::from_es384_pem(PRIVATE_PEM, PUBLIC_PEM)
33//!     .expect("valid ES384 key pair");
34//!
35//! // The kid is stable and shared between the codec and the JWKS provider.
36//! assert_eq!(authority.key_id(), authority.jwks_provider().key_id().unwrap());
37//! ```
38
39use std::sync::Arc;
40
41use crate::Result;
42use crate::jwt::jwks::JwksProvider;
43use crate::jwt::{JsonWebToken, JsonWebTokenOptions};
44
45use serde::{Serialize, de::DeserializeOwned};
46
47/// Authority bundle that owns an ES384 signing codec and a JWKS provider.
48///
49/// Construct this type once at startup with [`JwtAuthority::from_es384_pem`]
50/// and share it across handlers with `Arc<JwtAuthority<P>>`.
51///
52/// The `kid` embedded in every signed JWT header is guaranteed to match the `kid`
53/// in the published JWKS document.
54#[derive(Clone)]
55pub struct JwtAuthority<P>
56where
57    P: Serialize + DeserializeOwned + Clone,
58{
59    codec: Arc<JsonWebToken<P>>,
60    jwks_provider: JwksProvider,
61    key_id: String,
62}
63
64impl<P> JwtAuthority<P>
65where
66    P: Serialize + DeserializeOwned + Clone,
67{
68    /// Build an authority bundle from PEM-encoded ES384 private and public keys.
69    ///
70    /// The `kid` is derived deterministically from the public key bytes so it is
71    /// stable across restarts as long as the key pair does not change.
72    ///
73    /// # Errors
74    ///
75    /// Returns an error if either key cannot be parsed or the JWKS provider
76    /// cannot be constructed from the public key.
77    pub fn from_es384_pem(private_key_pem: &[u8], public_key_pem: &[u8]) -> Result<Self> {
78        let options = JsonWebTokenOptions::from_es384_pem(private_key_pem, public_key_pem)?;
79        let key_id = options.key_id().to_string();
80        let jwks_provider =
81            JwksProvider::from_es384_public_pem_with_kid(public_key_pem, key_id.clone())?;
82        let codec = Arc::new(JsonWebToken::new_with_options(options));
83        Ok(Self {
84            codec,
85            jwks_provider,
86            key_id,
87        })
88    }
89
90    /// Returns the signing codec.
91    ///
92    /// Use this to encode JWTs in login handlers.
93    pub fn codec(&self) -> Arc<JsonWebToken<P>> {
94        Arc::clone(&self.codec)
95    }
96
97    /// Returns the JWKS provider.
98    ///
99    /// Pass this to the JWKS publication route handler.
100    pub fn jwks_provider(&self) -> &JwksProvider {
101        &self.jwks_provider
102    }
103
104    /// Returns the active signing key id (`kid`).
105    ///
106    /// This value is identical to the `kid` in the published JWKS document.
107    pub fn key_id(&self) -> &str {
108        &self.key_id
109    }
110}
111
112impl<P> std::fmt::Debug for JwtAuthority<P>
113where
114    P: Serialize + DeserializeOwned + Clone,
115{
116    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117        f.debug_struct("JwtAuthority")
118            .field("key_id", &self.key_id)
119            .finish_non_exhaustive()
120    }
121}
122
123#[cfg(test)]
124#[allow(clippy::unwrap_used, clippy::expect_used)]
125mod tests {
126    use super::*;
127    use crate::Codec as _;
128    use crate::jwt::{JwtClaims, RegisteredClaims};
129
130    const PRIVATE_PEM: &[u8] = br#"-----BEGIN PRIVATE KEY-----
131MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDCFT7MfRqWZfNgVX/cH
132bxFTlPkBeCKqjsLkZXD/J3ZYHV1EtQksdrKtOzTr2hMs6pmhZANiAASyND9eQ5Qk
1337ZteSEPMpExbVJenRWwyobExJMb62mmp3eA7Fszy8uBbLj8HRB16y3QbLcTxCBoo
134ldBXfNFzM133OuTV2bBWXq5h34l+A0h4gU/odZ678LfAgnrRYMG4ZjU=
135-----END PRIVATE KEY-----
136"#;
137
138    const PUBLIC_PEM: &[u8] = br#"-----BEGIN PUBLIC KEY-----
139MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEsjQ/XkOUJO2bXkhDzKRMW1SXp0VsMqGx
140MSTG+tppqd3gOxbM8vLgWy4/B0Qdest0Gy3E8QgaKJXQV3zRczNd9zrk1dmwVl6u
141Yd+JfgNIeIFP6HWeu/C3wIJ60WDBuGY1
142-----END PUBLIC KEY-----
143"#;
144
145    #[test]
146    fn authority_builds_from_es384_pem() {
147        let authority = JwtAuthority::<JwtClaims<()>>::from_es384_pem(PRIVATE_PEM, PUBLIC_PEM)
148            .expect("authority should be created from valid ES384 PEM");
149
150        assert!(!authority.key_id().is_empty());
151        assert_eq!(
152            authority.key_id(),
153            authority.jwks_provider().key_id().expect("kid must be set")
154        );
155        assert_eq!(authority.jwks_provider().document().keys.len(), 1);
156    }
157
158    #[test]
159    fn authority_kid_is_stable_across_constructions() {
160        let a1 = JwtAuthority::<JwtClaims<()>>::from_es384_pem(PRIVATE_PEM, PUBLIC_PEM)
161            .expect("first construction");
162        let a2 = JwtAuthority::<JwtClaims<()>>::from_es384_pem(PRIVATE_PEM, PUBLIC_PEM)
163            .expect("second construction");
164
165        assert_eq!(a1.key_id(), a2.key_id());
166    }
167
168    #[test]
169    fn authority_codec_can_sign_and_verify() {
170        use jsonwebtoken::crypto::rust_crypto::DEFAULT_PROVIDER as JWT_CRYPTO_PROVIDER;
171        use webgates_core::accounts::Account;
172        use webgates_core::groups::Group;
173        use webgates_core::roles::Role;
174
175        let _ = JWT_CRYPTO_PROVIDER.install_default();
176
177        let authority = JwtAuthority::<JwtClaims<Account<Role, Group>>>::from_es384_pem(
178            PRIVATE_PEM,
179            PUBLIC_PEM,
180        )
181        .expect("authority should be created");
182
183        let account = Account::<Role, Group>::new("test-user");
184        let exp = chrono::Utc::now().timestamp() as u64 + 60;
185        let claims = JwtClaims::new(account, RegisteredClaims::new("test-issuer", exp));
186
187        let codec = authority.codec();
188        let encoded = codec.encode(&claims).expect("encoding should succeed");
189        let decoded = codec.decode(&encoded).expect("decoding should succeed");
190
191        assert_eq!(decoded.registered_claims.issuer, "test-issuer");
192    }
193}