Skip to main content

better_auth_api/plugins/jwt/
signing.rs

1use super::*;
2
3pub(super) struct ResolvedSigningKey {
4    algorithm: String,
5    key_id: Option<String>,
6    signer: Box<dyn JwsSigner>,
7}
8
9impl ResolvedSigningKey {
10    pub(super) fn sign(
11        &self,
12        payload: Map<String, Value>,
13        options: &JwtSigningOptions,
14    ) -> AuthResult<String> {
15        let mut header = JwsHeader::from_map(options.header.clone()).map_err(jose_error)?;
16        header.set_algorithm(&self.algorithm);
17        if let Some(id) = &self.key_id {
18            header.set_key_id(id);
19        }
20        josekit::jws::serialize_compact(
21            &serde_json::to_vec(&payload)?,
22            &header,
23            self.signer.as_ref(),
24        )
25        .map_err(jose_error)
26    }
27}
28
29impl JwtPlugin {
30    pub(super) async fn sign_session<S: AuthSchema>(
31        &self,
32        session: Value,
33        endpoint: &EndpointContext<'_, S>,
34    ) -> AuthResult<String> {
35        let user = session
36            .get("user")
37            .and_then(Value::as_object)
38            .ok_or_else(|| AuthError::internal("JWT user must be an object"))?;
39        let default_subject = user
40            .get("id")
41            .cloned()
42            .ok_or_else(|| AuthError::internal("JWT user must have an ID"))?;
43        let mut payload = match &self.config.define_payload {
44            Some(callback) => callback(session.clone()).await?,
45            None => user.clone(),
46        };
47        let subject = match &self.config.get_subject {
48            Some(callback) => callback(session).await?.into(),
49            None => default_subject,
50        };
51        let _ = payload
52            .entry("iat")
53            .or_insert_with(|| Utc::now().timestamp().into());
54        let _ = payload.insert("sub".into(), subject);
55        self.sign_in_endpoint(payload, &JwtSigningOptions::default(), endpoint)
56            .await
57    }
58
59    /// Sign an application payload with the same persisted keys as `/token`.
60    pub async fn sign<S: AuthSchema>(
61        &self,
62        payload: Map<String, Value>,
63        ctx: &AuthContext<S>,
64    ) -> AuthResult<String> {
65        self.sign_with_options(payload, &JwtSigningOptions::default(), ctx)
66            .await
67    }
68
69    /// Sign a native payload with explicit protected headers and key selection.
70    /// Use `sign_in_endpoint` to retain an endpoint request and transaction.
71    pub async fn sign_with_options<S: AuthSchema>(
72        &self,
73        payload: Map<String, Value>,
74        options: &JwtSigningOptions,
75        ctx: &AuthContext<S>,
76    ) -> AuthResult<String> {
77        ctx.with_native_context(Default::default(), |resolved| async move {
78            let mut endpoint = EndpointContext::new(
79                None,
80                better_auth_core::FieldValue::from_json(json!({"payload":payload}))?,
81                &resolved,
82            );
83            endpoint.path = Some("virtual:");
84            self.sign_in_endpoint(payload, options, &endpoint).await
85        })
86        .await
87    }
88
89    /// Sign with the supplied endpoint context, including its active transaction.
90    pub async fn sign_in_endpoint<S: AuthSchema>(
91        &self,
92        mut payload: Map<String, Value>,
93        options: &JwtSigningOptions,
94        endpoint: &EndpointContext<'_, S>,
95    ) -> AuthResult<String> {
96        let config = &endpoint.auth.config;
97        self.default_claims(&mut payload, config)?;
98        if let Some(callback) = &self.config.custom_sign {
99            return callback(payload, options.clone()).await;
100        }
101        let key = self.resolve_local_signing_key(options, endpoint).await?;
102        claims::prepare_local_claims(&mut payload)?;
103        key.sign(payload, options)
104    }
105
106    pub(super) async fn resolve_local_signing_key<S: AuthSchema>(
107        &self,
108        options: &JwtSigningOptions,
109        endpoint: &EndpointContext<'_, S>,
110    ) -> AuthResult<ResolvedSigningKey> {
111        let config = &endpoint.auth.config;
112        let selected =
113            if let Some(id) = &options.key_id {
114                let key = self
115                    .read_key(id, endpoint)
116                    .await?
117                    .ok_or_else(|| AuthError::config("Requested JWT signing key does not exist"))?;
118                if key.expires_at.as_ref().is_some_and(|expiry| {
119                    expiry.milliseconds() < Utc::now().timestamp_millis() as f64
120                }) || options.algorithm.is_some_and(|alg| {
121                    key.alg.as_deref().unwrap_or(self.config.algorithm.name()) != alg.name()
122                }) {
123                    return Err(AuthError::config(
124                        "Requested JWT signing key is expired or has a different algorithm",
125                    ));
126                }
127                Some(key)
128            } else {
129                let mut keys = self.read_keys(endpoint).await?.unwrap_or_default();
130                keys.sort_by(|left, right| {
131                    right
132                        .created_at
133                        .milliseconds()
134                        .partial_cmp(&left.created_at.milliseconds())
135                        .unwrap_or(std::cmp::Ordering::Equal)
136                });
137                keys.retain(|key| {
138                    key.expires_at.as_ref().is_none_or(|expiry| {
139                        expiry.milliseconds() > Utc::now().timestamp_millis() as f64
140                    })
141                });
142                let preferred = keys
143                    .iter()
144                    .find(|key| {
145                        key.alg.as_deref().unwrap_or(self.config.algorithm.name())
146                            == options.algorithm.unwrap_or(self.config.algorithm).name()
147                    })
148                    .cloned();
149                if preferred.is_some() || options.algorithm.is_some() {
150                    preferred
151                } else {
152                    let mut fallback = self.read_keys(endpoint).await?.unwrap_or_default();
153                    fallback.retain(|key| {
154                        key.expires_at.as_ref().is_none_or(|expiry| {
155                            expiry.milliseconds() > Utc::now().timestamp_millis() as f64
156                        })
157                    });
158                    fallback.sort_by(|left, right| {
159                        right
160                            .created_at
161                            .milliseconds()
162                            .partial_cmp(&left.created_at.milliseconds())
163                            .unwrap_or(std::cmp::Ordering::Equal)
164                    });
165                    fallback.into_iter().next()
166                }
167            };
168        let key = match selected {
169            Some(key) => key,
170            None => {
171                let algorithm = options.algorithm.unwrap_or(self.config.algorithm);
172                let parameters = self
173                    .config
174                    .key_pair_configs
175                    .iter()
176                    .find(|config| config.algorithm == algorithm)
177                    .copied()
178                    .or_else(|| {
179                        (algorithm == self.config.algorithm).then_some(JwtKeyPairConfig {
180                            algorithm,
181                            modulus_length: self.config.modulus_length,
182                        })
183                    })
184                    .ok_or_else(|| {
185                        AuthError::config("Requested JWT algorithm is not configured")
186                    })?;
187                self.create_key_pair_in_endpoint(parameters, endpoint)
188                    .await?
189            }
190        };
191        if (options.key_id.is_some() || options.algorithm.is_some())
192            && key
193                .expires_at
194                .as_ref()
195                .is_some_and(|expiry| expiry.milliseconds() < Utc::now().timestamp_millis() as f64)
196        {
197            return Err(AuthError::config("Requested JWT signing key is expired"));
198        }
199        let private = if self.config.disable_private_key_encryption {
200            key.private_key
201        } else {
202            crate::plugins::symmetric::decrypt(
203                config.encryption_secret(),
204                &serde_json::from_str::<String>(&key.private_key)?,
205            )?
206        };
207        let private = Jwk::from_bytes(private).map_err(jose_error)?;
208        let algorithm = key.alg.as_deref().unwrap_or(self.config.algorithm.name());
209        let signer: Box<dyn JwsSigner> = match algorithm {
210            "EdDSA" => Box::new(jws::EdDSA.signer_from_jwk(&private).map_err(jose_error)?),
211            "RS256" => Box::new(jws::RS256.signer_from_jwk(&private).map_err(jose_error)?),
212            "ES256" => Box::new(jws::ES256.signer_from_jwk(&private).map_err(jose_error)?),
213            "ES512" => Box::new(jws::ES512.signer_from_jwk(&private).map_err(jose_error)?),
214            "PS256" => Box::new(jws::PS256.signer_from_jwk(&private).map_err(jose_error)?),
215            _ => {
216                return Err(AuthError::config(format!(
217                    "Unsupported JWT signing algorithm: {algorithm}"
218                )));
219            }
220        };
221        Ok(ResolvedSigningKey {
222            algorithm: algorithm.to_owned(),
223            key_id: key.id.as_str().map(str::to_owned),
224            signer,
225        })
226    }
227
228    fn default_claims(
229        &self,
230        payload: &mut Map<String, Value>,
231        config: &better_auth_core::AuthConfig,
232    ) -> AuthResult<()> {
233        let expiration =
234            claims::default_expiration(&self.config.expiration_time, payload.get("iat"))?;
235        for (claim, default) in [
236            ("exp", expiration),
237            (
238                "iss",
239                self.config
240                    .issuer
241                    .as_deref()
242                    .unwrap_or_else(|| config.base_url.as_static().unwrap_or(""))
243                    .into(),
244            ),
245            (
246                "aud",
247                match &self.config.audience {
248                    Some(audience) => serde_json::to_value(audience)?,
249                    None => config.base_url.as_static().unwrap_or("").into(),
250                },
251            ),
252        ] {
253            if payload.get(claim).is_none_or(Value::is_null) {
254                let _ = payload.insert(claim.to_owned(), default);
255            }
256        }
257        Ok(())
258    }
259}