1use crate::plugins::endpoint_context::EndpointContext;
4use better_auth_core::{
5 AuthContext, AuthError, AuthInitContext, AuthPlugin, AuthRequest, AuthResponse, AuthResult,
6 AuthRoute, AuthSchema, CreateJwk, HttpMethod,
7};
8use chrono::{Duration, Utc};
9use josekit::{
10 jwk::{self, Jwk},
11 jws::{self, JwsHeader, JwsSigner},
12};
13use serde_json::{Map, Value, json};
14
15mod adapter;
16mod cache;
17mod callbacks;
18mod claims;
19mod native;
20mod options;
21mod overrides;
22mod signing;
23mod verification;
24pub use callbacks::{JwtAdapterFuture, JwtCallbacks};
25pub use native::JwtApi;
26pub use options::*;
27pub use overrides::{JwtCallOverrides, JwtKeyOptions, JwtTokenOptions};
28
29#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Deserialize)]
31pub enum JwtAlgorithm {
32 #[default]
34 #[serde(rename = "EdDSA")]
35 EdDsa,
36 #[serde(rename = "RS256")]
38 Rs256,
39 #[serde(rename = "ES256")]
41 Es256,
42 #[serde(rename = "ES512")]
44 Es512,
45 #[serde(rename = "PS256")]
47 Ps256,
48}
49
50impl JwtAlgorithm {
51 fn name(self) -> &'static str {
52 match self {
53 Self::EdDsa => "EdDSA",
54 Self::Rs256 => "RS256",
55 Self::Es256 => "ES256",
56 Self::Es512 => "ES512",
57 Self::Ps256 => "PS256",
58 }
59 }
60 fn curve(self) -> Option<&'static str> {
61 match self {
62 Self::EdDsa => Some("Ed25519"),
63 Self::Es256 => Some("P-256"),
64 Self::Es512 => Some("P-521"),
65 Self::Rs256 | Self::Ps256 => None,
66 }
67 }
68 fn generate(self, modulus_length: u32) -> Result<Jwk, josekit::JoseError> {
69 match self {
70 Self::EdDsa => Jwk::generate_ed_key(jwk::Ed25519),
71 Self::Rs256 | Self::Ps256 => Jwk::generate_rsa_key(modulus_length),
72 Self::Es256 => Jwk::generate_ec_key(jwk::P_256),
73 Self::Es512 => Jwk::generate_ec_key(jwk::P_521),
74 }
75 }
76}
77
78#[derive(Clone, better_auth_core::PluginConfig)]
80#[plugin(name = "JwtPlugin")]
81pub struct JwtPluginConfig {
82 #[config(default = false)]
84 pub session_cookie_cache: bool,
85 #[config(default = 2048)]
87 pub modulus_length: u32,
88 #[config(default = Vec::new())]
90 pub key_pair_configs: Vec<JwtKeyPairConfig>,
91 #[config(default = None)]
93 pub remote_url: Option<String>,
94 #[config(default = None)]
96 pub custom_sign: Option<JwtCustomSign>,
97 #[config(default = None)]
99 pub define_payload: Option<JwtDefinePayload>,
100 #[config(default = None)]
102 pub get_subject: Option<JwtGetSubject>,
103 #[config(default = "/jwks".to_owned())]
105 pub jwks_path: String,
106 #[config(default = JwtAlgorithm::EdDsa)]
108 pub algorithm: JwtAlgorithm,
109 #[config(default = JwtExpiration::After(Duration::minutes(15)), skip)]
111 pub expiration_time: JwtExpiration,
112 #[config(default = None)]
114 pub issuer: Option<String>,
115 #[config(default = None)]
117 pub audience: Option<JwtAudience>,
118 #[config(default = None)]
120 pub rotation_interval: Option<Duration>,
121 #[config(default = Duration::days(30))]
123 pub grace_period: Duration,
124 #[config(default = false)]
126 pub disable_private_key_encryption: bool,
127 #[config(default = false)]
129 pub disable_setting_jwt_header: bool,
130}
131
132pub struct JwtPlugin {
134 config: JwtPluginConfig,
135}
136
137impl JwtPlugin {
138 pub fn expiration_time(mut self, expiration: impl Into<JwtExpiration>) -> Self {
140 self.config.expiration_time = expiration.into();
141 self
142 }
143}
144
145#[async_trait::async_trait]
146impl<S: AuthSchema> AuthPlugin<S> for JwtPlugin {
147 fn name(&self) -> &'static str {
148 "jwt"
149 }
150 fn routes(&self) -> Vec<AuthRoute> {
151 vec![
152 AuthRoute::get(&self.config.jwks_path, "getJSONWebKeySet"),
153 AuthRoute::get("/token", "getJSONWebToken").require_headers(true),
154 ]
155 }
156 async fn on_init(&self, ctx: &mut AuthInitContext<S>) -> AuthResult<()> {
157 ctx.extensions.insert(self.config.clone());
158 if self.config.custom_sign.is_some() && self.config.remote_url.is_none() {
159 return Err(AuthError::config("custom_sign requires remote_url"));
160 }
161 if !self.config.jwks_path.starts_with('/') || self.config.jwks_path.contains("..") {
162 return Err(AuthError::config(
163 "jwks_path must start with '/' and must not contain '..'",
164 ));
165 }
166 if self.config.session_cookie_cache {
167 if !ctx
168 .config
169 .session
170 .cookie_cache
171 .as_ref()
172 .is_some_and(|cache| {
173 matches!(
174 cache.strategy(),
175 better_auth_core::config::CookieCacheStrategy::Jwt
176 )
177 })
178 {
179 return Err(AuthError::config(
180 "session_cookie_cache requires the JWT cookie cache strategy",
181 ));
182 }
183 if self.config.custom_sign.is_some() {
184 return Err(AuthError::config(
185 "session_cookie_cache requires locally managed JWT keys",
186 ));
187 }
188 let signer: std::sync::Arc<dyn better_auth_core::session::SessionCookieSigner<S>> =
189 std::sync::Arc::new(cache::CookieSigner {
190 plugin: Self::with_config(self.config.clone()),
191 runtime: ctx.runtime(),
192 });
193 ctx.extensions.insert(signer);
194 }
195 Ok(())
196 }
197 async fn on_request(
198 &self,
199 req: &AuthRequest,
200 ctx: &AuthContext<S>,
201 ) -> AuthResult<Option<AuthResponse>> {
202 if req.method() != &HttpMethod::Get
203 || (req.path() != self.config.jwks_path && req.path() != "/token")
204 {
205 return Ok(None);
206 }
207 let mut endpoint = EndpointContext::new(Some(req), request_body(req)?, ctx);
208 if req.path() == self.config.jwks_path {
209 return self.jwks(&endpoint).await.map(Some);
210 }
211 if req.path() == "/token" {
212 let (user, session) = ctx
213 .require_session(req)
214 .await
215 .map_err(|error| match error {
216 AuthError::Unauthenticated => AuthError::Upstream {
217 status: 401,
218 code: "UNAUTHORIZED",
219 message: "Unauthorized",
220 },
221 error => error,
222 })?;
223 endpoint.session = Some((user.clone(), session.clone()));
224 let token = self
225 .sign_session(json!({"user": user, "session": session}), &endpoint)
226 .await?;
227 return Ok(Some(AuthResponse::json(200, &json!({"token": token}))?));
228 }
229 Ok(None)
230 }
231 async fn after_request(
232 &self,
233 req: &AuthRequest,
234 response: &mut AuthResponse,
235 ctx: &AuthContext<S>,
236 ) -> AuthResult<()> {
237 if !(req.path() == "/get-session") {
238 return Ok(());
239 }
240 better_auth_core::observability::instrumentation::with_endpoint_hook(
241 &ctx.config,
242 req,
243 "after",
244 "plugin:jwt",
245 async {
246 if self.config.disable_setting_jwt_header || req.path() != "/get-session" {
247 return Ok(());
248 }
249 let snapshot = req.session_snapshot()?;
250 let Some(data) = snapshot
251 .clone()
252 .map(better_auth_core::session::NativeSessionData::from)
253 .or(req.new_session()?)
254 else {
255 return Ok(());
256 };
257 let payload = serde_json::to_value(&data)?;
258 let mut endpoint = EndpointContext::new(Some(req), request_body(req)?, ctx);
259 endpoint.session = snapshot.map(|data| (data.user, data.session));
260 endpoint.response = Some(response);
261 let token = self.sign_session(payload, &endpoint).await?;
262 let _ = response.headers.insert("set-auth-jwt", token);
263 let mut exposed: Vec<_> = response
264 .headers
265 .get("access-control-expose-headers")
266 .map(|value| {
267 value
268 .split(',')
269 .map(str::trim)
270 .filter(|value| !value.is_empty())
271 .map(str::to_owned)
272 .collect()
273 })
274 .unwrap_or_default();
275 if !exposed.iter().any(|value| value == "set-auth-jwt") {
276 exposed.push("set-auth-jwt".into());
277 }
278 let _ = response
279 .headers
280 .insert("Access-Control-Expose-Headers", exposed.join(", "));
281 Ok(())
282 },
283 )
284 .await
285 }
286}
287
288impl JwtPlugin {
289 async fn create_key<S: AuthSchema>(
290 &self,
291 endpoint: &EndpointContext<'_, S>,
292 ) -> AuthResult<better_auth_core::Jwk> {
293 self.create_key_pair_in_endpoint(
294 JwtKeyPairConfig {
295 algorithm: self.config.algorithm,
296 modulus_length: self.config.modulus_length,
297 },
298 endpoint,
299 )
300 .await
301 }
302
303 pub async fn create_key_pair<S: AuthSchema>(
305 &self,
306 parameters: JwtKeyPairConfig,
307 ctx: &AuthContext<S>,
308 ) -> AuthResult<better_auth_core::Jwk> {
309 ctx.with_native_context(Default::default(), |resolved| async move {
310 let mut endpoint =
311 EndpointContext::new(None, better_auth_core::FieldValue::Null, &resolved);
312 endpoint.path = Some("virtual:");
313 self.create_key_pair_in_endpoint(parameters, &endpoint)
314 .await
315 })
316 .await
317 }
318
319 pub async fn create_key_pair_in_endpoint<S: AuthSchema>(
321 &self,
322 parameters: JwtKeyPairConfig,
323 endpoint: &EndpointContext<'_, S>,
324 ) -> AuthResult<better_auth_core::Jwk> {
325 if matches!(
326 parameters.algorithm,
327 JwtAlgorithm::Rs256 | JwtAlgorithm::Ps256
328 ) && parameters.modulus_length < 2048
329 {
330 return Err(AuthError::config(
331 "RSA modulus_length must be at least 2048",
332 ));
333 }
334 let mut key = parameters
335 .algorithm
336 .generate(parameters.modulus_length)
337 .map_err(jose_error)?;
338 key.set_parameter("use", None).map_err(jose_error)?;
339 let public_key = key.to_public_key().map_err(jose_error)?.to_string();
340 let private_key = if self.config.disable_private_key_encryption {
341 key.to_string()
342 } else {
343 serde_json::to_string(&super::symmetric::encrypt(
344 endpoint.auth.config.encryption_secret(),
345 &key.to_string(),
346 )?)?
347 };
348 self.persist_key(
349 CreateJwk {
350 additional_fields: Default::default(),
351 public_key,
352 private_key,
353 created_at: Utc::now().into(),
354 expires_at: self
355 .config
356 .rotation_interval
357 .filter(|interval| !interval.is_zero())
358 .map(|interval| (Utc::now() + interval).into()),
359 alg: parameters.algorithm.name().to_owned(),
360 crv: parameters.algorithm.curve().map(str::to_owned),
361 },
362 endpoint,
363 )
364 .await
365 }
366
367 async fn jwks<S: AuthSchema>(
368 &self,
369 endpoint: &EndpointContext<'_, S>,
370 ) -> AuthResult<AuthResponse> {
371 if self.config.remote_url.is_some() {
372 let mut response = AuthResponse::new(404);
373 let _ = response.headers.insert("content-type", "application/json");
374 return Ok(response);
375 }
376 let mut keys = self.read_keys(endpoint).await?;
377 if keys.as_ref().is_none_or(Vec::is_empty) {
378 let _ = self.create_key(endpoint).await?;
379 keys = self.read_keys(endpoint).await?;
380 }
381 let keys = keys.filter(|keys| !keys.is_empty()).ok_or_else(|| {
382 AuthError::internal("No key sets found. Make sure you have a key in your database.")
383 })?;
384 let mut public = Vec::new();
385 for key in keys.into_iter().filter(|key| {
386 key.expires_at.as_ref().is_none_or(|expiry| {
387 expiry.milliseconds() + self.config.grace_period.num_milliseconds() as f64
388 > Utc::now().timestamp_millis() as f64
389 })
390 }) {
391 let mut value: Map<String, Value> = serde_json::from_str(&key.public_key)?;
392 let _ = value.entry("alg").or_insert_with(|| {
393 key.alg
394 .unwrap_or_else(|| self.config.algorithm.name().to_owned())
395 .into()
396 });
397 if let Some(curve) = key.crv {
398 let _ = value.entry("crv").or_insert(curve.into());
399 }
400 if let Some(id) = key.id.json()? {
401 let _ = value.insert("kid".into(), id);
402 }
403 public.push(value);
404 }
405 Ok(AuthResponse::json(200, &json!({"keys": public}))?)
406 }
407}
408
409fn request_body(request: &AuthRequest) -> AuthResult<better_auth_core::FieldValue> {
410 if let Some(context) = better_auth_core::hooks::current_request_hook_context() {
411 return Ok(context.body);
412 }
413 request.input_field_value()
414}
415
416fn jose_error(error: josekit::JoseError) -> AuthError {
417 AuthError::internal(format!("JWT signing key: {error}"))
418}
419
420#[cfg(test)]
421mod tests;