Skip to content

feat: allow users to specify a custom header not defined in the struct #420

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
May 4, 2025
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ If you want to set the `kid` parameter or change the algorithm for example:
```rust
let mut header = Header::new(Algorithm::HS512);
header.kid = Some("blabla".to_owned());

let mut extras = HashMap::with_capacity(1);
extras.insert("custom".to_string(), "header".to_string());
header.extras = Some(extras);

let token = encode(&header, &my_claims, &EncodingKey::from_secret("secret".as_ref()))?;
```
Look at `examples/custom_header.rs` for a full working example.
Expand Down
12 changes: 10 additions & 2 deletions examples/custom_header.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

use jsonwebtoken::errors::ErrorKind;
use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
Expand All @@ -15,8 +16,15 @@ fn main() {
Claims { sub: "b@b.com".to_owned(), company: "ACME".to_owned(), exp: 10000000000 };
let key = b"secret";

let header =
Header { kid: Some("signing_key".to_owned()), alg: Algorithm::HS512, ..Default::default() };
let mut extras = HashMap::with_capacity(1);
extras.insert("custom".to_string(), "header".to_string());

let header = Header {
kid: Some("signing_key".to_owned()),
alg: Algorithm::HS512,
extras: Some(extras),
..Default::default()
};

let token = match encode(&header, &my_claims, &EncodingKey::from_secret(key)) {
Ok(t) => t,
Expand Down
8 changes: 5 additions & 3 deletions src/header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,12 @@ pub struct Header {
#[serde(rename = "x5t#S256")]
pub x5t_s256: Option<String>,

/// Any additional headers
/// Any additional non-standard headers not defined in [RFC7515#4.1](https://datatracker.ietf.org/doc/html/rfc7515#section-4.1).
/// Once serialized, all keys will be converted to fields at the root level of the header payload
/// Ex: Dict("custom" -> "header") will be converted to "{"typ": "JWT", ..., "custom": "header"}"
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(flatten)]
pub extra: Option<HashMap<String, String>>,
pub extras: Option<HashMap<String, String>>,
}

impl Header {
Expand All @@ -86,7 +88,7 @@ impl Header {
x5c: None,
x5t: None,
x5t_s256: None,
extra: None
extras: None,
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/jwk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ impl<'de> Deserialize<'de> for PublicKeyUse {
D: Deserializer<'de>,
{
struct PublicKeyUseVisitor;
impl<'de> de::Visitor<'de> for PublicKeyUseVisitor {
impl de::Visitor<'_> for PublicKeyUseVisitor {
type Value = PublicKeyUse;

fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
Expand Down Expand Up @@ -116,7 +116,7 @@ impl<'de> Deserialize<'de> for KeyOperations {
D: Deserializer<'de>,
{
struct KeyOperationsVisitor;
impl<'de> de::Visitor<'de> for KeyOperationsVisitor {
impl de::Visitor<'_> for KeyOperationsVisitor {
type Value = KeyOperations;

fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
Expand Down
2 changes: 1 addition & 1 deletion src/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ where
{
struct NumericType(PhantomData<fn() -> TryParse<u64>>);

impl<'de> Visitor<'de> for NumericType {
impl Visitor<'_> for NumericType {
type Value = TryParse<u64>;

fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
Expand Down
52 changes: 49 additions & 3 deletions tests/hmac.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use std::collections::HashMap;
use jsonwebtoken::errors::ErrorKind;
use jsonwebtoken::jwk::Jwk;
use jsonwebtoken::{
crypto::{sign, verify},
decode, decode_header, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use time::OffsetDateTime;
use wasm_bindgen_test::wasm_bindgen_test;

Expand Down Expand Up @@ -37,14 +37,58 @@ fn verify_hs256() {
#[test]
#[wasm_bindgen_test]
fn encode_with_custom_header() {
let my_claims = Claims {
sub: "b@b.com".to_string(),
company: "ACME".to_string(),
exp: OffsetDateTime::now_utc().unix_timestamp() + 10000,
};
let header = Header { kid: Some("kid".to_string()), ..Default::default() };
let token = encode(&header, &my_claims, &EncodingKey::from_secret(b"secret")).unwrap();
let token_data = decode::<Claims>(
&token,
&DecodingKey::from_secret(b"secret"),
&Validation::new(Algorithm::HS256),
)
.unwrap();
assert_eq!(my_claims, token_data.claims);
assert_eq!("kid", token_data.header.kid.unwrap());
}

#[test]
#[wasm_bindgen_test]
fn encode_with_extra_custom_header() {
let my_claims = Claims {
sub: "b@b.com".to_string(),
company: "ACME".to_string(),
exp: OffsetDateTime::now_utc().unix_timestamp() + 10000,
};
let mut extra = HashMap::with_capacity(1);
extra.insert("custom".to_string(), "header".to_string());
let header = Header { kid: Some("kid".to_string()), extra: Some(extra), ..Default::default() };
let header = Header { kid: Some("kid".to_string()), extras: Some(extra), ..Default::default() };
let token = encode(&header, &my_claims, &EncodingKey::from_secret(b"secret")).unwrap();
let token_data = decode::<Claims>(
&token,
&DecodingKey::from_secret(b"secret"),
&Validation::new(Algorithm::HS256),
)
.unwrap();
assert_eq!(my_claims, token_data.claims);
assert_eq!("kid", token_data.header.kid.unwrap());
assert_eq!("header", token_data.header.extras.unwrap().get("custom").unwrap().as_str());
}

#[test]
#[wasm_bindgen_test]
fn encode_with_multiple_extra_custom_headers() {
let my_claims = Claims {
sub: "b@b.com".to_string(),
company: "ACME".to_string(),
exp: OffsetDateTime::now_utc().unix_timestamp() + 10000,
};
let mut extra = HashMap::with_capacity(1);
extra.insert("custom1".to_string(), "header1".to_string());
extra.insert("custom2".to_string(), "header2".to_string());
let header = Header { kid: Some("kid".to_string()), extras: Some(extra), ..Default::default() };
let token = encode(&header, &my_claims, &EncodingKey::from_secret(b"secret")).unwrap();
let token_data = decode::<Claims>(
&token,
Expand All @@ -54,7 +98,9 @@ fn encode_with_custom_header() {
.unwrap();
assert_eq!(my_claims, token_data.claims);
assert_eq!("kid", token_data.header.kid.unwrap());
assert_eq!("header", token_data.header.extra.unwrap().get("custom").unwrap().as_str());
let extras = token_data.header.extras.unwrap();
assert_eq!("header1", extras.get("custom1").unwrap().as_str());
assert_eq!("header2", extras.get("custom2").unwrap().as_str());
}

#[test]
Expand Down