From e3a4294e85ed5e170c56993e96206e12ddf35770 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Mon, 9 Jan 2017 14:50:51 +0900 Subject: [PATCH 01/13] Move to serde + base64 Remove Part trait Move tests to tests directory Reorganise code --- Cargo.toml | 5 + benches/jwt.rs | 5 +- examples/claims.rs | 10 +- examples/custom_header.rs | 11 +- src/crypto.rs | 103 +++++++++++++ src/errors.rs | 79 +++------- src/header.rs | 33 +++++ src/lib.rs | 295 ++------------------------------------ tests/lib.rs | 97 +++++++++++++ 9 files changed, 281 insertions(+), 357 deletions(-) create mode 100644 src/crypto.rs create mode 100644 src/header.rs create mode 100644 tests/lib.rs diff --git a/Cargo.toml b/Cargo.toml index bbf2eb5..aa04685 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,3 +12,8 @@ keywords = ["jwt", "web", "api", "token", "json"] [dependencies] rustc-serialize = "^0.3" ring = "^0.7" +error-chain = "0.9" +serde_json = "0.9" +serde_derive = "0.9" +serde = "0.9" +base64 = "0.4" diff --git a/benches/jwt.rs b/benches/jwt.rs index 5813bc9..3f8c692 100644 --- a/benches/jwt.rs +++ b/benches/jwt.rs @@ -1,11 +1,12 @@ #![feature(test)] extern crate test; extern crate jsonwebtoken as jwt; -extern crate rustc_serialize; +#[macro_use] +extern crate serde_derive; use jwt::{encode, decode, Algorithm, Header}; -#[derive(Debug, PartialEq, Clone, RustcEncodable, RustcDecodable)] +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] struct Claims { sub: String, company: String diff --git a/examples/claims.rs b/examples/claims.rs index 1c138ad..850b828 100644 --- a/examples/claims.rs +++ b/examples/claims.rs @@ -1,11 +1,11 @@ extern crate jsonwebtoken as jwt; -extern crate rustc_serialize; +#[macro_use] extern crate serde_derive; use jwt::{encode, decode, Header, Algorithm}; -use jwt::errors::{Error}; +use jwt::errors::{ErrorKind}; -#[derive(Debug, RustcEncodable, RustcDecodable)] +#[derive(Debug, Serialize, Deserialize)] struct Claims { sub: String, company: String @@ -38,8 +38,8 @@ fn main() { 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 + Err(err) => match *err.kind() { + ErrorKind::InvalidToken => panic!(), // Example on how to handle a specific error _ => panic!() } }; diff --git a/examples/custom_header.rs b/examples/custom_header.rs index 1b7e686..b09def8 100644 --- a/examples/custom_header.rs +++ b/examples/custom_header.rs @@ -1,11 +1,12 @@ extern crate jsonwebtoken as jwt; -extern crate rustc_serialize; +#[macro_use] +extern crate serde_derive; use jwt::{encode, decode, Header, Algorithm}; -use jwt::errors::{Error}; +use jwt::errors::{ErrorKind}; -#[derive(Debug, RustcEncodable, RustcDecodable)] +#[derive(Debug, Serialize, Deserialize)] struct Claims { sub: String, company: String @@ -29,8 +30,8 @@ fn main() { 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 + Err(err) => match *err.kind() { + ErrorKind::InvalidToken => panic!(), // Example on how to handle a specific error _ => panic!() } }; diff --git a/src/crypto.rs b/src/crypto.rs new file mode 100644 index 0000000..3063b17 --- /dev/null +++ b/src/crypto.rs @@ -0,0 +1,103 @@ +use base64; +use ring::{digest, hmac}; +use ring::constant_time::verify_slices_are_equal; +use serde::de::Deserialize; +use serde::ser::Serialize; +use serde_json; + + +use errors::{Result, ErrorKind}; +use header::Header; + + +/// The algorithms supported for signing/verifying +#[derive(Debug, PartialEq, Copy, Clone, Serialize, Deserialize)] +pub enum Algorithm { + HS256, + HS384, + HS512 +} + +/// Serializes and encodes to base64 +fn to_jwt_part(input: &T) -> Result { + let encoded = serde_json::to_string(input)?; + Ok(base64::encode_config(encoded.as_bytes(), base64::URL_SAFE_NO_PAD)) +} + +/// Decodes from base64 and deserializes +fn from_jwt_part, T: Deserialize>(encoded: B) -> Result { + let decoded = base64::decode_config(encoded.as_ref(), base64::URL_SAFE_NO_PAD)?; + let s = String::from_utf8(decoded)?; + + Ok(serde_json::from_str(&s)?) +} + + +/// The return type of a successful call to decode(...) +#[derive(Debug)] +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 +pub fn sign(data: &str, secret: &[u8], algorithm: Algorithm) -> String { + let digest = match algorithm { + Algorithm::HS256 => &digest::SHA256, + Algorithm::HS384 => &digest::SHA384, + Algorithm::HS512 => &digest::SHA512, + }; + let key = hmac::SigningKey::new(digest, secret); + base64::encode_config( + hmac::sign(&key, data.as_bytes()).as_ref(), + base64::URL_SAFE_NO_PAD + ) +} + +/// Compares the signature given with a re-computed signature +pub fn verify(signature: &str, data: &str, secret: &[u8], algorithm: Algorithm) -> bool { + verify_slices_are_equal(signature.as_ref(), sign(data, secret, algorithm).as_ref()).is_ok() +} + +/// 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 = to_jwt_part(&header)?; + let encoded_claims = to_jwt_part(&claims)?; + 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; + match (i.next(), i.next(), i.next()) { + (Some(first), Some(second), None) => (first, second), + _ => return Err(ErrorKind::InvalidToken.into()) + } + }} +} + +/// 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> { + let (signature, payload) = expect_two!(token.rsplitn(2, '.')); + + if !verify(signature, payload, secret, algorithm) { + return Err(ErrorKind::InvalidSignature.into()); + } + + let (claims, header) = expect_two!(payload.rsplitn(2, '.')); + + let header: Header = from_jwt_part(header)?; + if header.alg != algorithm { + return Err(ErrorKind::WrongAlgorithmHeader.into()); + } + let decoded_claims: T = from_jwt_part(claims)?; + + Ok(TokenData { header: header, claims: decoded_claims }) +} diff --git a/src/errors.rs b/src/errors.rs index 1d78005..5215c54 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,68 +1,25 @@ -use std::{string, fmt, error}; -use rustc_serialize::{json, base64}; +use base64; +use serde_json; -#[derive(Debug)] -/// All the errors we can encounter while signing/verifying tokens -/// and a couple of custom one for when the token we are trying -/// to verify is invalid -pub enum Error { - EncodeJSON(json::EncoderError), - DecodeBase64(base64::FromBase64Error), - DecodeJSON(json::DecoderError), - Utf8(string::FromUtf8Error), - - InvalidToken, - InvalidSignature, - WrongAlgorithmHeader -} - -macro_rules! impl_from_error { - ($f: ty, $e: expr) => { - impl From<$f> for Error { - fn from(f: $f) -> Error { $e(f) } +error_chain! { + errors { + InvalidToken { + description("invalid token") + display("Invalid token") } - } -} - -impl_from_error!(json::EncoderError, Error::EncodeJSON); -impl_from_error!(base64::FromBase64Error, Error::DecodeBase64); -impl_from_error!(json::DecoderError, Error::DecodeJSON); -impl_from_error!(string::FromUtf8Error, Error::Utf8); - -impl error::Error for Error { - fn description(&self) -> &str { - match *self { - Error::EncodeJSON(ref err) => err.description(), - Error::DecodeBase64(ref err) => err.description(), - Error::DecodeJSON(ref err) => err.description(), - Error::Utf8(ref err) => err.description(), - Error::InvalidToken => "Invalid Token", - Error::InvalidSignature => "Invalid Signature", - Error::WrongAlgorithmHeader => "Wrong Algorithm Header", + InvalidSignature { + description("invalid signature") + display("Invalid signature") + } + WrongAlgorithmHeader { + description("Wrong Algorithm Header") + display("Wrong Algorithm Header") } } - fn cause(&self) -> Option<&error::Error> { - Some(match *self { - Error::EncodeJSON(ref err) => err as &error::Error, - Error::DecodeBase64(ref err) => err as &error::Error, - Error::DecodeJSON(ref err) => err as &error::Error, - Error::Utf8(ref err) => err as &error::Error, - ref e => e as &error::Error, - }) - } -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - Error::EncodeJSON(ref err) => fmt::Display::fmt(err, f), - Error::DecodeBase64(ref err) => fmt::Display::fmt(err, f), - Error::DecodeJSON(ref err) => fmt::Display::fmt(err, f), - Error::Utf8(ref err) => fmt::Display::fmt(err, f), - Error::InvalidToken => write!(f, "{}", error::Error::description(self)), - Error::InvalidSignature => write!(f, "{}", error::Error::description(self)), - Error::WrongAlgorithmHeader => write!(f, "{}", error::Error::description(self)), - } + foreign_links { + Base64(base64::Base64Error); + Json(serde_json::Error); + Utf8(::std::string::FromUtf8Error); } } diff --git a/src/header.rs b/src/header.rs new file mode 100644 index 0000000..16084c6 --- /dev/null +++ b/src/header.rs @@ -0,0 +1,33 @@ +use crypto::Algorithm; + + +/// A basic JWT header, the alg defaults to HS256 and typ is automatically +/// set to `JWT`. All the other fields are optional +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct Header { + 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".to_string(), + alg: algorithm, + jku: None, + kid: None, + x5u: None, + x5t: None + } + } +} + +impl Default for Header { + fn default() -> Header { + Header::new(Algorithm::HS256) + } +} diff --git a/src/lib.rs b/src/lib.rs index 52283aa..c22e51d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,292 +1,19 @@ //! 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; +#[macro_use] +extern crate error_chain; +#[macro_use] +extern crate serde_derive; +extern crate serde_json; +extern crate serde; +extern crate base64; extern crate ring; -use ring::{digest, hmac}; -use ring::constant_time::verify_slices_are_equal; - -use rustc_serialize::{json, Encodable, Decodable}; -use rustc_serialize::base64::{self, ToBase64, FromBase64}; -use rustc_serialize::json::{ToJson, Json}; - pub mod errors; -use errors::Error; -use std::collections::BTreeMap; +mod header; +mod crypto; -#[derive(Debug, PartialEq, Copy, Clone, RustcDecodable, RustcEncodable)] -/// The algorithms supported for signing/verifying -pub enum Algorithm { - HS256, - HS384, - HS512 -} +pub use header::{Header}; +pub use crypto::{Algorithm, sign, verify, encode, decode}; -impl ToJson for Algorithm { - fn to_json(&self) -> Json { - match *self { - Algorithm::HS256 => Json::String("HS256".to_string()), - Algorithm::HS384 => Json::String("HS384".to_string()), - Algorithm::HS512 => Json::String("HS512".to_string()), - } - } -} - -/// 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)] -/// 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, - 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".to_string(), - alg: algorithm, - jku: None, - kid: None, - x5u: None, - x5t: None - } - } -} - -impl Default for Header { - fn default() -> Header { - Header::new(Algorithm::HS256) - } -} - -impl Encodable for Header { - fn encode(&self, s: &mut S) -> Result<(), S::Error> { - self.to_json().encode(s) - } -} - -impl ToJson for Header { - fn to_json(&self) -> Json { - let mut d = BTreeMap::new(); - d.insert("typ".to_string(), self.typ.to_json()); - d.insert("alg".to_string(), self.alg.to_json()); - - // Define a macro to reduce boilerplate. - macro_rules! optional { - ($field_name:ident) => ( - if let Some(ref value) = self.$field_name { - d.insert(stringify!($field_name).to_string(), value.to_json()); - } - ) - } - optional!(jku); - optional!(kid); - optional!(x5u); - optional!(x5t); - Json::Object(d) - } -} - -#[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 -pub fn sign(data: &str, secret: &[u8], algorithm: Algorithm) -> String { - let digest = match algorithm { - Algorithm::HS256 => &digest::SHA256, - Algorithm::HS384 => &digest::SHA384, - Algorithm::HS512 => &digest::SHA512, - }; - let key = hmac::SigningKey::new(digest, secret); - hmac::sign(&key, data.as_bytes()).as_ref().to_base64(base64::URL_SAFE) -} - -/// Compares the signature given with a re-computed signature -pub fn verify(signature: &str, data: &str, secret: &[u8], algorithm: Algorithm) -> bool { - verify_slices_are_equal(signature.as_ref(), sign(data, secret, algorithm).as_ref()).is_ok() -} - -/// 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.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_string(), - company: "ACME".to_string() - }; - let mut header = Header::default(); - header.kid = Some("kid".to_string()); - 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_string(), - company: "ACME".to_string() - }; - 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] - fn decode_token() { - let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUifQ.I1BvFoHe94AFf09O6tDbcSB8-jp8w6xZqmyHIwPeSdY"; - let claims = decode::(token, "secret".as_ref(), Algorithm::HS256); - claims.unwrap(); - } - - #[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] - #[should_panic(expected = "WrongAlgorithmHeader")] - fn decode_token_wrong_algorithm() { - let token = "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUifQ.pKscJVk7-aHxfmQKlaZxh5uhuKhGMAa-1F5IX5mfUwI"; - 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()); - } -} diff --git a/tests/lib.rs b/tests/lib.rs new file mode 100644 index 0000000..24ebfea --- /dev/null +++ b/tests/lib.rs @@ -0,0 +1,97 @@ +extern crate jsonwebtoken; +#[macro_use] +extern crate serde_derive; + +use jsonwebtoken::{encode, decode, Algorithm, Header, sign, verify}; + + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +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() { + let my_claims = Claims { + sub: "b@b.com".to_string(), + company: "ACME".to_string() + }; + let mut header = Header::default(); + header.kid = Some("kid".to_string()); + 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_string(), + company: "ACME".to_string() + }; + 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] +fn decode_token() { + let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUifQ.I1BvFoHe94AFf09O6tDbcSB8-jp8w6xZqmyHIwPeSdY"; + let claims = decode::(token, "secret".as_ref(), Algorithm::HS256); + claims.unwrap(); +} + +#[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] +#[should_panic(expected = "WrongAlgorithmHeader")] +fn decode_token_wrong_algorithm() { + let token = "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUifQ.pKscJVk7-aHxfmQKlaZxh5uhuKhGMAa-1F5IX5mfUwI"; + 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()); +} From f7d0a7a002633ae0170d3b7221a33f7760d9156b Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Wed, 22 Feb 2017 16:45:28 +0900 Subject: [PATCH 02/13] Work on RSA --- Cargo.toml | 3 +- src/crypto.rs | 166 ++++++++++++++++++++++++++------------ src/errors.rs | 38 ++++++++- src/lib.rs | 13 ++- src/serialization.rs | 30 +++++++ tests/lib.rs | 4 +- tests/notes.md | 19 +++++ tests/private_rsa_key.der | Bin 0 -> 1192 bytes tests/private_rsa_key.pem | 27 +++++++ tests/public_rsa_key.der | Bin 0 -> 294 bytes tests/public_rsa_key.pem | 9 +++ tests/rsa.rs | 32 ++++++++ 12 files changed, 283 insertions(+), 58 deletions(-) create mode 100644 src/serialization.rs create mode 100644 tests/notes.md create mode 100644 tests/private_rsa_key.der create mode 100644 tests/private_rsa_key.pem create mode 100644 tests/public_rsa_key.der create mode 100644 tests/public_rsa_key.pem create mode 100644 tests/rsa.rs diff --git a/Cargo.toml b/Cargo.toml index aa04685..0f5e7af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,9 +11,10 @@ keywords = ["jwt", "web", "api", "token", "json"] [dependencies] rustc-serialize = "^0.3" -ring = "^0.7" error-chain = "0.9" serde_json = "0.9" serde_derive = "0.9" serde = "0.9" +ring = { version = "0.7", features = ["rsa_signing", "dev_urandom_fallback"] } base64 = "0.4" +untrusted = "0.3" diff --git a/src/crypto.rs b/src/crypto.rs index 3063b17..ed91463 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -1,13 +1,16 @@ +use std::sync::Arc; + use base64; -use ring::{digest, hmac}; +use ring::{rand, digest, hmac, signature}; use ring::constant_time::verify_slices_are_equal; use serde::de::Deserialize; use serde::ser::Serialize; -use serde_json; +use untrusted; use errors::{Result, ErrorKind}; use header::Header; +use serialization::{from_jwt_part, to_jwt_part, TokenData}; /// The algorithms supported for signing/verifying @@ -15,59 +18,100 @@ use header::Header; pub enum Algorithm { HS256, HS384, - HS512 + HS512, + + RS256, + RS384, + RS512, } -/// Serializes and encodes to base64 -fn to_jwt_part(input: &T) -> Result { - let encoded = serde_json::to_string(input)?; - Ok(base64::encode_config(encoded.as_bytes(), base64::URL_SAFE_NO_PAD)) -} - -/// Decodes from base64 and deserializes -fn from_jwt_part, T: Deserialize>(encoded: B) -> Result { - let decoded = base64::decode_config(encoded.as_ref(), base64::URL_SAFE_NO_PAD)?; - let s = String::from_utf8(decoded)?; - - Ok(serde_json::from_str(&s)?) -} - - -/// The return type of a successful call to decode(...) -#[derive(Debug)] -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 -pub fn sign(data: &str, secret: &[u8], algorithm: Algorithm) -> String { - let digest = match algorithm { - Algorithm::HS256 => &digest::SHA256, - Algorithm::HS384 => &digest::SHA384, - Algorithm::HS512 => &digest::SHA512, - }; - let key = hmac::SigningKey::new(digest, secret); - base64::encode_config( - hmac::sign(&key, data.as_bytes()).as_ref(), - base64::URL_SAFE_NO_PAD - ) +/// Returns the base64 url safe encoded of the result +pub fn sign(signing_input: &str, key: &[u8], algorithm: Algorithm) -> Result { + match algorithm { + Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => { + let digest = match algorithm { + Algorithm::HS256 => &digest::SHA256, + Algorithm::HS384 => &digest::SHA384, + Algorithm::HS512 => &digest::SHA512, + _ => unreachable!(), + }; + let key = hmac::SigningKey::new(digest, key); + Ok(base64::encode_config( + hmac::sign(&key, signing_input.as_bytes()).as_ref(), + base64::URL_SAFE_NO_PAD + )) + }, + Algorithm::RS256 | Algorithm::RS384 | Algorithm::RS512 => { + let ring_alg = match algorithm { + Algorithm::RS256 => &signature::RSA_PKCS1_SHA256, + Algorithm::RS384 => &signature::RSA_PKCS1_SHA384, + Algorithm::RS512 => &signature::RSA_PKCS1_SHA512, + _ => unreachable!(), + }; + // Taken from Ring doc https://briansmith.org/rustdoc/ring/signature/index.html + let key_pair = Arc::new( + signature::RSAKeyPair::from_der( + untrusted::Input::from(key) + ).map_err(|_| ErrorKind::InvalidKey)? + ); + let mut signing_state = signature::RSASigningState::new(key_pair) + .map_err(|_| ErrorKind::InvalidKey)?; + let mut signature = vec![0; signing_state.key_pair().public_modulus_len()]; + let rng = rand::SystemRandom::new(); + signing_state.sign(ring_alg, &rng, signing_input.as_bytes(), &mut signature) + .map_err(|_| ErrorKind::InvalidKey)?; + + Ok(base64::encode_config( + signature.as_ref(), + base64::URL_SAFE_NO_PAD + )) + }, + } } -/// Compares the signature given with a re-computed signature -pub fn verify(signature: &str, data: &str, secret: &[u8], algorithm: Algorithm) -> bool { - verify_slices_are_equal(signature.as_ref(), sign(data, secret, algorithm).as_ref()).is_ok() -} - -/// 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 { +/// Encode the claims passed and sign the payload using the algorithm from the header and the key +pub fn encode(header: Header, claims: &T, key: &[u8]) -> Result { let encoded_header = to_jwt_part(&header)?; let encoded_claims = to_jwt_part(&claims)?; - let payload = [encoded_header.as_ref(), encoded_claims.as_ref()].join("."); - let signature = sign(&*payload, secret.as_ref(), header.alg); + let signing_input = [encoded_header.as_ref(), encoded_claims.as_ref()].join("."); + let signature = sign(&*signing_input, key.as_ref(), header.alg)?; - Ok([payload, signature].join(".")) + Ok([signing_input, signature].join(".")) +} + +/// Compares the signature given with a re-computed signature for HMAC or using the public key (`key`) +/// for RSA +/// +/// `signature` is the signature part of a jwt (text after the second '.') +/// `signing_input` is base64(header) + "." + base64(claims) +pub fn verify(signature: &str, signing_input: &str, key: &[u8], algorithm: Algorithm) -> Result { + match algorithm { + Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => { + // we just re-sign the data with the key and compare if they are equal + let signed = sign(signing_input, key, algorithm)?; + Ok(verify_slices_are_equal(signature.as_ref(), signed.as_ref()).is_ok()) + }, + Algorithm::RS256 | Algorithm::RS384 | Algorithm::RS512 => { + // we use ring to verify using the public key given + let verification_alg = match algorithm { + Algorithm::RS256 => &signature::RSA_PKCS1_2048_8192_SHA256, + Algorithm::RS384 => &signature::RSA_PKCS1_2048_8192_SHA384, + Algorithm::RS512 => &signature::RSA_PKCS1_2048_8192_SHA512, + _ => unreachable!(), + }; + let signature_bytes = base64::decode_config(signature, base64::URL_SAFE_NO_PAD)?; + let res = signature::verify( + verification_alg, + untrusted::Input::from(key), + untrusted::Input::from(signing_input.as_bytes()), + untrusted::Input::from(signature_bytes.as_slice()), + ); + + Ok(res.is_ok()) + }, + } } /// Used in decode: takes the result of a rsplit and ensure we only get 2 parts @@ -82,16 +126,17 @@ macro_rules! expect_two { }} } -/// 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> { - let (signature, payload) = expect_two!(token.rsplitn(2, '.')); +/// Decode fn used internally by `decode` and `decode_without_verifying` +fn internal_decode(token: &str, key: &[u8], algorithm: Algorithm, do_verification: bool) -> Result> { + let (signature, signing_input) = expect_two!(token.rsplitn(2, '.')); - if !verify(signature, payload, secret, algorithm) { - return Err(ErrorKind::InvalidSignature.into()); + if do_verification { + if !verify(signature, signing_input, key, algorithm)? { + return Err(ErrorKind::InvalidSignature.into()); + } } - let (claims, header) = expect_two!(payload.rsplitn(2, '.')); + let (claims, header) = expect_two!(signing_input.rsplitn(2, '.')); let header: Header = from_jwt_part(header)?; if header.alg != algorithm { @@ -101,3 +146,18 @@ pub fn decode(token: &str, secret: &[u8], algorithm: Algorithm) Ok(TokenData { header: header, claims: decoded_claims }) } + +/// Decode a token into a struct containing Claims and Header +/// +/// If the token or its signature is invalid, it will return an error +pub fn decode(token: &str, key: &[u8], algorithm: Algorithm) -> Result> { + internal_decode(token, key, algorithm, true) +} + +/// Decode a token into a struct containing Claims and Header +/// WARNING: this will not do any verification so only use that at your own risk +/// +/// If the token is invalid, it will return an error +pub fn decode_without_verification(token: &str, key: &[u8], algorithm: Algorithm) -> Result> { + internal_decode(token, key, algorithm, false) +} diff --git a/src/errors.rs b/src/errors.rs index 5215c54..f7e27e4 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,23 +1,59 @@ use base64; use serde_json; +use ring; error_chain! { errors { + /// When a token doesn't have a valid token shape InvalidToken { description("invalid token") display("Invalid token") } + /// When the signature doesn't match InvalidSignature { description("invalid signature") display("Invalid signature") } + /// When the algorithm in the header doesn't match the one passed to `decode` WrongAlgorithmHeader { - description("Wrong Algorithm Header") + description("wrong algorithm header") display("Wrong Algorithm Header") } + /// When the secret given is not a valid RSA key + InvalidKey { + description("invalid key") + display("Invalid Key") + } + + /// When a token’s `exp` claim indicates that it has expired + ExpiredSignature { + description("expired signature") + display("Expired Signature") + } + /// When a token’s `iss` claim does not match the expected issuer + InvalidIssuer { + description("invalid issuer") + display("Invalid Issuer") + } + /// When a token’s `aud` claim does not match one of the expected audience values + InvalidAudience { + description("invalid audience") + display("Invalid Audience") + } + /// When a token’s `iat` claim is in the future + InvalidIssuedAt { + description("invalid issued at") + display("Invalid Issued At") + } + /// When a token’s nbf claim represents a time in the future + ImmatureSignature { + description("immature signature") + display("Immature Signature") + } } foreign_links { + Unspecified(ring::error::Unspecified); Base64(base64::Base64Error); Json(serde_json::Error); Utf8(::std::string::FromUtf8Error); diff --git a/src/lib.rs b/src/lib.rs index c22e51d..bbbc1cb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,7 @@ //! Create and parses JWT (JSON Web Tokens) //! +#![recursion_limit = "300"] + #[macro_use] extern crate error_chain; @@ -9,11 +11,20 @@ extern crate serde_json; extern crate serde; extern crate base64; extern crate ring; +extern crate untrusted; pub mod errors; mod header; mod crypto; +mod serialization; pub use header::{Header}; -pub use crypto::{Algorithm, sign, verify, encode, decode}; +pub use crypto::{ + Algorithm, + sign, + verify, + encode, + decode, + decode_without_verification, +}; diff --git a/src/serialization.rs b/src/serialization.rs new file mode 100644 index 0000000..fb22d23 --- /dev/null +++ b/src/serialization.rs @@ -0,0 +1,30 @@ +use base64; +use serde::de::Deserialize; +use serde::ser::Serialize; +use serde_json; + + +use errors::{Result}; +use header::Header; + + +/// The return type of a successful call to decode(...) +#[derive(Debug)] +pub struct TokenData { + pub header: Header, + pub claims: T +} + +/// Serializes and encodes to base64 +pub fn to_jwt_part(input: &T) -> Result { + let encoded = serde_json::to_string(input)?; + Ok(base64::encode_config(encoded.as_bytes(), base64::URL_SAFE_NO_PAD)) +} + +/// Decodes from base64 and deserializes +pub fn from_jwt_part, T: Deserialize>(encoded: B) -> Result { + let decoded = base64::decode_config(encoded.as_ref(), base64::URL_SAFE_NO_PAD)?; + let s = String::from_utf8(decoded)?; + + Ok(serde_json::from_str(&s)?) +} diff --git a/tests/lib.rs b/tests/lib.rs index 24ebfea..091ee46 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -13,7 +13,7 @@ struct Claims { #[test] fn sign_hs256() { - let result = sign("hello world", b"secret", Algorithm::HS256); + let result = sign("hello world", b"secret", Algorithm::HS256).unwrap(); let expected = "c0zGLzKEFWj0VxWuufTXiRMk5tlI5MbGDAYhzaxIYjo"; assert_eq!(result, expected); } @@ -21,7 +21,7 @@ fn sign_hs256() { #[test] fn verify_hs256() { let sig = "c0zGLzKEFWj0VxWuufTXiRMk5tlI5MbGDAYhzaxIYjo"; - let valid = verify(sig, "hello world", b"secret", Algorithm::HS256); + let valid = verify(sig, "hello world", b"secret", Algorithm::HS256).unwrap(); assert!(valid); } diff --git a/tests/notes.md b/tests/notes.md new file mode 100644 index 0000000..066c22b --- /dev/null +++ b/tests/notes.md @@ -0,0 +1,19 @@ +# Generating RSA keys + +Using `openssl` + +## PEM +`openssl genrsa -out private_rsa_key.pem 2048` + +Getting public key: +`openssl rsa -in private_rsa_key.pem -outform PEM -pubout -out public_rsa_key.pem` + +## DER +Same as PEM but replace `PEM` by `DER`. +`openssl rsa -in private_rsa_key.pem -outform DER -pubout -out public_rsa_key.der` + +## Converting private PEM to DER +`openssl rsa -in private_rsa_key.pem -outform DER -out private_rsa_key.der` + +## Converting private DER to PEM +`openssl rsa -in private_rsa_key.der -inform DER -outform PEM -out private_rsa_key.pem` diff --git a/tests/private_rsa_key.der b/tests/private_rsa_key.der new file mode 100644 index 0000000000000000000000000000000000000000..09ee0a812f79573a0e5e42d24d6eb327a493323f GIT binary patch literal 1192 zcmV;Z1Xueof&`=j0RRGm0RaHX5jw1UjYmWq9PP*4t9GQ~hE^0R8I{PK=WamUSI*+4 zy#&iqJNTE2Ac7u$35Fv5%gFg9)lbGvy^bX?_M?%D%g%kcy-F0ikw?DNGbGmI2;l}HOG2%e} zQGxtMC7~kicOZr8!U*Uttdx>%f%f!l{JWRMZ7-OE%RV|Fth0A4*0i7}-`*XYvlvHI zWl#|xs8upFuEP42Ojd;O2rZT20)c@5`r^!=i4CZENlRckxCz3NKXhjiEjDe$@(?jn zxNNdZSmkt%0PtnUC;3L?Mj(lXwM9s;f$k+HxlMjNpLJcg(-;bx&i*DP>S_%|Bkks@ zFd&18n26ZgYL5-@?f*PJeZa%sS%N09WzEUq(&w7gDj~#1(44*8%9o3`<(pHf0)c@5 z%_?>FZCZ{eg_5W!2SyX7-_EXp@!!kuBb%opa%Bqd&qQ=|0m zVN`D2WB4jM3A19u<@_DMnyG@(<;hzlP^rItXX+y9jTeQg74I5p1!$>%{|B&CwuT(B^SHIk! zynRr?Wa!B%Rx0;(frO}ue>nYMt&Nkb(d`GAvK!L;G=`=?;>^iPsxd+f#oJ*YA!dka z0)c@5pWly;n8q;i@h?at@edVmI|a6jx^GurArwH^A?8Y3Wwjk(VN}e)d{B8vPcmpE zGNl*J&Ocfx;HKDE0ondAf&n5h4F(A+hDe6@4FLfG1potr0S^E$f&mHwf&l>l$q_oNdyPj#8yxM& z+^cq^;)YffDjAi?oab&p+*i)xrM(2pQ9JmTiy(p?e+h;n{maPtCDl*a4_P9Y#FfEa z-}o*|+&<+RkQxAy_5OFPbIGTpd6dxx6SC-xJF>FPv(N?3AJ3qwvdbB0n|R s*%ILcB1KbwZ!MwSmOL$mMy{q;Wj7$7Yonjbij8vFR6bL60s{d60ox;f+yDRo literal 0 HcmV?d00001 diff --git a/tests/public_rsa_key.pem b/tests/public_rsa_key.pem new file mode 100644 index 0000000..5a7ffd9 --- /dev/null +++ b/tests/public_rsa_key.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyRE6rHuNR0QbHO3H3Kt2 +pOKGVhQqGZXInOduQNxXzuKlvQTLUTv4l4sggh5/CYYi/cvI+SXVT9kPWSKXxJXB +Xd/4LkvcPuUakBoAkfh+eiFVMh2VrUyWyj3MFl0HTVF9KwRXLAcwkREiS3npThHR +yIxuy0ZMeZfxVL5arMhw1SRELB8HoGfG/AtH89BIE9jDBHZ9dLelK9a184zAf8Lw +oPLxvJb3Il5nncqPcSfKDDodMFBIMc4lQzDKL5gvmiXLXB1AGLm8KBjfE8s3L5xq +i+yUod+j8MtvIj812dkS4QMiRVN/by2h3ZY8LYVGrqZXZTcgn2ujn8uKjXLZVD5T +dQIDAQAB +-----END PUBLIC KEY----- diff --git a/tests/rsa.rs b/tests/rsa.rs new file mode 100644 index 0000000..b30ca59 --- /dev/null +++ b/tests/rsa.rs @@ -0,0 +1,32 @@ +extern crate jsonwebtoken; +#[macro_use] +extern crate serde_derive; + +use jsonwebtoken::{encode, decode, Algorithm, Header, sign, verify}; + + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +struct Claims { + sub: String, + company: String +} + +//#[test] +//fn round_trip_sign_verification() { +// let encrypted = sign("hello world", include_bytes!("private_rsa_key.der"), Algorithm::RS256).unwrap(); +// let is_valid = verify(&encrypted, "hello world", include_bytes!("public_rsa_key.der"), Algorithm::RS256).unwrap(); +// assert!(is_valid); +//} + + +#[test] +fn round_trip_claim() { + let my_claims = Claims { + sub: "b@b.com".to_string(), + company: "ACME".to_string() + }; + let token = encode(Header::new(Algorithm::RS256), &my_claims, include_bytes!("private_rsa_key.der")).unwrap(); + let token_data = decode::(&token, include_bytes!("public_rsa_key.der"), Algorithm::RS256).unwrap(); + assert_eq!(my_claims, token_data.claims); + assert!(token_data.header.kid.is_none()); +} From 67497950c6f8097b0c5070142c5072b5a34a8427 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Tue, 11 Apr 2017 12:40:01 +0900 Subject: [PATCH 03/13] RSA working --- CHANGELOG.md | 19 +++++++++++++++++++ README.md | 12 ------------ src/crypto.rs | 11 ++++++++--- src/header.rs | 8 ++++++-- tests/notes.md | 3 +++ tests/public_rsa_key.der | Bin 294 -> 270 bytes tests/rsa.rs | 12 ++++++------ 7 files changed, 42 insertions(+), 23 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..58237a6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,19 @@ +# Changelog + +## 2.0.0 (unreleased) + +- Use Serde instead of rustc_serialize +- Add RSA support +- Change API, see README for new usage + +## Previous + +- 1.1.7: update ring +- 1.1.6: update ring +- 1.1.5: update ring version +- 1.1.4: use ring instead of rust-crypto +- 1.1.3: Make sign and verify public +- 1.1.2: Update rust-crypto to 0.2.35 +- 1.1.1: Don't serialize empty fields in header +- 1.1.0: Impl Error for jsonwebtoken errors +- 1.0: Initial release diff --git a/README.md b/README.md index f499ef2..4a7757f 100644 --- a/README.md +++ b/README.md @@ -69,15 +69,3 @@ On my thinkpad 440s for a 2 claims struct using HMAC SHA256: test bench_decode ... bench: 4,947 ns/iter (+/- 611) test bench_encode ... bench: 3,301 ns/iter (+/- 465) ``` - -## Changelog - -- 1.1.7: update ring -- 1.1.6: update ring -- 1.1.5: update ring version -- 1.1.4: use ring instead of rust-crypto -- 1.1.3: Make sign and verify public -- 1.1.2: Update rust-crypto to 0.2.35 -- 1.1.1: Don't serialize empty fields in header -- 1.1.0: Impl Error for jsonwebtoken errors -- 1.0: Initial release diff --git a/src/crypto.rs b/src/crypto.rs index ed91463..7161c69 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -102,12 +102,17 @@ pub fn verify(signature: &str, signing_input: &str, key: &[u8], algorithm: Algor _ => unreachable!(), }; let signature_bytes = base64::decode_config(signature, base64::URL_SAFE_NO_PAD)?; + let public_key_der = untrusted::Input::from(key); + let message = untrusted::Input::from(signing_input.as_bytes()); + let expected_signature = untrusted::Input::from(signature_bytes.as_slice()); + let res = signature::verify( verification_alg, - untrusted::Input::from(key), - untrusted::Input::from(signing_input.as_bytes()), - untrusted::Input::from(signature_bytes.as_slice()), + public_key_der, + message, + expected_signature, ); + println!("{:?}", res); Ok(res.is_ok()) }, diff --git a/src/header.rs b/src/header.rs index 16084c6..0bd7703 100644 --- a/src/header.rs +++ b/src/header.rs @@ -3,14 +3,18 @@ use crypto::Algorithm; /// A basic JWT header, the alg defaults to HS256 and typ is automatically /// set to `JWT`. All the other fields are optional -#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Header { typ: String, pub alg: Algorithm, + #[serde(skip_serializing_if = "Option::is_none")] pub jku: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub kid: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub x5u: Option, - pub x5t: Option + #[serde(skip_serializing_if = "Option::is_none")] + pub x5t: Option, } impl Header { diff --git a/tests/notes.md b/tests/notes.md index 066c22b..41680ba 100644 --- a/tests/notes.md +++ b/tests/notes.md @@ -17,3 +17,6 @@ Same as PEM but replace `PEM` by `DER`. ## Converting private DER to PEM `openssl rsa -in private_rsa_key.der -inform DER -outform PEM -out private_rsa_key.pem` + +## Generating public key +`openssl rsa -in private_rsa_key.der -inform DER -RSAPublicKey_out -outform DER -out public_key.der` diff --git a/tests/public_rsa_key.der b/tests/public_rsa_key.der index 242e9c6adf3c5fe3b7aa14783574e67fd5233742..f548e15a9e76e96e999c22af68e92150eeef63d8 100644 GIT binary patch delta 8 PcmZ3+)W@_@f{zgZ3(W!% delta 32 ncmeBUTE-+{(8Q=@z{|#|)#lOmotKf3k(GhDiIJaS10N#*bMyvK diff --git a/tests/rsa.rs b/tests/rsa.rs index b30ca59..ce8fa07 100644 --- a/tests/rsa.rs +++ b/tests/rsa.rs @@ -11,12 +11,12 @@ struct Claims { company: String } -//#[test] -//fn round_trip_sign_verification() { -// let encrypted = sign("hello world", include_bytes!("private_rsa_key.der"), Algorithm::RS256).unwrap(); -// let is_valid = verify(&encrypted, "hello world", include_bytes!("public_rsa_key.der"), Algorithm::RS256).unwrap(); -// assert!(is_valid); -//} +#[test] +fn round_trip_sign_verification() { + let encrypted = sign("hello world", include_bytes!("private_rsa_key.der"), Algorithm::RS256).unwrap(); + let is_valid = verify(&encrypted, "hello world", include_bytes!("public_rsa_key.der"), Algorithm::RS256).unwrap(); + assert!(is_valid); +} #[test] From c244e835e0703fc3270fb668c4ca5079539be3e7 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Tue, 11 Apr 2017 12:54:32 +0900 Subject: [PATCH 04/13] Take ref to Header, not by value --- examples/claims.rs | 2 +- examples/custom_header.rs | 2 +- src/crypto.rs | 2 +- src/serialization.rs | 4 ++-- tests/lib.rs | 4 ++-- tests/rsa.rs | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/claims.rs b/examples/claims.rs index 850b828..d24c056 100644 --- a/examples/claims.rs +++ b/examples/claims.rs @@ -29,7 +29,7 @@ fn main() { company: "ACME".to_owned() }; let key = "secret"; - let token = match encode(Header::default(), &my_claims, key.as_ref()) { + let token = match encode(&Header::default(), &my_claims, key.as_ref()) { Ok(t) => t, Err(_) => panic!() // in practice you would return the error }; diff --git a/examples/custom_header.rs b/examples/custom_header.rs index b09def8..c93bccc 100644 --- a/examples/custom_header.rs +++ b/examples/custom_header.rs @@ -23,7 +23,7 @@ fn main() { header.kid = Some("signing_key".to_owned()); header.alg = Algorithm::HS512; - let token = match encode(header, &my_claims, key.as_ref()) { + let token = match encode(&header, &my_claims, key.as_ref()) { Ok(t) => t, Err(_) => panic!() // in practice you would return the error }; diff --git a/src/crypto.rs b/src/crypto.rs index 7161c69..f93e653 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -72,7 +72,7 @@ pub fn sign(signing_input: &str, key: &[u8], algorithm: Algorithm) -> Result(header: Header, claims: &T, key: &[u8]) -> Result { +pub fn encode(header: &Header, claims: &T, key: &[u8]) -> Result { let encoded_header = to_jwt_part(&header)?; let encoded_claims = to_jwt_part(&claims)?; let signing_input = [encoded_header.as_ref(), encoded_claims.as_ref()].join("."); diff --git a/src/serialization.rs b/src/serialization.rs index fb22d23..be4d736 100644 --- a/src/serialization.rs +++ b/src/serialization.rs @@ -15,13 +15,13 @@ pub struct TokenData { pub claims: T } -/// Serializes and encodes to base64 +/// Serializes to JSON and encodes to base64 pub fn to_jwt_part(input: &T) -> Result { let encoded = serde_json::to_string(input)?; Ok(base64::encode_config(encoded.as_bytes(), base64::URL_SAFE_NO_PAD)) } -/// Decodes from base64 and deserializes +/// Decodes from base64 and deserializes from JSON pub fn from_jwt_part, T: Deserialize>(encoded: B) -> Result { let decoded = base64::decode_config(encoded.as_ref(), base64::URL_SAFE_NO_PAD)?; let s = String::from_utf8(decoded)?; diff --git a/tests/lib.rs b/tests/lib.rs index 091ee46..abca7f1 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -33,7 +33,7 @@ fn encode_with_custom_header() { }; let mut header = Header::default(); header.kid = Some("kid".to_string()); - let token = encode(header, &my_claims, "secret".as_ref()).unwrap(); + 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()); @@ -45,7 +45,7 @@ fn round_trip_claim() { sub: "b@b.com".to_string(), company: "ACME".to_string() }; - let token = encode(Header::default(), &my_claims, "secret".as_ref()).unwrap(); + 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()); diff --git a/tests/rsa.rs b/tests/rsa.rs index ce8fa07..6d678ce 100644 --- a/tests/rsa.rs +++ b/tests/rsa.rs @@ -25,7 +25,7 @@ fn round_trip_claim() { sub: "b@b.com".to_string(), company: "ACME".to_string() }; - let token = encode(Header::new(Algorithm::RS256), &my_claims, include_bytes!("private_rsa_key.der")).unwrap(); + let token = encode(&Header::new(Algorithm::RS256), &my_claims, include_bytes!("private_rsa_key.der")).unwrap(); let token_data = decode::(&token, include_bytes!("public_rsa_key.der"), Algorithm::RS256).unwrap(); assert_eq!(my_claims, token_data.claims); assert!(token_data.header.kid.is_none()); From bdeefe5ed72cf252eb32aa483d0fcc4948d8dfd7 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Tue, 11 Apr 2017 12:58:50 +0900 Subject: [PATCH 05/13] Fix bench and docs --- Cargo.toml | 2 +- README.md | 15 ++++----------- benches/jwt.rs | 2 +- 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0f5e7af..77b6376 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jsonwebtoken" -version = "1.1.7" +version = "2.0.0" authors = ["Vincent Prouillet "] license = "MIT" readme = "README.md" diff --git a/README.md b/README.md index 4a7757f..93da2a6 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ header. ### Encoding ```rust -let token = encode(Header::default(), &my_claims, "secret".as_ref()).unwrap(); +let token = encode(&Header::default(), &my_claims, "secret".as_ref()).unwrap(); ``` In that example, `my_claims` is an instance of a Claims struct that derives `RustcEncodable` and `RustcDecodable`. The default algorithm is HS256. @@ -55,17 +55,10 @@ If you want to set the `kid` parameter for example: 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(); +let token = encode(&header, &my_claims, "secret".as_ref()).unwrap(); ``` Look at `examples/custom_header.rs` for a full working example. ## Algorithms -Right now, only HMAC SHA family is supported: HMAC SHA256, HMAC SHA384 and HMAC SHA512. - -## Performance -On my thinkpad 440s for a 2 claims struct using HMAC SHA256: - -``` -test bench_decode ... bench: 4,947 ns/iter (+/- 611) -test bench_encode ... bench: 3,301 ns/iter (+/- 465) -``` +The HMAC SHA family is supported: HMAC SHA256, HMAC SHA384 and HMAC SHA512 as well as the RSA PKCS1: RSA_PKCS1_SHA256, +RSA_PKCS1_SHA384 and RSA_PKCS1_SHA512. diff --git a/benches/jwt.rs b/benches/jwt.rs index 3f8c692..0744efe 100644 --- a/benches/jwt.rs +++ b/benches/jwt.rs @@ -19,7 +19,7 @@ fn bench_encode(b: &mut test::Bencher) { company: "ACME".to_owned() }; - b.iter(|| encode(Header::default(), &claim, "secret".as_ref())); + b.iter(|| encode(&Header::default(), &claim, "secret".as_ref())); } #[bench] From 410499e6b6c6559039d62229d1827798e53e2bf6 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Tue, 11 Apr 2017 14:41:44 +0900 Subject: [PATCH 06/13] Add validation --- Cargo.toml | 3 +- benches/jwt.rs | 4 +- examples/claims.rs | 4 +- examples/custom_header.rs | 4 +- src/crypto.rs | 35 ++--- src/errors.rs | 7 + src/lib.rs | 5 +- src/serialization.rs | 20 ++- src/validation.rs | 313 ++++++++++++++++++++++++++++++++++++++ tests/lib.rs | 18 +-- tests/rsa.rs | 4 +- 11 files changed, 368 insertions(+), 49 deletions(-) create mode 100644 src/validation.rs diff --git a/Cargo.toml b/Cargo.toml index 77b6376..28f1517 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,10 +11,11 @@ keywords = ["jwt", "web", "api", "token", "json"] [dependencies] rustc-serialize = "^0.3" -error-chain = "0.9" +error-chain = "0.10" serde_json = "0.9" serde_derive = "0.9" serde = "0.9" ring = { version = "0.7", features = ["rsa_signing", "dev_urandom_fallback"] } base64 = "0.4" untrusted = "0.3" +chrono = "0.3" diff --git a/benches/jwt.rs b/benches/jwt.rs index 0744efe..bafe8de 100644 --- a/benches/jwt.rs +++ b/benches/jwt.rs @@ -4,7 +4,7 @@ extern crate jsonwebtoken as jwt; #[macro_use] extern crate serde_derive; -use jwt::{encode, decode, Algorithm, Header}; +use jwt::{encode, decode, Algorithm, Header, Validation}; #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] struct Claims { @@ -25,5 +25,5 @@ fn bench_encode(b: &mut test::Bencher) { #[bench] fn bench_decode(b: &mut test::Bencher) { let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"; - b.iter(|| decode::(token, "secret".as_ref(), Algorithm::HS256)); + b.iter(|| decode::(token, "secret".as_ref(), Algorithm::HS256, Validation::default())); } diff --git a/examples/claims.rs b/examples/claims.rs index d24c056..4f75626 100644 --- a/examples/claims.rs +++ b/examples/claims.rs @@ -1,7 +1,7 @@ extern crate jsonwebtoken as jwt; #[macro_use] extern crate serde_derive; -use jwt::{encode, decode, Header, Algorithm}; +use jwt::{encode, decode, Header, Algorithm, Validation}; use jwt::errors::{ErrorKind}; @@ -36,7 +36,7 @@ fn main() { println!("{:?}", token); - let token_data = match decode::(&token, key.as_ref(), Algorithm::HS256) { + let token_data = match decode::(&token, key.as_ref(), Algorithm::HS256, Validation::default()) { Ok(c) => c, Err(err) => match *err.kind() { ErrorKind::InvalidToken => panic!(), // Example on how to handle a specific error diff --git a/examples/custom_header.rs b/examples/custom_header.rs index c93bccc..62aad32 100644 --- a/examples/custom_header.rs +++ b/examples/custom_header.rs @@ -2,7 +2,7 @@ extern crate jsonwebtoken as jwt; #[macro_use] extern crate serde_derive; -use jwt::{encode, decode, Header, Algorithm}; +use jwt::{encode, decode, Header, Algorithm, Validation}; use jwt::errors::{ErrorKind}; @@ -28,7 +28,7 @@ fn main() { Err(_) => panic!() // in practice you would return the error }; - let token_data = match decode::(&token, key.as_ref(), Algorithm::HS512) { + let token_data = match decode::(&token, key.as_ref(), Algorithm::HS512, Validation::default()) { Ok(c) => c, Err(err) => match *err.kind() { ErrorKind::InvalidToken => panic!(), // Example on how to handle a specific error diff --git a/src/crypto.rs b/src/crypto.rs index f93e653..cd141ed 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -10,7 +10,8 @@ use untrusted; use errors::{Result, ErrorKind}; use header::Header; -use serialization::{from_jwt_part, to_jwt_part, TokenData}; +use serialization::{from_jwt_part, to_jwt_part, from_jwt_part_claims, TokenData}; +use validation::{Validation, validate}; /// The algorithms supported for signing/verifying @@ -112,7 +113,6 @@ pub fn verify(signature: &str, signing_input: &str, key: &[u8], algorithm: Algor message, expected_signature, ); - println!("{:?}", res); Ok(res.is_ok()) }, @@ -131,14 +131,14 @@ macro_rules! expect_two { }} } -/// Decode fn used internally by `decode` and `decode_without_verifying` -fn internal_decode(token: &str, key: &[u8], algorithm: Algorithm, do_verification: bool) -> Result> { +/// Decode a token into a struct containing Claims and Header +/// +/// If the token or its signature is invalid, it will return an error +pub fn decode(token: &str, key: &[u8], algorithm: Algorithm, validation: Validation) -> Result> { let (signature, signing_input) = expect_two!(token.rsplitn(2, '.')); - if do_verification { - if !verify(signature, signing_input, key, algorithm)? { - return Err(ErrorKind::InvalidSignature.into()); - } + if validation.validate_signature && !verify(signature, signing_input, key, algorithm)? { + return Err(ErrorKind::InvalidSignature.into()); } let (claims, header) = expect_two!(signing_input.rsplitn(2, '.')); @@ -147,22 +147,9 @@ fn internal_decode(token: &str, key: &[u8], algorithm: Algorithm if header.alg != algorithm { return Err(ErrorKind::WrongAlgorithmHeader.into()); } - let decoded_claims: T = from_jwt_part(claims)?; + let (decoded_claims, claims_map): (T, _) = from_jwt_part_claims(claims)?; + + validate(&claims_map, &validation)?; Ok(TokenData { header: header, claims: decoded_claims }) } - -/// Decode a token into a struct containing Claims and Header -/// -/// If the token or its signature is invalid, it will return an error -pub fn decode(token: &str, key: &[u8], algorithm: Algorithm) -> Result> { - internal_decode(token, key, algorithm, true) -} - -/// Decode a token into a struct containing Claims and Header -/// WARNING: this will not do any verification so only use that at your own risk -/// -/// If the token is invalid, it will return an error -pub fn decode_without_verification(token: &str, key: &[u8], algorithm: Algorithm) -> Result> { - internal_decode(token, key, algorithm, false) -} diff --git a/src/errors.rs b/src/errors.rs index f7e27e4..c243760 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -25,6 +25,8 @@ error_chain! { display("Invalid Key") } + // Validation error + /// When a token’s `exp` claim indicates that it has expired ExpiredSignature { description("expired signature") @@ -40,6 +42,11 @@ error_chain! { description("invalid audience") display("Invalid Audience") } + /// When a token’s `aud` claim does not match one of the expected audience values + InvalidSubject { + description("invalid subject") + display("Invalid Subject") + } /// When a token’s `iat` claim is in the future InvalidIssuedAt { description("invalid issued at") diff --git a/src/lib.rs b/src/lib.rs index bbbc1cb..3544903 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,11 +12,13 @@ extern crate serde; extern crate base64; extern crate ring; extern crate untrusted; +extern crate chrono; pub mod errors; mod header; mod crypto; mod serialization; +mod validation; pub use header::{Header}; pub use crypto::{ @@ -25,6 +27,5 @@ pub use crypto::{ verify, encode, decode, - decode_without_verification, }; - +pub use validation::Validation; diff --git a/src/serialization.rs b/src/serialization.rs index be4d736..41df835 100644 --- a/src/serialization.rs +++ b/src/serialization.rs @@ -1,8 +1,8 @@ use base64; use serde::de::Deserialize; use serde::ser::Serialize; -use serde_json; - +use serde_json::{from_str, to_string, Value}; +use serde_json::map::Map; use errors::{Result}; use header::Header; @@ -17,14 +17,24 @@ pub struct TokenData { /// Serializes to JSON and encodes to base64 pub fn to_jwt_part(input: &T) -> Result { - let encoded = serde_json::to_string(input)?; + let encoded = to_string(input)?; Ok(base64::encode_config(encoded.as_bytes(), base64::URL_SAFE_NO_PAD)) } -/// Decodes from base64 and deserializes from JSON +/// Decodes from base64 and deserializes from JSON to a struct pub fn from_jwt_part, T: Deserialize>(encoded: B) -> Result { let decoded = base64::decode_config(encoded.as_ref(), base64::URL_SAFE_NO_PAD)?; let s = String::from_utf8(decoded)?; - Ok(serde_json::from_str(&s)?) + Ok(from_str(&s)?) +} + +/// Decodes from base64 and deserializes from JSON to a struct AND a hashmap +pub fn from_jwt_part_claims, T: Deserialize>(encoded: B) -> Result<(T, Map)> { + let decoded = base64::decode_config(encoded.as_ref(), base64::URL_SAFE_NO_PAD)?; + let s = String::from_utf8(decoded)?; + + let claims: T = from_str(&s)?; + let map: Map<_,_> = from_str(&s)?; + Ok((claims, map)) } diff --git a/src/validation.rs b/src/validation.rs new file mode 100644 index 0000000..5a8a67c --- /dev/null +++ b/src/validation.rs @@ -0,0 +1,313 @@ +use chrono::UTC; +use serde::ser::Serialize; +use serde_json::{Value, from_value, to_value}; +use serde_json::map::Map; + +use errors::{Result, ErrorKind}; + + +#[derive(Debug, Clone, PartialEq)] +pub struct Validation { + pub leeway: i64, + pub validate_signature: bool, + pub validate_exp: bool, + pub validate_iat: bool, + pub validate_nbf: bool, + + pub aud: Option, + pub iss: Option, + pub sub: Option, +} + +impl Validation { + pub fn set_audience(&mut self, audience: &T) { + self.aud = Some(to_value(audience).unwrap()); + } +} + +impl Default for Validation { + fn default() -> Validation { + Validation { + leeway: 0, + + validate_signature: true, + + validate_exp: true, + validate_iat: true, + validate_nbf: true, + + iss: None, + sub: None, + aud: None, + } + } +} + + + +pub fn validate(claims: &Map, options: &Validation) -> Result<()> { + let now = UTC::now().timestamp(); + + if let Some(iat) = claims.get("iat") { + if options.validate_iat && from_value::(iat.clone())? > now + options.leeway { + return Err(ErrorKind::InvalidIssuedAt.into()); + } + } + + if let Some(exp) = claims.get("exp") { + if options.validate_exp && from_value::(exp.clone())? < now - options.leeway { + return Err(ErrorKind::ExpiredSignature.into()); + } + } + + if let Some(nbf) = claims.get("nbf") { + if options.validate_nbf && from_value::(nbf.clone())? > now + options.leeway { + return Err(ErrorKind::ImmatureSignature.into()); + } + } + + if let Some(iss) = claims.get("iss") { + if let Some(ref correct_iss) = options.iss { + if from_value::(iss.clone())? != *correct_iss { + return Err(ErrorKind::InvalidIssuer.into()); + } + } + } + + if let Some(sub) = claims.get("sub") { + if let Some(ref correct_sub) = options.sub { + if from_value::(sub.clone())? != *correct_sub { + return Err(ErrorKind::InvalidSubject.into()); + } + } + } + + if let Some(aud) = claims.get("aud") { + if let Some(ref correct_aud) = options.aud { + if aud != correct_aud { + return Err(ErrorKind::InvalidAudience.into()); + } + } + } + + Ok(()) +} + + +#[cfg(test)] +mod tests { + use serde_json::{to_value}; + use serde_json::map::Map; + use chrono::UTC; + + use super::{validate, Validation}; + + use errors::ErrorKind; + + #[test] + fn iat_in_past_ok() { + let mut claims = Map::new(); + claims.insert("iat".to_string(), to_value(UTC::now().timestamp() - 10000).unwrap()); + let res = validate(&claims, &Validation::default()); + assert!(res.is_ok()); + } + + #[test] + fn iat_in_future_fails() { + let mut claims = Map::new(); + claims.insert("iat".to_string(), to_value(UTC::now().timestamp() + 100000).unwrap()); + let res = validate(&claims, &Validation::default()); + assert!(res.is_err()); + + match res.unwrap_err().kind() { + &ErrorKind::InvalidIssuedAt => (), + _ => assert!(false), + }; + } + + #[test] + fn iat_in_future_but_in_leeway_ok() { + let mut claims = Map::new(); + claims.insert("iat".to_string(), to_value(UTC::now().timestamp() + 50).unwrap()); + let validation = Validation { + leeway: 1000 * 60, + ..Default::default() + }; + let res = validate(&claims, &validation); + assert!(res.is_ok()); + } + + #[test] + fn exp_in_future_ok() { + let mut claims = Map::new(); + claims.insert("exp".to_string(), to_value(UTC::now().timestamp() + 10000).unwrap()); + let res = validate(&claims, &Validation::default()); + assert!(res.is_ok()); + } + + #[test] + fn exp_in_past_fails() { + let mut claims = Map::new(); + claims.insert("exp".to_string(), to_value(UTC::now().timestamp() - 100000).unwrap()); + let res = validate(&claims, &Validation::default()); + assert!(res.is_err()); + + match res.unwrap_err().kind() { + &ErrorKind::ExpiredSignature => (), + _ => assert!(false), + }; + } + + #[test] + fn exp_in_past_but_in_leeway_ok() { + let mut claims = Map::new(); + claims.insert("exp".to_string(), to_value(UTC::now().timestamp() - 500).unwrap()); + let validation = Validation { + leeway: 1000 * 60, + ..Default::default() + }; + let res = validate(&claims, &validation); + assert!(res.is_ok()); + } + + #[test] + fn nbf_in_past_ok() { + let mut claims = Map::new(); + claims.insert("nbf".to_string(), to_value(UTC::now().timestamp() - 10000).unwrap()); + let res = validate(&claims, &Validation::default()); + assert!(res.is_ok()); + } + + #[test] + fn nbf_in_future_fails() { + let mut claims = Map::new(); + claims.insert("nbf".to_string(), to_value(UTC::now().timestamp() + 100000).unwrap()); + let res = validate(&claims, &Validation::default()); + assert!(res.is_err()); + + match res.unwrap_err().kind() { + &ErrorKind::ImmatureSignature => (), + _ => assert!(false), + }; + } + + #[test] + fn nbf_in_future_but_in_leeway_ok() { + let mut claims = Map::new(); + claims.insert("nbf".to_string(), to_value(UTC::now().timestamp() + 500).unwrap()); + let validation = Validation { + leeway: 1000 * 60, + ..Default::default() + }; + let res = validate(&claims, &validation); + assert!(res.is_ok()); + } + + #[test] + fn iss_ok() { + let mut claims = Map::new(); + claims.insert("iss".to_string(), to_value("Keats").unwrap()); + let validation = Validation { + iss: Some("Keats".to_string()), + ..Default::default() + }; + let res = validate(&claims, &validation); + assert!(res.is_ok()); + } + + #[test] + fn iss_not_matching_fails() { + let mut claims = Map::new(); + claims.insert("iss".to_string(), to_value("Hacked").unwrap()); + let validation = Validation { + iss: Some("Keats".to_string()), + ..Default::default() + }; + let res = validate(&claims, &validation); + assert!(res.is_err()); + + match res.unwrap_err().kind() { + &ErrorKind::InvalidIssuer => (), + _ => assert!(false), + }; + } + + #[test] + fn sub_ok() { + let mut claims = Map::new(); + claims.insert("sub".to_string(), to_value("Keats").unwrap()); + let validation = Validation { + sub: Some("Keats".to_string()), + ..Default::default() + }; + let res = validate(&claims, &validation); + assert!(res.is_ok()); + } + + #[test] + fn sub_not_matching_fails() { + let mut claims = Map::new(); + claims.insert("sub".to_string(), to_value("Hacked").unwrap()); + let validation = Validation { + sub: Some("Keats".to_string()), + ..Default::default() + }; + let res = validate(&claims, &validation); + assert!(res.is_err()); + + match res.unwrap_err().kind() { + &ErrorKind::InvalidSubject => (), + _ => assert!(false), + }; + } + + #[test] + fn aud_string_ok() { + let mut claims = Map::new(); + claims.insert("aud".to_string(), to_value("Everyone").unwrap()); + let mut validation = Validation::default(); + validation.set_audience(&"Everyone"); + let res = validate(&claims, &validation); + assert!(res.is_ok()); + } + + #[test] + fn aud_array_of_string_ok() { + let mut claims = Map::new(); + claims.insert("aud".to_string(), to_value(["UserA", "UserB"]).unwrap()); + let mut validation = Validation::default(); + validation.set_audience(&["UserA", "UserB"]); + let res = validate(&claims, &validation); + assert!(res.is_ok()); + } + + #[test] + fn aud_type_mismatch_fails() { + let mut claims = Map::new(); + claims.insert("aud".to_string(), to_value("Everyone").unwrap()); + let mut validation = Validation::default(); + validation.set_audience(&["UserA", "UserB"]); + let res = validate(&claims, &validation); + assert!(res.is_err()); + + match res.unwrap_err().kind() { + &ErrorKind::InvalidAudience => (), + _ => assert!(false), + }; + } + + #[test] + fn aud_correct_type_not_matching_fails() { + let mut claims = Map::new(); + claims.insert("aud".to_string(), to_value("Everyone").unwrap()); + let mut validation = Validation::default(); + validation.set_audience(&"None"); + let res = validate(&claims, &validation); + assert!(res.is_err()); + + match res.unwrap_err().kind() { + &ErrorKind::InvalidAudience => (), + _ => assert!(false), + }; + } +} diff --git a/tests/lib.rs b/tests/lib.rs index abca7f1..bb84cf1 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -2,7 +2,7 @@ extern crate jsonwebtoken; #[macro_use] extern crate serde_derive; -use jsonwebtoken::{encode, decode, Algorithm, Header, sign, verify}; +use jsonwebtoken::{encode, decode, Algorithm, Header, sign, verify, Validation}; #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] @@ -34,7 +34,7 @@ fn encode_with_custom_header() { let mut header = Header::default(); header.kid = Some("kid".to_string()); let token = encode(&header, &my_claims, "secret".as_ref()).unwrap(); - let token_data = decode::(&token, "secret".as_ref(), Algorithm::HS256).unwrap(); + let token_data = decode::(&token, "secret".as_ref(), Algorithm::HS256, Validation::default()).unwrap(); assert_eq!(my_claims, token_data.claims); assert_eq!("kid", token_data.header.kid.unwrap()); } @@ -46,7 +46,7 @@ fn round_trip_claim() { company: "ACME".to_string() }; let token = encode(&Header::default(), &my_claims, "secret".as_ref()).unwrap(); - let token_data = decode::(&token, "secret".as_ref(), Algorithm::HS256).unwrap(); + let token_data = decode::(&token, "secret".as_ref(), Algorithm::HS256, Validation::default()).unwrap(); assert_eq!(my_claims, token_data.claims); assert!(token_data.header.kid.is_none()); } @@ -54,7 +54,7 @@ fn round_trip_claim() { #[test] fn decode_token() { let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUifQ.I1BvFoHe94AFf09O6tDbcSB8-jp8w6xZqmyHIwPeSdY"; - let claims = decode::(token, "secret".as_ref(), Algorithm::HS256); + let claims = decode::(token, "secret".as_ref(), Algorithm::HS256, Validation::default()); claims.unwrap(); } @@ -62,7 +62,7 @@ fn decode_token() { #[should_panic(expected = "InvalidToken")] fn decode_token_missing_parts() { let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"; - let claims = decode::(token, "secret".as_ref(), Algorithm::HS256); + let claims = decode::(token, "secret".as_ref(), Algorithm::HS256, Validation::default()); claims.unwrap(); } @@ -70,7 +70,7 @@ fn decode_token_missing_parts() { #[should_panic(expected = "InvalidSignature")] fn decode_token_invalid_signature() { let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUifQ.wrong"; - let claims = decode::(token, "secret".as_ref(), Algorithm::HS256); + let claims = decode::(token, "secret".as_ref(), Algorithm::HS256, Validation::default()); claims.unwrap(); } @@ -78,20 +78,20 @@ fn decode_token_invalid_signature() { #[should_panic(expected = "WrongAlgorithmHeader")] fn decode_token_wrong_algorithm() { let token = "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUifQ.pKscJVk7-aHxfmQKlaZxh5uhuKhGMAa-1F5IX5mfUwI"; - let claims = decode::(token, "secret".as_ref(), Algorithm::HS256); + let claims = decode::(token, "secret".as_ref(), Algorithm::HS256, Validation::default()); 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); + let claims = decode::(token, b"\x01\x02\x03", Algorithm::HS256, Validation::default()); 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); + let claims = decode::(token, "secret".as_ref(), Algorithm::HS256, Validation::default()); assert!(claims.is_ok()); } diff --git a/tests/rsa.rs b/tests/rsa.rs index 6d678ce..8ed2904 100644 --- a/tests/rsa.rs +++ b/tests/rsa.rs @@ -2,7 +2,7 @@ extern crate jsonwebtoken; #[macro_use] extern crate serde_derive; -use jsonwebtoken::{encode, decode, Algorithm, Header, sign, verify}; +use jsonwebtoken::{encode, decode, Algorithm, Header, sign, verify, Validation}; #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] @@ -26,7 +26,7 @@ fn round_trip_claim() { company: "ACME".to_string() }; let token = encode(&Header::new(Algorithm::RS256), &my_claims, include_bytes!("private_rsa_key.der")).unwrap(); - let token_data = decode::(&token, include_bytes!("public_rsa_key.der"), Algorithm::RS256).unwrap(); + let token_data = decode::(&token, include_bytes!("public_rsa_key.der"), Algorithm::RS256, Validation::default()).unwrap(); assert_eq!(my_claims, token_data.claims); assert!(token_data.header.kid.is_none()); } From 50d676865f5376018c5753c7548f7d6d5dd7e6b2 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Wed, 12 Apr 2017 18:03:28 +0900 Subject: [PATCH 07/13] Add test for decode without validating signature --- src/lib.rs | 8 +++++++- tests/lib.rs | 8 ++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 3544903..add2702 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,6 @@ //! #![recursion_limit = "300"] - #[macro_use] extern crate error_chain; #[macro_use] @@ -29,3 +28,10 @@ pub use crypto::{ decode, }; pub use validation::Validation; + +// To consider: +//pub mod prelude { +// pub use crypto::{Algorithm, encode, decode}; +// pub use validation::Validation; +// pub use header::Header; +//} diff --git a/tests/lib.rs b/tests/lib.rs index bb84cf1..cbbea40 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -95,3 +95,11 @@ fn decode_token_with_shuffled_header_fields() { let claims = decode::(token, "secret".as_ref(), Algorithm::HS256, Validation::default()); assert!(claims.is_ok()); } + +#[test] +fn decode_without_validating_signature() { + let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjb21wYW55IjoiMTIzNDU2Nzg5MCIsInN1YiI6IkpvaG4gRG9lIn0.S"; + let claims = decode::(token, "secret".as_ref(), Algorithm::HS256, Validation {validate_signature: false, ..Validation::default()}); + assert!(claims.is_ok()); + +} From 35fd9d63cc8b30bd68f65a91807f2fc5e2b56b52 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Thu, 13 Apr 2017 10:08:07 +0900 Subject: [PATCH 08/13] Add some docs --- src/crypto.rs | 15 ++++++++---- src/errors.rs | 8 +++---- src/header.rs | 30 ++++++++++++++++++++++-- src/lib.rs | 2 ++ src/serialization.rs | 2 +- src/validation.rs | 56 +++++++++++++++++++++++++++++++++++++++++++- 6 files changed, 101 insertions(+), 12 deletions(-) diff --git a/src/crypto.rs b/src/crypto.rs index cd141ed..b5415a8 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -17,12 +17,18 @@ use validation::{Validation, validate}; /// The algorithms supported for signing/verifying #[derive(Debug, PartialEq, Copy, Clone, Serialize, Deserialize)] pub enum Algorithm { + /// HMAC using SHA-256 HS256, + /// HMAC using SHA-384 HS384, + /// HMAC using SHA-512 HS512, + /// RSASSA-PKCS1-v1_5 using SHA-256 RS256, + /// RSASSA-PKCS1-v1_5 using SHA-384 RS384, + /// RSASSA-PKCS1-v1_5 using SHA-512 RS512, } @@ -72,7 +78,7 @@ pub fn sign(signing_input: &str, key: &[u8], algorithm: Algorithm) -> Result(header: &Header, claims: &T, key: &[u8]) -> Result { let encoded_header = to_jwt_part(&header)?; let encoded_claims = to_jwt_part(&claims)?; @@ -82,10 +88,11 @@ pub fn encode(header: &Header, claims: &T, key: &[u8]) -> Result Result { match algorithm { @@ -131,9 +138,9 @@ macro_rules! expect_two { }} } -/// Decode a token into a struct containing Claims and Header +/// Decode a token into a struct containing 2 fields: `claims` and `header`. /// -/// If the token or its signature is invalid, it will return an error +/// If the token or its signature is invalid or the claims fail validation, it will return an error. pub fn decode(token: &str, key: &[u8], algorithm: Algorithm, validation: Validation) -> Result> { let (signature, signing_input) = expect_two!(token.rsplitn(2, '.')); diff --git a/src/errors.rs b/src/errors.rs index c243760..729ab51 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -60,9 +60,9 @@ error_chain! { } foreign_links { - Unspecified(ring::error::Unspecified); - Base64(base64::Base64Error); - Json(serde_json::Error); - Utf8(::std::string::FromUtf8Error); + Unspecified(ring::error::Unspecified) #[doc = "An error happened while signing/verifying a token with RSA"]; + Base64(base64::Base64Error) #[doc = "An error happened while decoding some base64 text"]; + Json(serde_json::Error) #[doc = "An error happened while serializing/deserializing JSON"]; + Utf8(::std::string::FromUtf8Error) #[doc = "An error happened while trying to convert the result of base64 decoding to a String"]; } } diff --git a/src/header.rs b/src/header.rs index 0bd7703..591074d 100644 --- a/src/header.rs +++ b/src/header.rs @@ -2,35 +2,61 @@ use crypto::Algorithm; /// A basic JWT header, the alg defaults to HS256 and typ is automatically -/// set to `JWT`. All the other fields are optional +/// set to `JWT`. All the other fields are optional. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Header { + /// The type of JWS: it can only be "JWT" here + /// + /// Defined in [RFC7515#4.1.9](https://tools.ietf.org/html/rfc7515#section-4.1.9). typ: String, + /// The algorithm used + /// + /// Defined in [RFC7515#4.1.1](https://tools.ietf.org/html/rfc7515#section-4.1.1). pub alg: Algorithm, + /// Content type + /// + /// Defined in [RFC7519#5.2](https://tools.ietf.org/html/rfc7519#section-5.2). + #[serde(skip_serializing_if = "Option::is_none")] + pub cty: Option, + /// JSON Key URL + /// + /// Defined in [RFC7515#4.1.2](https://tools.ietf.org/html/rfc7515#section-4.1.2). #[serde(skip_serializing_if = "Option::is_none")] pub jku: Option, + /// Key ID + /// + /// Defined in [RFC7515#4.1.4](https://tools.ietf.org/html/rfc7515#section-4.1.4). #[serde(skip_serializing_if = "Option::is_none")] pub kid: Option, + /// X.509 URL + /// + /// Defined in [RFC7515#4.1.5](https://tools.ietf.org/html/rfc7515#section-4.1.5). #[serde(skip_serializing_if = "Option::is_none")] pub x5u: Option, + /// X.509 certificate thumbprint + /// + /// Defined in [RFC7515#4.1.7](https://tools.ietf.org/html/rfc7515#section-4.1.7). #[serde(skip_serializing_if = "Option::is_none")] pub x5t: Option, } impl Header { + /// Returns a JWT header with the algorithm given pub fn new(algorithm: Algorithm) -> Header { Header { typ: "JWT".to_string(), alg: algorithm, + cty: None, jku: None, kid: None, x5u: None, - x5t: None + x5t: None, } } } impl Default for Header { + /// Returns a JWT header using HS256 fn default() -> Header { Header::new(Algorithm::HS256) } diff --git a/src/lib.rs b/src/lib.rs index add2702..1a2567d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ //! Create and parses JWT (JSON Web Tokens) //! #![recursion_limit = "300"] +#![deny(missing_docs)] #[macro_use] extern crate error_chain; @@ -13,6 +14,7 @@ extern crate ring; extern crate untrusted; extern crate chrono; +/// All the errors, generated using error-chain pub mod errors; mod header; mod crypto; diff --git a/src/serialization.rs b/src/serialization.rs index 41df835..fe7529e 100644 --- a/src/serialization.rs +++ b/src/serialization.rs @@ -8,7 +8,7 @@ use errors::{Result}; use header::Header; -/// The return type of a successful call to decode(...) +/// The return type of a successful call to decode #[derive(Debug)] pub struct TokenData { pub header: Header, diff --git a/src/validation.rs b/src/validation.rs index 5a8a67c..4a2cf4e 100644 --- a/src/validation.rs +++ b/src/validation.rs @@ -6,20 +6,74 @@ use serde_json::map::Map; use errors::{Result, ErrorKind}; +/// Contains the various validations that are applied after decoding a token. +/// +/// All time validation happen on UTC timestamps. +/// ```rust +/// use jsonwebtoken::Validation; +/// +/// // Default value +/// let validation = Validation::default(); +/// // Changing one parameter +/// let mut validation = Validation {leeway: 1000 * 60, ..Default::default()}; +/// // Setting audience +/// let mut validation = Validation::default(); +/// validation.set_audience(&"Me"); // string +/// validation.set_audience(&["Me", "You"]); // array of strings +/// ``` #[derive(Debug, Clone, PartialEq)] pub struct Validation { + /// Add some leeway (in ms) to the `exp`, `iat` and `nbf` validation to + /// account for clock skew. + /// + /// Defaults to `0`. pub leeway: i64, + /// Whether to actually validate the signature of the token. + /// + /// WARNING: only set that to false if you know what you are doing. + /// + /// Defaults to `true`. pub validate_signature: bool, + /// Whether to validate the `exp` field. + /// + /// It will return an error if the time in the `exp` field is past. + /// + /// Defaults to `true`. pub validate_exp: bool, + /// Whether to validate the `iat` field. + /// + /// It will return an error if the time in the `iat` field is in the future. + /// + /// Defaults to `true`. pub validate_iat: bool, + /// Whether to validate the `nbf` field. + /// + /// It will return an error if the current timestamp is before the time in the `nbf` field. + /// + /// Defaults to `true`. pub validate_nbf: bool, - + /// If it contains a value, the validation will check that the `aud` field is the same as the + /// one provided and will error otherwise. + /// Since `aud` can be either a String or a Vec in the JWT spec, you will need to use + /// the [set_audience](struct.Validation.html#method.set_audience) method to set it. + /// + /// Default to `None`. pub aud: Option, + /// If it contains a value, the validation will check that the `iss` field is the same as the + /// one provided and will error otherwise. + /// + /// Default to None pub iss: Option, + /// If it contains a value, the validation will check that the `sub` field is the same as the + /// one provided and will error otherwise. + /// + /// Default to `None`. pub sub: Option, } impl Validation { + /// Since `aud` can be either a String or an array of String in the JWT spec, this method will take + /// care of serializing the value. pub fn set_audience(&mut self, audience: &T) { self.aud = Some(to_value(audience).unwrap()); } From 3e3c752974b0ce8d2c1960ab2bed611404d9ee0f Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Thu, 13 Apr 2017 10:29:30 +0900 Subject: [PATCH 09/13] Update README --- README.md | 54 ++++++++++++++++++++------- examples/{claims.rs => validation.rs} | 20 +++------- 2 files changed, 45 insertions(+), 29 deletions(-) rename examples/{claims.rs => validation.rs} (72%) diff --git a/README.md b/README.md index 93da2a6..edcf517 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ Add the following to Cargo.toml: ```toml -jsonwebtoken = "1" -rustc-serialize = "0.3" +jsonwebtoken = "2" +serde_derive = "0.9" ``` ## How to use @@ -16,9 +16,10 @@ There is a complete example in `examples/claims.rs` but here's a quick one. In terms of imports: ```rust extern crate jsonwebtoken as jwt; -extern crate rustc_serialize; +#[macro_use] +extern crate serde_derive; -use jwt::{encode, decode, Header, Algorithm}; +use jwt::{encode, decode, Header, Algorithm, Validation}; ``` Look at the examples directory for 2 examples: a basic one and one with a custom @@ -28,24 +29,43 @@ header. ```rust let token = encode(&Header::default(), &my_claims, "secret".as_ref()).unwrap(); ``` -In that example, `my_claims` is an instance of a Claims struct that derives `RustcEncodable` and `RustcDecodable`. +In that example, `my_claims` is an instance of a Claims struct that derives `Serialize` and `Deserialize`. The default algorithm is HS256. Look at custom headers section to see how to change that. ### Decoding ```rust -let token = decode::(&token, "secret", Algorithm::HS256).unwrap(); +let token = decode::(&token, "secret", Algorithm::HS256, &Validation::default()).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: +`decode` can error for a variety of reasons: -- **InvalidToken**: if the token is not a valid JWT -- **InvalidSignature**: if the signature doesn't match -- **WrongAlgorithmHeader**: if the alg in the header doesn't match the one given to decode +- the token or its signature is invalid +- error while decoding base64 or the result of decoding base64 is not valid UTF-8 +- validation failed ### Validation -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: there is an example of that in `examples/claims.rs`. +This library validates automatically the `iat`, `exp` and `nbf` claims if found. You can also validate the `sub`, `iss` and `aud` but +those require setting the expected value. +You can add some leeway to the `iat`, `exp` and `nbf` validation by setting the `leeway` parameter as shown in the example below. + +```rust +use jsonwebtoken::Validation; + +// Default valuation +let validation = Validation::default(); +// Adding some leeway +let mut validation = Validation {leeway: 1000 * 60, ..Default::default()}; +// Checking issuer +let mut validation = Validation {iss: Some("issuer".to_string()), ..Default::default()}; +// Setting audience +let mut validation = Validation::default(); +validation.set_audience(&"Me"); // string +validation.set_audience(&["Me", "You"]); // array of strings +``` + +It's also possible to disable verifying the signature of a token by setting the `validate_signature` to `false`. This should +only be done if you know what you are doing. ### 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. @@ -60,5 +80,11 @@ let token = encode(&header, &my_claims, "secret".as_ref()).unwrap(); Look at `examples/custom_header.rs` for a full working example. ## Algorithms -The HMAC SHA family is supported: HMAC SHA256, HMAC SHA384 and HMAC SHA512 as well as the RSA PKCS1: RSA_PKCS1_SHA256, -RSA_PKCS1_SHA384 and RSA_PKCS1_SHA512. +This library currently supports the following: + +- HS256 +- HS384 +- HS512 +- RS256 +- RS384 +- RS512 diff --git a/examples/claims.rs b/examples/validation.rs similarity index 72% rename from examples/claims.rs rename to examples/validation.rs index 4f75626..7ca0507 100644 --- a/examples/claims.rs +++ b/examples/validation.rs @@ -1,5 +1,6 @@ extern crate jsonwebtoken as jwt; -#[macro_use] extern crate serde_derive; +#[macro_use] +extern crate serde_derive; use jwt::{encode, decode, Header, Algorithm, Validation}; use jwt::errors::{ErrorKind}; @@ -11,18 +12,6 @@ struct Claims { company: String } -// Example validation implementation -impl Claims { - fn is_valid(&self) -> bool { - if self.company != "ACME" { - return false; - } - // expiration etc - - true - } -} - fn main() { let my_claims = Claims { sub: "b@b.com".to_owned(), @@ -35,15 +24,16 @@ fn main() { }; println!("{:?}", token); + let validation = Validation {sub: Some("b@b.com".to_string()), ..Validation::default()}; - let token_data = match decode::(&token, key.as_ref(), Algorithm::HS256, Validation::default()) { + let token_data = match decode::(&token, key.as_ref(), Algorithm::HS256, validation) { Ok(c) => c, Err(err) => match *err.kind() { ErrorKind::InvalidToken => panic!(), // Example on how to handle a specific error + ErrorKind::InvalidIssuer => panic!(), // Example on how to handle a specific error _ => panic!() } }; println!("{:?}", token_data.claims); println!("{:?}", token_data.header); - println!("{:?}", token_data.claims.is_valid()); } From d14422725633f2a0712d73de34fc0e57705629df Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Thu, 13 Apr 2017 16:36:32 +0900 Subject: [PATCH 10/13] Reorganise a bit and add more docs --- README.md | 4 +- examples/custom_header.rs | 1 + src/crypto.rs | 61 +++---------------------- src/lib.rs | 94 ++++++++++++++++++++++++++++++++++++++- 4 files changed, 102 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index edcf517..80645dd 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ let token = decode::(&token, "secret", Algorithm::HS256, &Validation::de - the token or its signature is invalid - error while decoding base64 or the result of decoding base64 is not valid UTF-8 -- validation failed +- validation of at least one reserved claim failed ### Validation This library validates automatically the `iat`, `exp` and `nbf` claims if found. You can also validate the `sub`, `iss` and `aud` but @@ -54,7 +54,7 @@ use jsonwebtoken::Validation; // Default valuation let validation = Validation::default(); -// Adding some leeway +// Adding some leeway (in ms) for iat, exp and nbf checks let mut validation = Validation {leeway: 1000 * 60, ..Default::default()}; // Checking issuer let mut validation = Validation {iss: Some("issuer".to_string()), ..Default::default()}; diff --git a/examples/custom_header.rs b/examples/custom_header.rs index 62aad32..ddca412 100644 --- a/examples/custom_header.rs +++ b/examples/custom_header.rs @@ -27,6 +27,7 @@ fn main() { Ok(t) => t, Err(_) => panic!() // in practice you would return the error }; + println!("{:?}", token); let token_data = match decode::(&token, key.as_ref(), Algorithm::HS512, Validation::default()) { Ok(c) => c, diff --git a/src/crypto.rs b/src/crypto.rs index b5415a8..c432d35 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -3,15 +3,9 @@ use std::sync::Arc; use base64; use ring::{rand, digest, hmac, signature}; use ring::constant_time::verify_slices_are_equal; -use serde::de::Deserialize; -use serde::ser::Serialize; use untrusted; - use errors::{Result, ErrorKind}; -use header::Header; -use serialization::{from_jwt_part, to_jwt_part, from_jwt_part_claims, TokenData}; -use validation::{Validation, validate}; /// The algorithms supported for signing/verifying @@ -33,8 +27,10 @@ pub enum Algorithm { } -/// Take the payload of a JWT and sign it using the algorithm given. -/// Returns the base64 url safe encoded of the result +/// Take the payload of a JWT, sign it using the algorithm given and return +/// the base64 url safe encoded of the result. +/// +/// Only use this function if you want to do something other than JWT. pub fn sign(signing_input: &str, key: &[u8], algorithm: Algorithm) -> Result { match algorithm { Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => { @@ -78,18 +74,10 @@ pub fn sign(signing_input: &str, key: &[u8], algorithm: Algorithm) -> Result(header: &Header, claims: &T, key: &[u8]) -> Result { - let encoded_header = to_jwt_part(&header)?; - let encoded_claims = to_jwt_part(&claims)?; - let signing_input = [encoded_header.as_ref(), encoded_claims.as_ref()].join("."); - let signature = sign(&*signing_input, key.as_ref(), header.alg)?; - - Ok([signing_input, signature].join(".")) -} - /// Compares the signature given with a re-computed signature for HMAC or using the public key -/// for RSA +/// for RSA. +/// +/// Only use this function if you want to do something other than JWT. /// /// `signature` is the signature part of a jwt (text after the second '.') /// @@ -125,38 +113,3 @@ pub fn verify(signature: &str, signing_input: &str, key: &[u8], algorithm: Algor }, } } - -/// 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; - match (i.next(), i.next(), i.next()) { - (Some(first), Some(second), None) => (first, second), - _ => return Err(ErrorKind::InvalidToken.into()) - } - }} -} - -/// Decode a token into a struct containing 2 fields: `claims` and `header`. -/// -/// If the token or its signature is invalid or the claims fail validation, it will return an error. -pub fn decode(token: &str, key: &[u8], algorithm: Algorithm, validation: Validation) -> Result> { - let (signature, signing_input) = expect_two!(token.rsplitn(2, '.')); - - if validation.validate_signature && !verify(signature, signing_input, key, algorithm)? { - return Err(ErrorKind::InvalidSignature.into()); - } - - let (claims, header) = expect_two!(signing_input.rsplitn(2, '.')); - - let header: Header = from_jwt_part(header)?; - if header.alg != algorithm { - return Err(ErrorKind::WrongAlgorithmHeader.into()); - } - let (decoded_claims, claims_map): (T, _) = from_jwt_part_claims(claims)?; - - validate(&claims_map, &validation)?; - - Ok(TokenData { header: header, claims: decoded_claims }) -} diff --git a/src/lib.rs b/src/lib.rs index 1a2567d..32c83f5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ //! Create and parses JWT (JSON Web Tokens) //! +//! Documentation: [stable](https://docs.rs/jsonwebtoken/) #![recursion_limit = "300"] #![deny(missing_docs)] @@ -26,11 +27,100 @@ pub use crypto::{ Algorithm, sign, verify, - encode, - decode, }; pub use validation::Validation; + +use serde::de::Deserialize; +use serde::ser::Serialize; + +use errors::{Result, ErrorKind}; +use serialization::{TokenData, from_jwt_part, from_jwt_part_claims, to_jwt_part}; +use validation::{validate}; + + +/// Encode the header and claims given and sign the payload using the algorithm from the header and the key +/// +/// ```rust,ignore +/// #[macro_use] +/// extern crate serde_derive; +/// use jsonwebtoken::{encode, Algorithm, Header}; +/// +/// /// #[derive(Debug, Serialize, Deserialize)] +/// struct Claims { +/// sub: String, +/// company: String +/// } +/// +/// let my_claims = Claims { +/// sub: "b@b.com".to_owned(), +/// company: "ACME".to_owned() +/// }; +/// +/// // my_claims is a struct that implements Serialize +/// // This will create a JWT using HS256 as algorithm +/// let token = encode(&Header::default(), &my_claims, "secret".as_ref()).unwrap(); +/// ``` +pub fn encode(header: &Header, claims: &T, key: &[u8]) -> Result { + let encoded_header = to_jwt_part(&header)?; + let encoded_claims = to_jwt_part(&claims)?; + let signing_input = [encoded_header.as_ref(), encoded_claims.as_ref()].join("."); + let signature = sign(&*signing_input, key.as_ref(), header.alg)?; + + Ok([signing_input, 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; + match (i.next(), i.next(), i.next()) { + (Some(first), Some(second), None) => (first, second), + _ => return Err(ErrorKind::InvalidToken.into()) + } + }} +} + +/// Decode a token into a struct containing 2 fields: `claims` and `header`. +/// +/// If the token or its signature is invalid or the claims fail validation, it will return an error. +/// +/// ```rust,ignore +/// #[macro_use] +/// extern crate serde_derive; +/// use jsonwebtoken::{decode, Algorithm, Validation}; +/// +/// #[derive(Debug, Serialize, Deserialize)] +/// struct Claims { +/// sub: String, +/// company: String +/// } +/// +/// let token = "a.jwt.token".to_string(); +/// // Claims is a struct that implements Deserialize +/// let token_data = decode::(&token, "secret", Algorithm::HS256, &Validation::default()); +/// ``` +pub fn decode(token: &str, key: &[u8], algorithm: Algorithm, validation: Validation) -> Result> { + let (signature, signing_input) = expect_two!(token.rsplitn(2, '.')); + + if validation.validate_signature && !verify(signature, signing_input, key, algorithm)? { + return Err(ErrorKind::InvalidSignature.into()); + } + + let (claims, header) = expect_two!(signing_input.rsplitn(2, '.')); + + let header: Header = from_jwt_part(header)?; + if header.alg != algorithm { + return Err(ErrorKind::WrongAlgorithmHeader.into()); + } + let (decoded_claims, claims_map): (T, _) = from_jwt_part_claims(claims)?; + + validate(&claims_map, &validation)?; + + Ok(TokenData { header: header, claims: decoded_claims }) +} + // To consider: //pub mod prelude { // pub use crypto::{Algorithm, encode, decode}; From 7ebceb1b4afa7c00a0034e2f6a7f10f5d07b29b8 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Fri, 14 Apr 2017 18:15:06 +0900 Subject: [PATCH 11/13] Add notes about RSA in README --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 80645dd..6fbdb7f 100644 --- a/README.md +++ b/README.md @@ -88,3 +88,17 @@ This library currently supports the following: - RS256 - RS384 - RS512 + +### RSA +`jsonwebtoken` can only read DER encoded keys currently. If you have openssl installed, +you can run the following commands to obtain the DER keys from .pem: + +```bash +// private key +$ openssl rsa -in private_rsa_key.pem -outform DER -out private_rsa_key.der +// public key +$ openssl rsa -in private_rsa_key.der -inform DER -RSAPublicKey_out -outform DER -out public_key.der +``` + +If you are getting an error with your public key, make sure you get it by using the command above to ensure +it is in the right format. From 64ad3187ebf0a6634fc1989fe1aa0d8037ec2066 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Fri, 14 Apr 2017 20:23:28 +0900 Subject: [PATCH 12/13] Cleanup crypto nesting and remove error-chain default features --- Cargo.toml | 3 +- src/crypto.rs | 118 +++++++++++++++++++++++++------------------------- 2 files changed, 60 insertions(+), 61 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 28f1517..cff552d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,8 +10,7 @@ repository = "https://github.com/Keats/rust-jwt" keywords = ["jwt", "web", "api", "token", "json"] [dependencies] -rustc-serialize = "^0.3" -error-chain = "0.10" +error-chain = { version = "0.10", default-features = false } serde_json = "0.9" serde_derive = "0.9" serde = "0.9" diff --git a/src/crypto.rs b/src/crypto.rs index c432d35..3414c6c 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -26,6 +26,42 @@ pub enum Algorithm { RS512, } +/// The actual HS signing + encoding +fn sign_hmac(alg: &'static digest::Algorithm, key: &[u8], signing_input: &str) -> Result { + let signing_key = hmac::SigningKey::new(&alg, key); + Ok(base64::encode_config( + hmac::sign(&signing_key, signing_input.as_bytes()).as_ref(), + base64::URL_SAFE_NO_PAD + )) +} + +/// The actual RSA signing + encoding +/// Taken from Ring doc https://briansmith.org/rustdoc/ring/signature/index.html +fn sign_rsa(alg: Algorithm, key: &[u8], signing_input: &str) -> Result { + let ring_alg = match alg { + Algorithm::RS256 => &signature::RSA_PKCS1_SHA256, + Algorithm::RS384 => &signature::RSA_PKCS1_SHA384, + Algorithm::RS512 => &signature::RSA_PKCS1_SHA512, + _ => unreachable!(), + }; + + let key_pair = Arc::new( + signature::RSAKeyPair::from_der( + untrusted::Input::from(key) + ).map_err(|_| ErrorKind::InvalidKey)? + ); + let mut signing_state = signature::RSASigningState::new(key_pair) + .map_err(|_| ErrorKind::InvalidKey)?; + let mut signature = vec![0; signing_state.key_pair().public_modulus_len()]; + let rng = rand::SystemRandom::new(); + signing_state.sign(ring_alg, &rng, signing_input.as_bytes(), &mut signature) + .map_err(|_| ErrorKind::InvalidKey)?; + + Ok(base64::encode_config( + signature.as_ref(), + base64::URL_SAFE_NO_PAD + )) +} /// Take the payload of a JWT, sign it using the algorithm given and return /// the base64 url safe encoded of the result. @@ -33,47 +69,30 @@ pub enum Algorithm { /// Only use this function if you want to do something other than JWT. pub fn sign(signing_input: &str, key: &[u8], algorithm: Algorithm) -> Result { match algorithm { - Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => { - let digest = match algorithm { - Algorithm::HS256 => &digest::SHA256, - Algorithm::HS384 => &digest::SHA384, - Algorithm::HS512 => &digest::SHA512, - _ => unreachable!(), - }; - let key = hmac::SigningKey::new(digest, key); - Ok(base64::encode_config( - hmac::sign(&key, signing_input.as_bytes()).as_ref(), - base64::URL_SAFE_NO_PAD - )) - }, - Algorithm::RS256 | Algorithm::RS384 | Algorithm::RS512 => { - let ring_alg = match algorithm { - Algorithm::RS256 => &signature::RSA_PKCS1_SHA256, - Algorithm::RS384 => &signature::RSA_PKCS1_SHA384, - Algorithm::RS512 => &signature::RSA_PKCS1_SHA512, - _ => unreachable!(), - }; - // Taken from Ring doc https://briansmith.org/rustdoc/ring/signature/index.html - let key_pair = Arc::new( - signature::RSAKeyPair::from_der( - untrusted::Input::from(key) - ).map_err(|_| ErrorKind::InvalidKey)? - ); - let mut signing_state = signature::RSASigningState::new(key_pair) - .map_err(|_| ErrorKind::InvalidKey)?; - let mut signature = vec![0; signing_state.key_pair().public_modulus_len()]; - let rng = rand::SystemRandom::new(); - signing_state.sign(ring_alg, &rng, signing_input.as_bytes(), &mut signature) - .map_err(|_| ErrorKind::InvalidKey)?; + Algorithm::HS256 => sign_hmac(&digest::SHA256, key, signing_input), + Algorithm::HS384 => sign_hmac(&digest::SHA384, key, signing_input), + Algorithm::HS512 => sign_hmac(&digest::SHA512, key, signing_input), - Ok(base64::encode_config( - signature.as_ref(), - base64::URL_SAFE_NO_PAD - )) - }, + Algorithm::RS256 | Algorithm::RS384 | Algorithm::RS512 => sign_rsa(algorithm, key, signing_input), +// TODO: if PKCS1 is made prublic, remove the line above and uncomment below +// Algorithm::RS256 => sign_rsa(&signature::RSA_PKCS1_SHA256, key, signing_input), +// Algorithm::RS384 => sign_rsa(&signature::RSA_PKCS1_SHA384, key, signing_input), +// Algorithm::RS512 => sign_rsa(&signature::RSA_PKCS1_SHA512, key, signing_input), } } +/// See Ring RSA docs for more details +fn verify_rsa(alg: &signature::RSAParameters, signature: &str, signing_input: &str, key: &[u8]) -> Result { + let signature_bytes = base64::decode_config(signature, base64::URL_SAFE_NO_PAD)?; + let public_key_der = untrusted::Input::from(key); + let message = untrusted::Input::from(signing_input.as_bytes()); + let expected_signature = untrusted::Input::from(signature_bytes.as_slice()); + + let res = signature::verify(alg, public_key_der, message, expected_signature); + + Ok(res.is_ok()) +} + /// Compares the signature given with a re-computed signature for HMAC or using the public key /// for RSA. /// @@ -89,27 +108,8 @@ pub fn verify(signature: &str, signing_input: &str, key: &[u8], algorithm: Algor let signed = sign(signing_input, key, algorithm)?; Ok(verify_slices_are_equal(signature.as_ref(), signed.as_ref()).is_ok()) }, - Algorithm::RS256 | Algorithm::RS384 | Algorithm::RS512 => { - // we use ring to verify using the public key given - let verification_alg = match algorithm { - Algorithm::RS256 => &signature::RSA_PKCS1_2048_8192_SHA256, - Algorithm::RS384 => &signature::RSA_PKCS1_2048_8192_SHA384, - Algorithm::RS512 => &signature::RSA_PKCS1_2048_8192_SHA512, - _ => unreachable!(), - }; - let signature_bytes = base64::decode_config(signature, base64::URL_SAFE_NO_PAD)?; - let public_key_der = untrusted::Input::from(key); - let message = untrusted::Input::from(signing_input.as_bytes()); - let expected_signature = untrusted::Input::from(signature_bytes.as_slice()); - - let res = signature::verify( - verification_alg, - public_key_der, - message, - expected_signature, - ); - - Ok(res.is_ok()) - }, + Algorithm::RS256 => verify_rsa(&signature::RSA_PKCS1_2048_8192_SHA256, signature, signing_input, key), + Algorithm::RS384 => verify_rsa(&signature::RSA_PKCS1_2048_8192_SHA384, signature, signing_input, key), + Algorithm::RS512 => verify_rsa(&signature::RSA_PKCS1_2048_8192_SHA512, signature, signing_input, key), } } From 57c513c757233eca7001e1e818d44483cf2cbd7c Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Fri, 14 Apr 2017 20:32:39 +0900 Subject: [PATCH 13/13] Actually take a ref to Validation Seems like I updated the docs but not the code... --- benches/jwt.rs | 2 +- examples/custom_header.rs | 2 +- examples/validation.rs | 2 +- src/crypto.rs | 2 +- src/lib.rs | 4 ++-- tests/lib.rs | 18 +++++++++--------- tests/rsa.rs | 2 +- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/benches/jwt.rs b/benches/jwt.rs index bafe8de..f98009d 100644 --- a/benches/jwt.rs +++ b/benches/jwt.rs @@ -25,5 +25,5 @@ fn bench_encode(b: &mut test::Bencher) { #[bench] fn bench_decode(b: &mut test::Bencher) { let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"; - b.iter(|| decode::(token, "secret".as_ref(), Algorithm::HS256, Validation::default())); + b.iter(|| decode::(token, "secret".as_ref(), Algorithm::HS256, &Validation::default())); } diff --git a/examples/custom_header.rs b/examples/custom_header.rs index ddca412..2cdfc22 100644 --- a/examples/custom_header.rs +++ b/examples/custom_header.rs @@ -29,7 +29,7 @@ fn main() { }; println!("{:?}", token); - let token_data = match decode::(&token, key.as_ref(), Algorithm::HS512, Validation::default()) { + let token_data = match decode::(&token, key.as_ref(), Algorithm::HS512, &Validation::default()) { Ok(c) => c, Err(err) => match *err.kind() { ErrorKind::InvalidToken => panic!(), // Example on how to handle a specific error diff --git a/examples/validation.rs b/examples/validation.rs index 7ca0507..c170df3 100644 --- a/examples/validation.rs +++ b/examples/validation.rs @@ -26,7 +26,7 @@ fn main() { println!("{:?}", token); let validation = Validation {sub: Some("b@b.com".to_string()), ..Validation::default()}; - let token_data = match decode::(&token, key.as_ref(), Algorithm::HS256, validation) { + let token_data = match decode::(&token, key.as_ref(), Algorithm::HS256, &validation) { Ok(c) => c, Err(err) => match *err.kind() { ErrorKind::InvalidToken => panic!(), // Example on how to handle a specific error diff --git a/src/crypto.rs b/src/crypto.rs index 3414c6c..2351420 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -28,7 +28,7 @@ pub enum Algorithm { /// The actual HS signing + encoding fn sign_hmac(alg: &'static digest::Algorithm, key: &[u8], signing_input: &str) -> Result { - let signing_key = hmac::SigningKey::new(&alg, key); + let signing_key = hmac::SigningKey::new(alg, key); Ok(base64::encode_config( hmac::sign(&signing_key, signing_input.as_bytes()).as_ref(), base64::URL_SAFE_NO_PAD diff --git a/src/lib.rs b/src/lib.rs index 32c83f5..1f517cc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -101,7 +101,7 @@ macro_rules! expect_two { /// // Claims is a struct that implements Deserialize /// let token_data = decode::(&token, "secret", Algorithm::HS256, &Validation::default()); /// ``` -pub fn decode(token: &str, key: &[u8], algorithm: Algorithm, validation: Validation) -> Result> { +pub fn decode(token: &str, key: &[u8], algorithm: Algorithm, validation: &Validation) -> Result> { let (signature, signing_input) = expect_two!(token.rsplitn(2, '.')); if validation.validate_signature && !verify(signature, signing_input, key, algorithm)? { @@ -116,7 +116,7 @@ pub fn decode(token: &str, key: &[u8], algorithm: Algorithm, val } let (decoded_claims, claims_map): (T, _) = from_jwt_part_claims(claims)?; - validate(&claims_map, &validation)?; + validate(&claims_map, validation)?; Ok(TokenData { header: header, claims: decoded_claims }) } diff --git a/tests/lib.rs b/tests/lib.rs index cbbea40..88f2815 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -34,7 +34,7 @@ fn encode_with_custom_header() { let mut header = Header::default(); header.kid = Some("kid".to_string()); let token = encode(&header, &my_claims, "secret".as_ref()).unwrap(); - let token_data = decode::(&token, "secret".as_ref(), Algorithm::HS256, Validation::default()).unwrap(); + let token_data = decode::(&token, "secret".as_ref(), Algorithm::HS256, &Validation::default()).unwrap(); assert_eq!(my_claims, token_data.claims); assert_eq!("kid", token_data.header.kid.unwrap()); } @@ -46,7 +46,7 @@ fn round_trip_claim() { company: "ACME".to_string() }; let token = encode(&Header::default(), &my_claims, "secret".as_ref()).unwrap(); - let token_data = decode::(&token, "secret".as_ref(), Algorithm::HS256, Validation::default()).unwrap(); + let token_data = decode::(&token, "secret".as_ref(), Algorithm::HS256, &Validation::default()).unwrap(); assert_eq!(my_claims, token_data.claims); assert!(token_data.header.kid.is_none()); } @@ -54,7 +54,7 @@ fn round_trip_claim() { #[test] fn decode_token() { let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUifQ.I1BvFoHe94AFf09O6tDbcSB8-jp8w6xZqmyHIwPeSdY"; - let claims = decode::(token, "secret".as_ref(), Algorithm::HS256, Validation::default()); + let claims = decode::(token, "secret".as_ref(), Algorithm::HS256, &Validation::default()); claims.unwrap(); } @@ -62,7 +62,7 @@ fn decode_token() { #[should_panic(expected = "InvalidToken")] fn decode_token_missing_parts() { let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"; - let claims = decode::(token, "secret".as_ref(), Algorithm::HS256, Validation::default()); + let claims = decode::(token, "secret".as_ref(), Algorithm::HS256, &Validation::default()); claims.unwrap(); } @@ -70,7 +70,7 @@ fn decode_token_missing_parts() { #[should_panic(expected = "InvalidSignature")] fn decode_token_invalid_signature() { let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUifQ.wrong"; - let claims = decode::(token, "secret".as_ref(), Algorithm::HS256, Validation::default()); + let claims = decode::(token, "secret".as_ref(), Algorithm::HS256, &Validation::default()); claims.unwrap(); } @@ -78,28 +78,28 @@ fn decode_token_invalid_signature() { #[should_panic(expected = "WrongAlgorithmHeader")] fn decode_token_wrong_algorithm() { let token = "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUifQ.pKscJVk7-aHxfmQKlaZxh5uhuKhGMAa-1F5IX5mfUwI"; - let claims = decode::(token, "secret".as_ref(), Algorithm::HS256, Validation::default()); + let claims = decode::(token, "secret".as_ref(), Algorithm::HS256, &Validation::default()); 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, Validation::default()); + let claims = decode::(token, b"\x01\x02\x03", Algorithm::HS256, &Validation::default()); 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, Validation::default()); + let claims = decode::(token, "secret".as_ref(), Algorithm::HS256, &Validation::default()); assert!(claims.is_ok()); } #[test] fn decode_without_validating_signature() { let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjb21wYW55IjoiMTIzNDU2Nzg5MCIsInN1YiI6IkpvaG4gRG9lIn0.S"; - let claims = decode::(token, "secret".as_ref(), Algorithm::HS256, Validation {validate_signature: false, ..Validation::default()}); + let claims = decode::(token, "secret".as_ref(), Algorithm::HS256, &Validation {validate_signature: false, ..Validation::default()}); assert!(claims.is_ok()); } diff --git a/tests/rsa.rs b/tests/rsa.rs index 8ed2904..e1cb31c 100644 --- a/tests/rsa.rs +++ b/tests/rsa.rs @@ -26,7 +26,7 @@ fn round_trip_claim() { company: "ACME".to_string() }; let token = encode(&Header::new(Algorithm::RS256), &my_claims, include_bytes!("private_rsa_key.der")).unwrap(); - let token_data = decode::(&token, include_bytes!("public_rsa_key.der"), Algorithm::RS256, Validation::default()).unwrap(); + let token_data = decode::(&token, include_bytes!("public_rsa_key.der"), Algorithm::RS256, &Validation::default()).unwrap(); assert_eq!(my_claims, token_data.claims); assert!(token_data.header.kid.is_none()); }