Merge pull request #6 from Keats/header

Add all params of header from RFC
This commit is contained in:
Vincent Prouillet 2015-12-22 17:07:17 +00:00
commit 0ea17b5bd9
5 changed files with 149 additions and 94 deletions

View File

@ -18,19 +18,21 @@ In terms of imports:
extern crate jsonwebtoken as jwt; extern crate jsonwebtoken as jwt;
extern crate rustc_serialize; extern crate rustc_serialize;
use jwt::{encode, decode, Algorithm}; use jwt::{encode, decode, Header, Algorithm};
``` ```
### Encoding ### Encoding
```rust ```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. 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 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 ### Decoding
```rust ```rust
let claims = decode::<Claims>(&token, "secret", Algorithm::HS256); let token = decode::<Claims>(&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: 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. 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. 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 ## Algorithms
Right now, only SHA family is supported: SHA256, SHA384 and SHA512. 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 ## Performance
On my thinkpad 440s for a 2 claims struct using SHA256: On my thinkpad 440s for a 2 claims struct using SHA256:

View File

@ -3,7 +3,7 @@ extern crate test;
extern crate jsonwebtoken as jwt; extern crate jsonwebtoken as jwt;
extern crate rustc_serialize; extern crate rustc_serialize;
use jwt::{encode, decode, Algorithm}; use jwt::{encode, decode, Algorithm, Header};
#[derive(Debug, PartialEq, Clone, RustcEncodable, RustcDecodable)] #[derive(Debug, PartialEq, Clone, RustcEncodable, RustcDecodable)]
struct Claims { struct Claims {
@ -18,11 +18,11 @@ fn bench_encode(b: &mut test::Bencher) {
company: "ACME".to_owned() company: "ACME".to_owned()
}; };
b.iter(|| encode(&claim, "secret", Algorithm::HS256)); b.iter(|| encode(Header::default(), &claim, "secret".as_ref()));
} }
#[bench] #[bench]
fn bench_decode(b: &mut test::Bencher) { fn bench_decode(b: &mut test::Bencher) {
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"; let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ";
b.iter(|| decode::<Claims>(token, "secret", Algorithm::HS256)); b.iter(|| decode::<Claims>(token, "secret".as_ref(), Algorithm::HS256));
} }

View File

@ -1,31 +1,47 @@
extern crate jsonwebtoken as jwt; extern crate jsonwebtoken as jwt;
extern crate rustc_serialize; extern crate rustc_serialize;
use jwt::{encode, decode, Algorithm}; use jwt::{encode, decode, Header, Algorithm};
use jwt::errors::{Error}; use jwt::errors::{Error};
#[derive(Debug, RustcEncodable, RustcDecodable)] #[derive(Debug, RustcEncodable, RustcDecodable)]
struct Claims { struct Claims {
sub: String, sub: String,
company: 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() { fn main() {
let my_claims = Claims { let my_claims = Claims {
sub: "b@b.com".to_owned(), sub: "b@b.com".to_owned(),
company: "ACME".to_owned() company: "ACME".to_owned()
}; };
let key = "secret"; 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, Ok(t) => t,
Err(_) => panic!() // in practice you would return the error Err(_) => panic!() // in practice you would return the error
}; };
let claims = match decode::<Claims>(&token, key.as_ref(), Algorithm::HS256) { let token_data = match decode::<Claims>(&token, key.as_ref(), Algorithm::HS256) {
Ok(c) => c, Ok(c) => c,
Err(err) => match err { Err(err) => match err {
Error::InvalidToken => panic!(), // Example on how to handle a specific error Error::InvalidToken => panic!(), // Example on how to handle a specific error
_ => panic!() _ => panic!()
} }
}; };
println!("{:?}", token_data.claims);
println!("{:?}", token_data.header);
println!("{:?}", token_data.claims.is_valid());
} }

39
examples/custom_header.rs Normal file
View File

@ -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::<Claims>(&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);
}

View File

@ -19,7 +19,7 @@ use crypto::util::fixed_time_eq;
pub mod errors; pub mod errors;
use errors::Error; use errors::Error;
#[derive(Debug, PartialEq, Copy, Clone)] #[derive(Debug, PartialEq, Copy, Clone, RustcDecodable, RustcEncodable)]
/// The algorithms supported for signing/verifying /// The algorithms supported for signing/verifying
pub enum Algorithm { pub enum Algorithm {
HS256, HS256,
@ -51,46 +51,42 @@ impl<T> Part for T where T: Encodable + Decodable {
} }
} }
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq, RustcDecodable, RustcEncodable)]
/// A basic JWT header part, the alg is automatically filled for use /// A basic JWT header part, the alg defaults to HS256 and typ is automatically
/// It's missing things like the kid but that's for later /// set to `JWT`. All the other fields are optional
pub struct Header { pub struct Header {
typ: &'static str, typ: String,
alg: Algorithm, pub alg: Algorithm,
pub jku: Option<String>,
pub kid: Option<String>,
pub x5u: Option<String>,
pub x5t: Option<String>
} }
impl Header { impl Header {
pub fn new(algorithm: Algorithm) -> Header { pub fn new(algorithm: Algorithm) -> Header {
Header { Header {
typ: "JWT", typ: "JWT".to_owned(),
alg: algorithm, alg: algorithm,
jku: None,
kid: None,
x5u: None,
x5t: None
} }
} }
} }
impl Part for Header { impl Default for Header {
type Encoded = &'static str; fn default() -> Header {
Header::new(Algorithm::HS256)
fn from_base64<B: AsRef<[u8]>>(encoded: B) -> Result<Self, Error> 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))
} }
}
fn to_base64(&self) -> Result<Self::Encoded, Error> { #[derive(Debug)]
let encoded = match self.alg { /// The return type of a successful call to decode(...)
Algorithm::HS256 => { "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9" }, pub struct TokenData<T: Part> {
Algorithm::HS384 => { "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9" }, pub header: Header,
Algorithm::HS512 => { "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9" }, pub claims: T
};
Ok(encoded)
}
} }
/// Take the payload of a JWT and sign it using the algorithm given. /// 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()) 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 /// Encode the claims passed and sign the payload using the algorithm from the header and the secret
pub fn encode<T: Part, B: AsRef<[u8]>>(claims: &T, secret: B, algorithm: Algorithm) -> Result<String, Error> { pub fn encode<T: Part>(header: Header, claims: &T, secret: &[u8]) -> Result<String, Error> {
let encoded_header = try!(Header::new(algorithm).to_base64()); let encoded_header = try!(header.to_base64());
let encoded_claims = try!(claims.to_base64()); let encoded_claims = try!(claims.to_base64());
// seems to be a tiny bit faster than format!("{}.{}", x, y) // seems to be a tiny bit faster than format!("{}.{}", x, y)
let payload = [encoded_header, encoded_claims.as_ref()].join("."); let payload = [encoded_header.as_ref(), encoded_claims.as_ref()].join(".");
let signature = sign(&*payload, secret.as_ref(), algorithm); let signature = sign(&*payload, secret.as_ref(), header.alg);
Ok([payload, signature].join(".")) 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 /// Decode a token into a Claims struct
/// If the token or its signature is invalid, it will return an error /// If the token or its signature is invalid, it will return an error
pub fn decode<T: Part>(token: &str, secret: &[u8], algorithm: Algorithm) -> Result<T, Error> { pub fn decode<T: Part>(token: &str, secret: &[u8], algorithm: Algorithm) -> Result<TokenData<T>, Error> {
// We don't use AsRef<[u8]> for `secret` because it would require changing this:
//
// decode::<MyStruct>(...)
//
// to:
//
// decode::<MyStruct, _>(...)
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)
}
}}
}
let (signature, payload) = expect_two!(token.rsplitn(2, '.')); let (signature, payload) = expect_two!(token.rsplitn(2, '.'));
let is_valid = verify( let is_valid = verify(
@ -165,13 +155,14 @@ pub fn decode<T: Part>(token: &str, secret: &[u8], algorithm: Algorithm) -> Resu
if header.alg != algorithm { if header.alg != algorithm {
return Err(Error::WrongAlgorithmHeader); 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)] #[cfg(test)]
mod tests { 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)] #[derive(Debug, PartialEq, Clone, RustcEncodable, RustcDecodable)]
struct Claims { struct Claims {
@ -179,29 +170,6 @@ mod tests {
company: String 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] #[test]
fn sign_hs256() { fn sign_hs256() {
let result = sign("hello world", b"secret", Algorithm::HS256); let result = sign("hello world", b"secret", Algorithm::HS256);
@ -216,15 +184,31 @@ mod tests {
assert!(valid); 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::<Claims>(&token, "secret".as_ref(), Algorithm::HS256).unwrap();
assert_eq!(my_claims, token_data.claims);
assert_eq!("kid", token_data.header.kid.unwrap());
}
#[test] #[test]
fn round_trip_claim() { fn round_trip_claim() {
let my_claims = Claims { let my_claims = Claims {
sub: "b@b.com".to_owned(), sub: "b@b.com".to_owned(),
company: "ACME".to_owned() company: "ACME".to_owned()
}; };
let token = encode(&my_claims, "secret", Algorithm::HS256).unwrap(); let token = encode(Header::default(), &my_claims, "secret".as_ref()).unwrap();
let claims = decode::<Claims>(&token, "secret".as_ref(), Algorithm::HS256).unwrap(); let token_data = decode::<Claims>(&token, "secret".as_ref(), Algorithm::HS256).unwrap();
assert_eq!(my_claims, claims); assert_eq!(my_claims, token_data.claims);
assert!(token_data.header.kid.is_none());
} }
#[test] #[test]
@ -243,11 +227,17 @@ mod tests {
claims.unwrap(); claims.unwrap();
} }
#[test] #[test]
fn decode_token_with_bytes_secret() { fn decode_token_with_bytes_secret() {
let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiY29tcGFueSI6Ikdvb2dvbCJ9.27QxgG96vpX4akKNpD1YdRGHE3_u2X35wR3EHA2eCrs"; let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiY29tcGFueSI6Ikdvb2dvbCJ9.27QxgG96vpX4akKNpD1YdRGHE3_u2X35wR3EHA2eCrs";
let claims = decode::<Claims>(token, b"\x01\x02\x03", Algorithm::HS256); let claims = decode::<Claims>(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::<Claims>(token, "secret".as_ref(), Algorithm::HS256);
assert!(claims.is_ok());
} }
} }