Skip to content

Commit b012686

Browse files
committed
feat: add macros for custom headers and claims
Add `Header` derive macro, `header` attribute macro, and `claims` attribute macro. `Header` derive macro implements required traits for custom headers `header` and `claims` are convenience attribute macros that add the required derive macros.
1 parent 2f1ce37 commit b012686

File tree

11 files changed

+191
-51
lines changed

11 files changed

+191
-51
lines changed

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ rand = { version = "0.8.5", optional = true, features = ["std"], default-feature
4444
rsa = { version = "0.9.6", optional = true }
4545
sha2 = { version = "0.10.7", optional = true, features = ["oid"] }
4646

47+
# proc-macros (e.g. Header, claims)
48+
jsonwebtoken-proc-macros = { path = "jsonwebtoken-proc-macros" }
49+
4750
[target.'cfg(target_arch = "wasm32")'.dependencies]
4851
js-sys = "0.3"
4952
getrandom = "0.2"

examples/custom_header.rs

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,25 @@
1-
use serde::{Deserialize, Serialize};
21
use std::collections::HashMap;
32

43
use jsonwebtoken::errors::ErrorKind;
4+
use jsonwebtoken::macros::{claims, header};
55
use jsonwebtoken::{
6-
Algorithm, DecodingKey, EncodingKey, Validation, decode_with_custom_header, encode, header,
6+
Algorithm, DecodingKey, EncodingKey, Validation, decode_with_custom_header, encode,
77
};
88

9-
#[derive(Debug, Serialize, Deserialize, Clone)]
9+
#[claims]
1010
struct Claims {
1111
sub: String,
1212
company: String,
1313
exp: u64,
1414
}
1515

16-
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
16+
#[header]
17+
#[derive(PartialEq, Eq)] // only required for assertions in tests, not required by jsonwebtoken
1718
struct CustomHeader {
1819
alg: Algorithm,
1920
custom: String,
2021
another_custom_field: Option<usize>,
2122
}
22-
impl header::FromEncoded for CustomHeader {}
23-
impl header::Alg for CustomHeader {
24-
fn alg(&self) -> &Algorithm {
25-
&self.alg
26-
}
27-
}
2823

2924
fn main() {
3025
let my_claims =

examples/extras_header.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
use std::collections::HashMap;
2+
3+
use jsonwebtoken::errors::ErrorKind;
4+
use jsonwebtoken::{
5+
Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode, macros::claims,
6+
};
7+
8+
#[claims]
9+
struct Claims {
10+
sub: String,
11+
company: String,
12+
exp: u64,
13+
}
14+
15+
fn main() {
16+
let my_claims =
17+
Claims { sub: "[email protected]".to_owned(), company: "ACME".to_owned(), exp: 10000000000 };
18+
let key = b"secret";
19+
20+
let mut extras = HashMap::with_capacity(1);
21+
extras.insert("custom".to_string(), "header".to_string());
22+
23+
let header = Header { alg: Algorithm::HS512, extras, ..Default::default() };
24+
25+
let token = match encode(&header, &my_claims, &EncodingKey::from_secret(key)) {
26+
Ok(t) => t,
27+
Err(_) => panic!(), // in practice you would return the error
28+
};
29+
println!("{:?}", token);
30+
31+
let token_data = match decode::<Claims>(
32+
&token,
33+
&DecodingKey::from_secret(key),
34+
&Validation::new(Algorithm::HS512),
35+
) {
36+
Ok(c) => c,
37+
Err(err) => match *err.kind() {
38+
ErrorKind::InvalidToken => panic!(), // Example on how to handle a specific error
39+
_ => panic!(),
40+
},
41+
};
42+
println!("{:?}", token_data.claims);
43+
println!("{:?}", token_data.header);
44+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[package]
2+
name = "jsonwebtoken-proc-macros"
3+
version = "0.1.0"
4+
edition = "2024"
5+
6+
[lib]
7+
proc-macro = true
8+
9+
[dependencies]
10+
quote = "1.0.41"
11+
syn = { version = "2.0.106", features = ["full"] }
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
//! This library provides convenient derive and attribute macros for custom Header and Claim
2+
//! structs for `jsonwebtoken`.
3+
//!
4+
//! Example
5+
//! ```rust
6+
//! use jsonwebtoken::Algorithm;
7+
//! use jsonwebtoken::macros::header;
8+
//! #[header]
9+
//! struct CustomJwtHeader {
10+
//! // `alg` is the only required struct field
11+
//! alg: Algorithm,
12+
//! custom_header: Option<String>,
13+
//! another_custom_header: String,
14+
//! }
15+
//! ````
16+
extern crate proc_macro;
17+
18+
use proc_macro::TokenStream;
19+
use quote::quote;
20+
use syn::{DeriveInput, Item, ItemStruct, parse_macro_input};
21+
22+
/// Convenience macro for JWT header structs
23+
///
24+
/// Adds the following derive macros:
25+
/// ```rust
26+
/// #[derive(
27+
/// Debug,
28+
/// Clone,
29+
/// Default,
30+
/// serde::Serialize,
31+
/// serde::Deserialize
32+
/// )]
33+
/// ```
34+
#[proc_macro_attribute]
35+
pub fn claims(_attr: TokenStream, input: TokenStream) -> TokenStream {
36+
let mut item = parse_macro_input!(input as Item);
37+
38+
match &mut item {
39+
Item::Struct(ItemStruct { attrs, .. }) => {
40+
attrs.push(
41+
syn::parse_quote!(#[derive(Debug, Clone, Default, jsonwebtoken::serde::Serialize, jsonwebtoken::serde::Deserialize)]),
42+
);
43+
quote!(#item).into()
44+
}
45+
_ => syn::Error::new_spanned(&item, "#[header] can only be used on structs")
46+
.to_compile_error()
47+
.into(),
48+
}
49+
}
50+
51+
/// Convenience macro for JWT header structs
52+
///
53+
/// Adds the following derive macros:
54+
/// ```rust
55+
/// #[derive(
56+
/// Debug,
57+
/// Clone,
58+
/// Default,
59+
/// jsonwebtoken::macros::Header,
60+
/// serde::Serialize,
61+
/// serde::Deserialize
62+
/// )]
63+
/// ```
64+
#[proc_macro_attribute]
65+
pub fn header(_attr: TokenStream, input: TokenStream) -> TokenStream {
66+
let mut item = parse_macro_input!(input as Item);
67+
68+
match &mut item {
69+
Item::Struct(ItemStruct { attrs, .. }) => {
70+
attrs.push(syn::parse_quote!(#[derive(Debug, Clone, Default, jsonwebtoken::serde::Serialize, jsonwebtoken::serde::Deserialize, jsonwebtoken::macros::Header)]));
71+
quote!(#item).into()
72+
}
73+
_ => syn::Error::new_spanned(&item, "#[header] can only be used on structs")
74+
.to_compile_error()
75+
.into(),
76+
}
77+
}
78+
79+
/// Derive macro required for custom JWT headers used with `jsonwebtoken`
80+
///
81+
/// Requires an `alg: jsonwebtoken::Algorithm` field exists in the struct
82+
#[proc_macro_derive(Header)]
83+
pub fn derive_header(input: TokenStream) -> TokenStream {
84+
let input = parse_macro_input!(input as DeriveInput);
85+
86+
let name = &input.ident;
87+
let generics = &input.generics;
88+
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
89+
90+
let expanded = quote! {
91+
impl #impl_generics ::jsonwebtoken::header::FromEncoded for #name #ty_generics #where_clause {}
92+
impl #impl_generics ::jsonwebtoken::header::Alg for #name #ty_generics #where_clause {
93+
fn alg(&self) -> &::jsonwebtoken::Algorithm {
94+
&self.alg
95+
}
96+
}
97+
};
98+
99+
TokenStream::from(expanded)
100+
}

src/crypto/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! The cryptography of the `jsonwebtoken` crate is decoupled behind
2-
//! [`JwtSigner`] and [`JwtVerifier`] traits. These make use of `RustCrypto`'s
3-
//! [`Signer`] and [`Verifier`] traits respectively.
2+
//! [`JwtSigner`](crate::crypto::JwtSigner) and [`JwtVerifier`](crate::crypto::JwtVerifier) traits. These make use of `RustCrypto`'s
3+
//! [`Signer`](signature::Signer) and [`Verifier`](signature::Verifier) traits respectively.
44
55
use crate::algorithms::Algorithm;
66
use crate::errors::Result;

src/decoding.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@ where
325325
/// DANGER: This performs zero validation on the JWT
326326
pub fn insecure_decode<T: DeserializeOwned + Clone>(
327327
token: impl AsRef<[u8]>,
328-
) -> Result<TokenData<T>> {
328+
) -> Result<TokenData<Header, T>> {
329329
let token = token.as_ref();
330330

331331
let (_, message) = expect_two!(token.rsplitn(2, |b| *b == b'.'));

src/encoding.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,10 +153,9 @@ impl Debug for EncodingKey {
153153
/// If the algorithm given is RSA or EC, the key needs to be in the PEM format.
154154
///
155155
/// ```rust
156-
/// use serde::{Deserialize, Serialize};
157-
/// use jsonwebtoken::{encode, Algorithm, Header, EncodingKey};
156+
/// use jsonwebtoken::{encode, macros::claims, Algorithm, Header, EncodingKey};
158157
///
159-
/// #[derive(Debug, Serialize, Deserialize)]
158+
/// #[claims]
160159
/// struct Claims {
161160
/// sub: String,
162161
/// company: String

src/header.rs

Lines changed: 5 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ use std::collections::HashMap;
33
use std::result;
44

55
use base64::{Engine, engine::general_purpose::STANDARD};
6+
use jsonwebtoken_proc_macros::header;
67
use serde::{Deserialize, Deserializer, Serialize, Serializer};
78

89
use crate::algorithms::Algorithm;
910
use crate::errors::Result;
1011
use crate::jwk::Jwk;
12+
use crate::macros::Header;
1113
use crate::serialization::b64_decode;
1214

1315
const ZIP_SERIAL_DEFLATE: &str = "DEF";
@@ -137,7 +139,7 @@ pub trait FromEncoded {
137139

138140
/// A basic JWT header, the alg defaults to HS256 and typ is automatically
139141
/// set to `JWT`. All the other fields are optional.
140-
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
142+
#[derive(Header, Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
141143
pub struct Header {
142144
/// The type of JWS: it can only be "JWT" here
143145
///
@@ -254,24 +256,10 @@ impl Header {
254256
}
255257
}
256258

257-
impl Default for Header {
258-
/// Returns a JWT header using the default Algorithm, HS256
259-
fn default() -> Self {
260-
Header::new(Algorithm::default())
261-
}
262-
}
263-
264-
impl Alg for Header {
265-
fn alg(&self) -> &Algorithm {
266-
&self.alg
267-
}
268-
}
269-
270-
impl FromEncoded for Header {}
271-
272259
/// A truly basic JWT header, the alg defaults to HS256 and typ is automatically
273260
/// set to `JWT`. All the other fields are optional.
274-
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
261+
#[header]
262+
#[derive(Hash, PartialEq, Eq)]
275263
pub struct BasicHeader {
276264
/// The type of JWS: it can only be "JWT" here
277265
///
@@ -370,18 +358,3 @@ impl BasicHeader {
370358
}
371359
}
372360
}
373-
374-
impl Default for BasicHeader {
375-
/// Returns a JWT header using the default Algorithm, HS256
376-
fn default() -> Self {
377-
Self::new(Algorithm::default())
378-
}
379-
}
380-
381-
impl Alg for BasicHeader {
382-
fn alg(&self) -> &Algorithm {
383-
&self.alg
384-
}
385-
}
386-
387-
impl FromEncoded for BasicHeader {}

src/jwk.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#![allow(missing_docs)]
22
//! This crate contains types only for working JWK and JWK Sets
33
//! This is only meant to be used to deal with public JWK, not generate ones.
4-
//! Most of the code in this file is taken from https://github.com/lawliet89/biscuit but
4+
//! Most of the code in this file is taken from <https://github.com/lawliet89/biscuit> but
55
//! tweaked to remove the private bits as it's not the goal for this crate currently.
66
77
use std::{fmt, str::FromStr};
@@ -596,7 +596,7 @@ impl Jwk {
596596

597597
/// Compute the thumbprint of the JWK.
598598
///
599-
/// Per (RFC-7638)[https://datatracker.ietf.org/doc/html/rfc7638]
599+
/// Per (RFC-7638)[<https://datatracker.ietf.org/doc/html/rfc7638>]
600600
pub fn thumbprint(&self, hash_function: ThumbprintHash) -> String {
601601
let pre = match &self.algorithm {
602602
AlgorithmParameters::EllipticCurve(a) => match a.curve {

0 commit comments

Comments
 (0)