jwt_hmac/
lib.rs

1use serde::{Serialize, Deserialize};
2use base64_url;
3use serde_json;
4use hmac;
5use hmac::{Mac, HmacCore};
6use sha2::Sha256;
7use std::fmt::{Display, Formatter};
8use std::result;
9use std::str::{from_utf8, Utf8Error};
10use base64_url::base64::DecodeError;
11use hmac::digest::{CtOutput, InvalidLength};
12use hmac::digest::core_api::CoreWrapper;
13
14static HEADER: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9";
15static HEADER_LENGTH: usize = HEADER.len();
16static SIGNATURE_LENGTH: usize = 43;
17static MIN_TOKEN_LENGTH: usize = HEADER_LENGTH + SIGNATURE_LENGTH + 3;
18
19#[derive(Debug)]
20pub enum Error{
21    JsonError(serde_json::error::Error),
22    InvalidKeyLength(InvalidLength),
23    Base64UrlDecodeError(DecodeError),
24    Utf8Error(Utf8Error),
25    InvalidHeader,
26    InvalidChecksum,
27    TooShort
28}
29
30pub type Result<T> = result::Result<T, Error>;
31
32impl Display for Error {
33    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
34        match &*self{
35            Self::JsonError(error) => error.fmt(f),
36            Self::Base64UrlDecodeError(error) => error.fmt(f),
37            Self::InvalidKeyLength(error) => error.fmt(f),
38            Self::Utf8Error(error) => error.fmt(f),
39            Self::InvalidHeader => f.write_str("unsupported header values"),
40            Self::InvalidChecksum => f.write_str("checksum does not match"),
41            Self::TooShort => f.write_str("token is too short")
42        }
43    }
44}
45
46impl std::error::Error for Error{}
47
48impl From<Utf8Error> for Error{
49    fn from(value: Utf8Error) -> Self {
50        Self::Utf8Error(value)
51    }
52}
53
54impl From<DecodeError> for Error{
55    fn from(value: DecodeError) -> Self {
56        Self::Base64UrlDecodeError(value)
57    }
58}
59
60impl From<serde_json::error::Error> for Error{
61    fn from(value: serde_json::Error) -> Self {
62        Self::JsonError(value)
63    }
64}
65
66impl From<InvalidLength> for Error{
67    fn from(value: InvalidLength) -> Self {
68        Self::InvalidKeyLength(value)
69    }
70}
71
72fn calc_checksum(secret: &[u8], value: &[u8]) -> Result<CtOutput<CoreWrapper<HmacCore<Sha256>>>>{
73    let mut hasher = hmac::Hmac::<Sha256>::new_from_slice(secret)?;
74    hasher.update(value);
75    Ok(hasher.finalize())
76}
77
78fn body_with_header<T>(claims: &T) -> Result<String> where T: Serialize {
79    let serialized = base64_url::encode(serde_json::to_string(&claims)?.as_bytes());
80    let mut output = String::with_capacity(serialized.len() + MIN_TOKEN_LENGTH);
81    output.push_str(HEADER);
82    output.push('.');
83    output.push_str(serialized.as_str());
84    Ok(output)
85}
86
87/// Deserialize the claims struct from a jwt token
88///
89/// #Example
90///
91/// ```
92/// use jwt_hmac;
93/// use serde::Deserialize;
94///
95/// #[derive(Deserialize)]
96///  struct Claims{
97///     sub: String
98///  }
99///
100///  fn main(){
101///     let secret = "I'm a secret".as_bytes();
102///     match jwt_hmac::parse::<Claims>(
103///         secret,
104///         "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.6ekn8MWtOmVT6FMqbAlVQQmretopbWpef_lHV9dYMf4"
105///     ){
106///         Ok(claims) => println!("Sub: {}", claims.sub),
107///         Err(error) => match error{
108///             jwt_hmac::Error::InvalidChecksum => println!("Secret doesn't match"),
109///             _ => println!("Probably not a valid JWT: {}", error)
110///         }
111///     }
112///  }
113/// ```
114pub fn parse<T>(secret: &[u8], token: &str) -> Result<T> where T: for<'a> Deserialize<'a> {
115    let len = token.len();
116    if len < MIN_TOKEN_LENGTH{
117        return Err(Error::TooShort);
118    }
119    let bytes = token.as_bytes();
120    if &bytes[..HEADER_LENGTH] != HEADER.as_bytes() {
121        return Err(Error::InvalidHeader)
122    }
123    let sig_offset = len - SIGNATURE_LENGTH;
124    let checksum = calc_checksum(secret, &bytes[..sig_offset - 1])?;
125    let signature = base64_url::decode(from_utf8(&bytes[sig_offset..])?)?;
126    if checksum.into_bytes().as_slice() != signature.as_slice(){
127        return Err(Error::InvalidChecksum);
128    }
129    Ok(serde_json::from_slice::<T>(
130        base64_url::decode(&bytes[HEADER_LENGTH + 1 .. sig_offset - 1])?.as_slice()
131    )?)
132}
133
134/// Generate a JWT token with the provided claims using the given secret
135///
136/// #Example
137///
138/// ```
139/// use jwt_hmac;
140/// use serde::Serialize;
141///
142/// #[derive(Serialize)]
143///  struct Claims{
144///     sub: String,
145///     name: String,
146///     admin: bool
147///  }
148///
149///  fn main(){
150///     let secret = "I'm a secret".as_bytes();
151///     let claims = Claims{
152///         sub: "1234567890".to_string(),
153///         name: "John Doe".to_string(),
154///         admin: true
155///     };
156///     match jwt_hmac::create(secret, &claims) {
157///         Ok(token) => println!("Token: {}", token),
158///         Err(error) => println!("This can't be happening {}", error)
159///     }
160///  }
161/// ```
162pub fn create<T>(secret: &[u8], claims: &T) -> Result<String> where T: Serialize {
163    let mut main = body_with_header(claims)?;
164    let hash = base64_url::encode(
165        calc_checksum(secret, main.as_bytes())?.into_bytes().as_slice()
166    );
167    main.push('.');
168    main.push_str(hash.as_str());
169    Ok(main)
170}
171
172#[cfg(test)]
173mod tests {
174    use crate::{create, parse, Error};
175    use serde::{Deserialize, Serialize};
176
177    static SERIALIZED: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.6ekn8MWtOmVT6FMqbAlVQQmretopbWpef_lHV9dYMf4";
178    static BAD_CHECKSUM: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.azXPRJHeWcZ_B5aHtA98gsnowX5gifvMJX2hoH_4YPs";
179    static NAME: &str = "John Doe";
180    static SECRET: &str = "I'm a secret";
181
182    #[derive(Serialize)]
183    struct OutClaims{
184        sub: String,
185        name: String,
186        admin: bool
187    }
188
189    #[derive(Deserialize)]
190    struct InClaims{
191        name: String
192    }
193
194    #[test]
195    fn can_create() {
196        let test = OutClaims{
197            sub: "1234567890".to_string(),
198            name: NAME.to_string(),
199            admin: true
200        };
201        match create(SECRET.as_bytes(), &test){
202            Ok(token) => {
203                assert_eq!(token.as_str(), SERIALIZED)
204            },
205            Err(error) => panic!("{}", error)
206        }
207    }
208
209    #[test]
210    fn can_parse(){
211        match parse::<InClaims>(SECRET.as_bytes(), SERIALIZED){
212            Ok(claims) => assert_eq!(claims.name, NAME),
213            Err(error) => panic!("{}", error)
214        }
215    }
216
217    #[test]
218    fn recognizes_bad_checksum(){
219        match parse::<InClaims>(SECRET.as_bytes(), BAD_CHECKSUM){
220            Ok(_) => assert!(false, "Should not recognize checksum"),
221            Err(error) => match error {
222                Error::InvalidChecksum => return,
223                _ => assert!(false, "Should have produced Error::InvalidChecksum")
224            }
225        }
226    }
227}