//! Create and parses JWT (JSON Web Tokens) //! #![cfg_attr(feature = "dev", allow(unstable_features))] #![cfg_attr(feature = "dev", feature(plugin))] #![cfg_attr(feature = "dev", plugin(clippy))] extern crate rustc_serialize; extern crate crypto; use rustc_serialize::{json, Encodable, Decodable}; use rustc_serialize::base64::{self, ToBase64, FromBase64}; use crypto::sha2::{Sha256, Sha384, Sha512}; use crypto::hmac::Hmac; use crypto::mac::Mac; use crypto::digest::Digest; use crypto::util::fixed_time_eq; pub mod errors; use errors::Error; #[derive(Debug, PartialEq, Copy, Clone, RustcDecodable, RustcEncodable)] /// The algorithms supported for signing/verifying pub enum Algorithm { HS256, HS384, HS512 } /// A part of the JWT: header and claims specifically /// Allows converting from/to struct with base64 pub trait Part { type Encoded: AsRef; fn from_base64>(encoded: B) -> Result where Self: Sized; fn to_base64(&self) -> Result; } impl Part for T where T: Encodable + Decodable { type Encoded = String; fn to_base64(&self) -> Result { let encoded = try!(json::encode(&self)); Ok(encoded.as_bytes().to_base64(base64::URL_SAFE)) } fn from_base64>(encoded: B) -> Result { let decoded = try!(encoded.as_ref().from_base64()); let s = try!(String::from_utf8(decoded)); Ok(try!(json::decode(&s))) } } #[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: String, 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".to_owned(), alg: algorithm, jku: None, kid: None, x5u: None, x5t: None } } } impl Default for Header { fn default() -> Header { Header::new(Algorithm::HS256) } } #[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. /// Returns the base64 url safe encoded of the hmac result fn sign(data: &str, secret: &[u8], algorithm: Algorithm) -> String { fn crypt(digest: D, data: &str, secret: &[u8]) -> String { let mut hmac = Hmac::new(digest, secret); hmac.input(data.as_bytes()); hmac.result().code().to_base64(base64::URL_SAFE) } match algorithm { Algorithm::HS256 => crypt(Sha256::new(), data, secret), Algorithm::HS384 => crypt(Sha384::new(), data, secret), Algorithm::HS512 => crypt(Sha512::new(), data, secret), } } /// Compares the signature given with a re-computed signature fn verify(signature: &str, data: &str, secret: &[u8], algorithm: Algorithm) -> bool { fixed_time_eq(signature.as_ref(), sign(data, secret, algorithm).as_ref()) } /// Encode the claims passed and sign the payload using the algorithm from the header and the secret pub fn encode(claims: &T, secret: &[u8], header: Header) -> 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.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, Error> { let (signature, payload) = expect_two!(token.rsplitn(2, '.')); let is_valid = verify( signature, payload, secret, algorithm ); if !is_valid { return Err(Error::InvalidSignature); } let (claims, header) = expect_two!(payload.rsplitn(2, '.')); let header = try!(Header::from_base64(header)); if header.alg != algorithm { return Err(Error::WrongAlgorithmHeader); } let decoded_claims = try!(T::from_base64(claims)); Ok(TokenData { header: header, claims: decoded_claims}) } #[cfg(test)] mod tests { use super::{encode, decode, Algorithm, Header, sign, verify}; #[derive(Debug, PartialEq, Clone, RustcEncodable, RustcDecodable)] struct Claims { sub: String, company: String } #[test] fn sign_hs256() { let result = sign("hello world", b"secret", Algorithm::HS256); let expected = "c0zGLzKEFWj0VxWuufTXiRMk5tlI5MbGDAYhzaxIYjo"; assert_eq!(result, expected); } #[test] fn verify_hs256() { let sig = "c0zGLzKEFWj0VxWuufTXiRMk5tlI5MbGDAYhzaxIYjo"; let valid = verify(sig, "hello world", b"secret", Algorithm::HS256); 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(&my_claims, "secret".as_ref(), header).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".as_ref(), Header::default()).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] #[should_panic(expected = "InvalidToken")] fn decode_token_missing_parts() { let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"; let claims = decode::(token, "secret".as_ref(), Algorithm::HS256); claims.unwrap(); } #[test] #[should_panic(expected = "InvalidSignature")] fn decode_token_invalid_signature() { let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUifQ.wrong"; let claims = decode::(token, "secret".as_ref(), Algorithm::HS256); 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); 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()); } }