diff --git a/README.md b/README.md index 7d1a96b..4f654d2 100644 --- a/README.md +++ b/README.md @@ -18,19 +18,21 @@ In terms of imports: extern crate jsonwebtoken as jwt; extern crate rustc_serialize; -use jwt::{encode, decode, Algorithm}; +use jwt::{encode, decode, Header, Algorithm}; ``` ### Encoding ```rust -let token = encode(&my_claims, "secret", Algorithm::HS256); +let token = encode(Header::default(), &my_claims, "secret".as_ref()).unwrap(); ``` In that example, `my_claims` is an instance of the Claims struct. The struct you are using for your claims should derive `RustcEncodable` and `RustcDecodable`. +The default algorithm is HS256. Look at custom headers section to see how to change that. ### Decoding ```rust -let claims = decode::(&token, "secret", Algorithm::HS256); +let token = decode::(&token, "secret", Algorithm::HS256).unwrap(); +// token is a struct with 2 params: header and claims ``` In addition to the normal base64/json decoding errors, `decode` can return two custom errors: @@ -42,12 +44,20 @@ In addition to the normal base64/json decoding errors, `decode` can return two c Right now, the library only validates the algorithm type used but does not verify claims such as expiration. Feel free to add a `validate` method to your claims struct to handle that. +### Custom headers +All the parameters from the RFC are supported but the default header only has `typ` and `alg` set: all the other fields are optional. +If you want to set the `kid` parameter for example: + +```rust +let mut header = Header::default(); +header.kid = Some("blabla".to_owned()); +header.alg = Algorithm::HS512; +let token = encode(header, &my_claims, "secret".as_ref()).unwrap(); +``` + ## Algorithms Right now, only SHA family is supported: SHA256, SHA384 and SHA512. -## Missing -The header is currently not customisable and therefore does not support things like kid right now. - ## Performance On my thinkpad 440s for a 2 claims struct using SHA256: diff --git a/benches/jwt.rs b/benches/jwt.rs index d45d193..5813bc9 100644 --- a/benches/jwt.rs +++ b/benches/jwt.rs @@ -3,7 +3,7 @@ extern crate test; extern crate jsonwebtoken as jwt; extern crate rustc_serialize; -use jwt::{encode, decode, Algorithm}; +use jwt::{encode, decode, Algorithm, Header}; #[derive(Debug, PartialEq, Clone, RustcEncodable, RustcDecodable)] struct Claims { @@ -18,11 +18,11 @@ fn bench_encode(b: &mut test::Bencher) { company: "ACME".to_owned() }; - b.iter(|| encode(&claim, "secret", Algorithm::HS256)); + b.iter(|| encode(Header::default(), &claim, "secret".as_ref())); } #[bench] fn bench_decode(b: &mut test::Bencher) { let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"; - b.iter(|| decode::(token, "secret", Algorithm::HS256)); + b.iter(|| decode::(token, "secret".as_ref(), Algorithm::HS256)); } diff --git a/examples/claims.rs b/examples/claims.rs index b6bbe39..0e70c18 100644 --- a/examples/claims.rs +++ b/examples/claims.rs @@ -1,31 +1,47 @@ extern crate jsonwebtoken as jwt; extern crate rustc_serialize; -use jwt::{encode, decode, Algorithm}; +use jwt::{encode, decode, Header, Algorithm}; use jwt::errors::{Error}; + #[derive(Debug, RustcEncodable, RustcDecodable)] struct Claims { sub: String, company: String } +// Example validation implementation +impl Claims { + fn is_valid(self) -> bool { + if self.company != "ACME".to_owned() { + return false; + } + // expiration etc + + true + } +} + fn main() { let my_claims = Claims { sub: "b@b.com".to_owned(), company: "ACME".to_owned() }; let key = "secret"; - let token = match encode(&my_claims, key, Algorithm::HS256) { + let token = match encode(Header::default(), &my_claims, key.as_ref()) { Ok(t) => t, Err(_) => panic!() // in practice you would return the error }; - let claims = match decode::(&token, key.as_ref(), Algorithm::HS256) { + let token_data = match decode::(&token, key.as_ref(), Algorithm::HS256) { Ok(c) => c, Err(err) => match err { Error::InvalidToken => panic!(), // Example on how to handle a specific error _ => panic!() } }; + println!("{:?}", token_data.claims); + println!("{:?}", token_data.header); + println!("{:?}", token_data.claims.is_valid()); } diff --git a/examples/custom_header.rs b/examples/custom_header.rs new file mode 100644 index 0000000..1b7e686 --- /dev/null +++ b/examples/custom_header.rs @@ -0,0 +1,39 @@ +extern crate jsonwebtoken as jwt; +extern crate rustc_serialize; + +use jwt::{encode, decode, Header, Algorithm}; +use jwt::errors::{Error}; + + +#[derive(Debug, RustcEncodable, RustcDecodable)] +struct Claims { + sub: String, + company: String +} + +fn main() { + let my_claims = Claims { + sub: "b@b.com".to_owned(), + company: "ACME".to_owned() + }; + let key = "secret"; + + let mut header = Header::default(); + header.kid = Some("signing_key".to_owned()); + header.alg = Algorithm::HS512; + + let token = match encode(header, &my_claims, key.as_ref()) { + Ok(t) => t, + Err(_) => panic!() // in practice you would return the error + }; + + let token_data = match decode::(&token, key.as_ref(), Algorithm::HS512) { + Ok(c) => c, + Err(err) => match err { + Error::InvalidToken => panic!(), // Example on how to handle a specific error + _ => panic!() + } + }; + println!("{:?}", token_data.claims); + println!("{:?}", token_data.header); +} diff --git a/src/lib.rs b/src/lib.rs index bfaa55d..451c391 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,7 +19,7 @@ use crypto::util::fixed_time_eq; pub mod errors; use errors::Error; -#[derive(Debug, PartialEq, Copy, Clone)] +#[derive(Debug, PartialEq, Copy, Clone, RustcDecodable, RustcEncodable)] /// The algorithms supported for signing/verifying pub enum Algorithm { HS256, @@ -51,46 +51,42 @@ impl Part for T where T: Encodable + Decodable { } } -#[derive(Debug, PartialEq)] -/// A basic JWT header part, the alg is automatically filled for use -/// It's missing things like the kid but that's for later +#[derive(Debug, PartialEq, RustcDecodable, RustcEncodable)] +/// A basic JWT header part, the alg defaults to HS256 and typ is automatically +/// set to `JWT`. All the other fields are optional pub struct Header { - typ: &'static str, - alg: Algorithm, + typ: String, + pub alg: Algorithm, + pub jku: Option, + pub kid: Option, + pub x5u: Option, + pub x5t: Option } impl Header { pub fn new(algorithm: Algorithm) -> Header { Header { - typ: "JWT", + typ: "JWT".to_owned(), alg: algorithm, + jku: None, + kid: None, + x5u: None, + x5t: None } } } -impl Part for Header { - type Encoded = &'static str; - - fn from_base64>(encoded: B) -> Result where Self: Sized { - let algoritm = match encoded.as_ref() { - b"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9" => { Algorithm::HS256 }, - b"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9" => { Algorithm::HS384 }, - b"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9" => { Algorithm::HS512 }, - _ => return Err(Error::InvalidToken) - }; - - Ok(Header::new(algoritm)) +impl Default for Header { + fn default() -> Header { + Header::new(Algorithm::HS256) } +} - fn to_base64(&self) -> Result { - let encoded = match self.alg { - Algorithm::HS256 => { "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9" }, - Algorithm::HS384 => { "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9" }, - Algorithm::HS512 => { "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9" }, - }; - - Ok(encoded) - } +#[derive(Debug)] +/// The return type of a successful call to decode(...) +pub struct TokenData { + pub header: Header, + pub claims: T } /// Take the payload of a JWT and sign it using the algorithm given. @@ -114,38 +110,32 @@ fn verify(signature: &str, data: &str, secret: &[u8], algorithm: Algorithm) -> b fixed_time_eq(signature.as_ref(), sign(data, secret, algorithm).as_ref()) } -/// Encode the claims passed and sign the payload using the algorithm and the secret -pub fn encode>(claims: &T, secret: B, algorithm: Algorithm) -> Result { - let encoded_header = try!(Header::new(algorithm).to_base64()); +/// Encode the claims passed and sign the payload using the algorithm from the header and the secret +pub fn encode(header: Header, claims: &T, secret: &[u8]) -> Result { + let encoded_header = try!(header.to_base64()); let encoded_claims = try!(claims.to_base64()); // seems to be a tiny bit faster than format!("{}.{}", x, y) - let payload = [encoded_header, encoded_claims.as_ref()].join("."); - let signature = sign(&*payload, secret.as_ref(), algorithm); + let payload = [encoded_header.as_ref(), encoded_claims.as_ref()].join("."); + let signature = sign(&*payload, secret.as_ref(), header.alg); Ok([payload, signature].join(".")) } +/// Used in decode: takes the result of a rsplit and ensure we only get 2 parts +/// Errors if we don't +macro_rules! expect_two { + ($iter:expr) => {{ + let mut i = $iter; // evaluate the expr + match (i.next(), i.next(), i.next()) { + (Some(first), Some(second), None) => (first, second), + _ => return Err(Error::InvalidToken) + } + }} +} + /// Decode a token into a Claims struct /// If the token or its signature is invalid, it will return an error -pub fn decode(token: &str, secret: &[u8], algorithm: Algorithm) -> Result { - // We don't use AsRef<[u8]> for `secret` because it would require changing this: - // - // decode::(...) - // - // to: - // - // decode::(...) - - macro_rules! expect_two { - ($iter:expr) => {{ - let mut i = $iter; // evaluate the expr - match (i.next(), i.next(), i.next()) { - (Some(first), Some(second), None) => (first, second), - _ => return Err(Error::InvalidToken) - } - }} - } - +pub fn decode(token: &str, secret: &[u8], algorithm: Algorithm) -> Result, Error> { let (signature, payload) = expect_two!(token.rsplitn(2, '.')); let is_valid = verify( @@ -165,13 +155,14 @@ pub fn decode(token: &str, secret: &[u8], algorithm: Algorithm) -> Resu if header.alg != algorithm { return Err(Error::WrongAlgorithmHeader); } + let decoded_claims = try!(T::from_base64(claims)); - T::from_base64(claims) + Ok(TokenData { header: header, claims: decoded_claims}) } #[cfg(test)] mod tests { - use super::{encode, decode, Algorithm, Header, Part, sign, verify}; + use super::{encode, decode, Algorithm, Header, sign, verify}; #[derive(Debug, PartialEq, Clone, RustcEncodable, RustcDecodable)] struct Claims { @@ -179,29 +170,6 @@ mod tests { company: String } - #[test] - fn to_base64() { - let expected = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9".to_owned(); - let result = Header::new(Algorithm::HS256).to_base64(); - - assert_eq!(expected, result.unwrap()); - } - - #[test] - fn from_base64() { - let encoded = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9"; - let header = Header::from_base64(encoded).unwrap(); - - assert_eq!(header.typ, "JWT"); - assert_eq!(header.alg, Algorithm::HS256); - } - - #[test] - fn round_trip_base64() { - let header = Header::new(Algorithm::HS256); - assert_eq!(Header::from_base64(header.to_base64().unwrap()).unwrap(), header); - } - #[test] fn sign_hs256() { let result = sign("hello world", b"secret", Algorithm::HS256); @@ -216,15 +184,31 @@ mod tests { assert!(valid); } + #[test] + fn encode_with_custom_header() { + // TODO: test decode value + let my_claims = Claims { + sub: "b@b.com".to_owned(), + company: "ACME".to_owned() + }; + let mut header = Header::default(); + header.kid = Some("kid".to_owned()); + let token = encode(header, &my_claims, "secret".as_ref()).unwrap(); + let token_data = decode::(&token, "secret".as_ref(), Algorithm::HS256).unwrap(); + assert_eq!(my_claims, token_data.claims); + assert_eq!("kid", token_data.header.kid.unwrap()); + } + #[test] fn round_trip_claim() { let my_claims = Claims { sub: "b@b.com".to_owned(), company: "ACME".to_owned() }; - let token = encode(&my_claims, "secret", Algorithm::HS256).unwrap(); - let claims = decode::(&token, "secret".as_ref(), Algorithm::HS256).unwrap(); - assert_eq!(my_claims, claims); + let token = encode(Header::default(), &my_claims, "secret".as_ref()).unwrap(); + let token_data = decode::(&token, "secret".as_ref(), Algorithm::HS256).unwrap(); + assert_eq!(my_claims, token_data.claims); + assert!(token_data.header.kid.is_none()); } #[test] @@ -243,11 +227,17 @@ mod tests { claims.unwrap(); } - #[test] fn decode_token_with_bytes_secret() { let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiY29tcGFueSI6Ikdvb2dvbCJ9.27QxgG96vpX4akKNpD1YdRGHE3_u2X35wR3EHA2eCrs"; let claims = decode::(token, b"\x01\x02\x03", Algorithm::HS256); - claims.unwrap(); + assert!(claims.is_ok()); + } + + #[test] + fn decode_token_with_shuffled_header_fields() { + let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjb21wYW55IjoiMTIzNDU2Nzg5MCIsInN1YiI6IkpvaG4gRG9lIn0.SEIZ4Jg46VGhquuwPYDLY5qHF8AkQczF14aXM3a2c28"; + let claims = decode::(token, "secret".as_ref(), Algorithm::HS256); + assert!(claims.is_ok()); } }