Files
5095456bb6 chore(security): bump jsonwebtoken to 10 (#11194)
* refactor(jwt): route JWK algorithm lookup through one helper

Add `windmill_common::jwt::jwk_algorithm` as the single place that reads a
JWK's `alg`, and make `guest_jwt::jwk_algorithms` use it. EE code will call
the same helper, so the upcoming jsonwebtoken bump (which renames the field
to `key_algorithm: Option<KeyAlgorithm>`) only has to touch the helper's body.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* chore(security): bump jsonwebtoken to 10

Resolves dependency alerts 193 and 344 (jsonwebtoken) and, for the
windmill-parser-wasm lock, 341 (ring 0.16).

- jsonwebtoken 8.3.0 -> 10.4.0 with the `rust_crypto` backend (10.x has no
  default crypto backend). `jwk_algorithm` now reads `key_algorithm` (a
  `KeyAlgorithm`, signing and encryption algorithms alike) and maps only the
  signing subset onto `Algorithm`; `guest_jwt::jwk_algorithms` refuses a key
  naming a non-signing `alg` (RSA-OAEP, ...) instead of treating it as
  alg-less. `decode_without_verify` moves off the removed
  `insecure_disable_signature_validation` onto `dangerous::insecure_decode`.
- Workspace lock: only jsonwebtoken and `pem 1.1.1` (removed) change; the
  rust_crypto tree was already present. `ring 0.16.20` stays in
  backend/Cargo.lock because `gcp_auth 0.9.0` holds it.
- windmill-parser-wasm lock: jsonwebtoken 8.3.0 -> 10.4.0 drops `ring 0.16.20`
  (jsonwebtoken was its only holder there), `spin 0.5.2`, `untrusted 0.7.1`,
  `base64 0.13.1`; `pem 1.1.1 -> 3.0.6` and `serde_json 1.0.143 -> 1.0.151`
  were forced by the new jsonwebtoken.
- ee-repo-ref.txt -> 514eb5f5 (windmill-ee-private chore/ee-dep-bumps, on top
  of the previous ref d252afcc), which reads the JWK algorithm through
  `windmill_common::jwt::jwk_algorithm` and builds proto `KeyValue`s with
  `..Default::default()`.

The opentelemetry 0.32 bump is deferred: tracing-opentelemetry 0.33 removed
`OtelData`/`PreSampledTracer`, which the EE `otel_ee.rs` log bridge uses, and
`otel` ships in every EE image (`ee_core`); that bridge needs a
`Dispatch`-based rewrite first.

Checks (SQLX_OFFLINE, EE files from 514eb5f5): cargo check --features
all_sqlx_features; --features all_sqlx_features,private; --features
enterprise,private,otel; cargo test -p windmill-common --lib jwt (22 passed).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(jwt): keep audience-bearing tokens and 8192-bit RSA keys working on jsonwebtoken 10

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* chore: update ee-repo-ref to 6f3161192ab0eba36a8630b92e6765a36a3326fc

This commit updates the EE repository reference after PR #809 was merged in windmill-ee-private.

Previous ee-repo-ref: 7dcc5741c2fa9f9997b3da085f1f18ffc1e446d6

New ee-repo-ref: 6f3161192ab0eba36a8630b92e6765a36a3326fc

Automated by sync-ee-ref workflow.

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2026-09-21 14:36:03 +00:00

99 lines
3.5 KiB
Rust

use crate::error::{self, to_anyhow, Error};
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use hmac::{Hmac, Mac};
use serde::{de::DeserializeOwned, Serialize};
use sha2::Sha256;
lazy_static::lazy_static! {
pub static ref JWT_SECRET: arc_swap::ArcSwap<String> = arc_swap::ArcSwap::from_pointee("".to_string());
}
pub async fn encode_with_internal_secret<T: Serialize>(claims: T) -> error::Result<String> {
let jwt_secret = JWT_SECRET.load();
if jwt_secret.is_empty() {
return Err(Error::internal_err("JWT secret is not set".to_string()));
}
let token = jsonwebtoken::encode(
&jsonwebtoken::Header::new(jsonwebtoken::Algorithm::HS256),
&claims,
&jsonwebtoken::EncodingKey::from_secret(jwt_secret.as_bytes()),
)
.map_err(to_anyhow)?;
Ok(token)
}
pub async fn decode_with_internal_secret<T: DeserializeOwned>(token: &str) -> error::Result<T> {
let jwt_secret = JWT_SECRET.load();
if jwt_secret.is_empty() {
return Err(Error::internal_err("JWT secret is not set".to_string()));
}
let result = jsonwebtoken::decode::<T>(
token,
&jsonwebtoken::DecodingKey::from_secret(jwt_secret.as_bytes()),
&jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256),
)
.map_err(to_anyhow)?;
Ok(result.claims)
}
pub fn decode_without_verify<T: DeserializeOwned>(token: &str) -> anyhow::Result<T> {
// No signature, expiry, nbf or required-claim checks: the caller wants the raw claims.
let token_data = jsonwebtoken::dangerous::insecure_decode::<T>(token)?;
Ok(token_data.claims)
}
// header_and_payload: `{header}.{payload}`
pub async fn generate_signature(header_and_payload: &str) -> anyhow::Result<String> {
let header_and_payload = header_and_payload.trim_start_matches("jwt_ext_");
let header_and_payload = header_and_payload.trim_start_matches("jwt_");
let secret = JWT_SECRET.load();
// Create HMAC-SHA256
let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes())?;
mac.update(header_and_payload.as_bytes());
// Finalize and encode
let result = mac.finalize().into_bytes();
Ok(URL_SAFE_NO_PAD.encode(result))
}
/// The signing algorithm a JWK names in its `alg`, or `None` when it names none (or one
/// that is not a signing algorithm, e.g. `RSA-OAEP`). The only place that reads the
/// field, so callers, including EE ones, stay agnostic of how `jsonwebtoken` models it.
pub fn jwk_algorithm(jwk: &jsonwebtoken::jwk::Jwk) -> Option<jsonwebtoken::Algorithm> {
// `alg` is a `KeyAlgorithm` (signing and encryption algorithms alike); only the
// signing subset maps onto `Algorithm`.
jwk.common
.key_algorithm
.and_then(|alg| alg.to_string().parse::<jsonwebtoken::Algorithm>().ok())
}
#[cfg(test)]
mod tests {
use super::jwk_algorithm;
use jsonwebtoken::{jwk::Jwk, Algorithm};
#[test]
fn jwk_algorithm_reads_alg_when_present() {
let with: Jwk =
serde_json::from_value(serde_json::json!({"kty":"RSA","alg":"RS256","n":"aa","e":"AQAB"}))
.unwrap();
assert_eq!(jwk_algorithm(&with), Some(Algorithm::RS256));
let without: Jwk =
serde_json::from_value(serde_json::json!({"kty":"RSA","n":"aa","e":"AQAB"})).unwrap();
assert_eq!(jwk_algorithm(&without), None);
let encryption: Jwk = serde_json::from_value(
serde_json::json!({"kty":"RSA","alg":"RSA-OAEP","n":"aa","e":"AQAB"}),
)
.unwrap();
assert_eq!(jwk_algorithm(&encryption), None);
}
}