mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-14 08:02:58 +00:00
* 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>
99 lines
3.5 KiB
Rust
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);
|
|
}
|
|
}
|