better_auth_api/plugins/jwt/
signing.rs1use 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 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 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 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}