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/Cargo.toml b/Cargo.toml index bbf2eb5..cff552d 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" @@ -10,5 +10,11 @@ repository = "https://github.com/Keats/rust-jwt" keywords = ["jwt", "web", "api", "token", "json"] [dependencies] -rustc-serialize = "^0.3" -ring = "^0.7" +error-chain = { version = "0.10", default-features = false } +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/README.md b/README.md index f499ef2..6fbdb7f 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 @@ -26,26 +27,45 @@ 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`. +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 of at least one reserved claim 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 (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()}; +// 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. @@ -55,29 +75,30 @@ 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. +This library currently supports the following: -## Performance -On my thinkpad 440s for a 2 claims struct using HMAC SHA256: +- HS256 +- HS384 +- HS512 +- RS256 +- RS384 +- RS512 -``` -test bench_decode ... bench: 4,947 ns/iter (+/- 611) -test bench_encode ... bench: 3,301 ns/iter (+/- 465) +### 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 ``` -## 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 +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. diff --git a/benches/jwt.rs b/benches/jwt.rs index 5813bc9..f98009d 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}; +use jwt::{encode, decode, Algorithm, Header, Validation}; -#[derive(Debug, PartialEq, Clone, RustcEncodable, RustcDecodable)] +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] struct Claims { sub: String, company: String @@ -18,11 +19,11 @@ 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] 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 deleted file mode 100644 index 1c138ad..0000000 --- a/examples/claims.rs +++ /dev/null @@ -1,49 +0,0 @@ -extern crate jsonwebtoken as jwt; -extern crate rustc_serialize; - -use jwt::{encode, decode, Header, Algorithm}; -use jwt::errors::{Error}; - - -#[derive(Debug, RustcEncodable, RustcDecodable)] -struct Claims { - sub: String, - company: String -} - -// 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(), - company: "ACME".to_owned() - }; - let key = "secret"; - let token = match encode(Header::default(), &my_claims, key.as_ref()) { - Ok(t) => t, - Err(_) => panic!() // in practice you would return the error - }; - - println!("{:?}", token); - - let token_data = match decode::(&token, key.as_ref(), Algorithm::HS256) { - Ok(c) => c, - Err(err) => match err { - Error::InvalidToken => panic!(), // Example on how to handle a specific error - _ => panic!() - } - }; - println!("{:?}", token_data.claims); - println!("{:?}", token_data.header); - println!("{:?}", token_data.claims.is_valid()); -} diff --git a/examples/custom_header.rs b/examples/custom_header.rs index 1b7e686..2cdfc22 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::{encode, decode, Header, Algorithm, Validation}; +use jwt::errors::{ErrorKind}; -#[derive(Debug, RustcEncodable, RustcDecodable)] +#[derive(Debug, Serialize, Deserialize)] struct Claims { sub: String, company: String @@ -22,15 +23,16 @@ 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 }; + println!("{:?}", token); - 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 { - 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/validation.rs b/examples/validation.rs new file mode 100644 index 0000000..c170df3 --- /dev/null +++ b/examples/validation.rs @@ -0,0 +1,39 @@ +extern crate jsonwebtoken as jwt; +#[macro_use] +extern crate serde_derive; + +use jwt::{encode, decode, Header, Algorithm, Validation}; +use jwt::errors::{ErrorKind}; + + +#[derive(Debug, Serialize, Deserialize)] +struct Claims { + sub: String, + company: String +} + +fn main() { + let my_claims = Claims { + sub: "b@b.com".to_owned(), + company: "ACME".to_owned() + }; + let key = "secret"; + let token = match encode(&Header::default(), &my_claims, key.as_ref()) { + Ok(t) => t, + Err(_) => panic!() // in practice you would return the error + }; + + 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) { + 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); +} diff --git a/src/crypto.rs b/src/crypto.rs new file mode 100644 index 0000000..2351420 --- /dev/null +++ b/src/crypto.rs @@ -0,0 +1,115 @@ +use std::sync::Arc; + +use base64; +use ring::{rand, digest, hmac, signature}; +use ring::constant_time::verify_slices_are_equal; +use untrusted; + +use errors::{Result, ErrorKind}; + + +/// 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, +} + +/// 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. +/// +/// 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 => 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), + + 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. +/// +/// 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 '.') +/// +/// `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 => 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), + } +} diff --git a/src/errors.rs b/src/errors.rs index 1d78005..729ab51 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,68 +1,68 @@ -use std::{string, fmt, error}; -use rustc_serialize::{json, base64}; +use base64; +use serde_json; +use ring; -#[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 { + /// 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") + display("Wrong Algorithm Header") + } + /// When the secret given is not a valid RSA key + InvalidKey { + description("invalid key") + display("Invalid Key") } - } -} -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); + // Validation error -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", + /// 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 `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") + display("Invalid Issued At") + } + /// When a token’s nbf claim represents a time in the future + ImmatureSignature { + description("immature signature") + display("Immature Signature") } } - 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 { + 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 new file mode 100644 index 0000000..591074d --- /dev/null +++ b/src/header.rs @@ -0,0 +1,63 @@ +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, 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, + } + } +} + +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 52283aa..1f517cc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,292 +1,129 @@ //! Create and parses JWT (JSON Web Tokens) //! +//! Documentation: [stable](https://docs.rs/jsonwebtoken/) +#![recursion_limit = "300"] +#![deny(missing_docs)] -#![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; +extern crate untrusted; +extern crate chrono; -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}; - +/// All the errors, generated using error-chain pub mod errors; -use errors::Error; -use std::collections::BTreeMap; +mod header; +mod crypto; +mod serialization; +mod validation; -#[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, +}; +pub use validation::Validation; -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; +use serde::de::Deserialize; +use serde::ser::Serialize; - fn from_base64>(encoded: B) -> Result where Self: Sized; - fn to_base64(&self) -> Result; -} +use errors::{Result, ErrorKind}; +use serialization::{TokenData, from_jwt_part, from_jwt_part_claims, to_jwt_part}; +use validation::{validate}; -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)) - } +/// 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)?; - 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(".")) + 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; // evaluate the expr + let mut i = $iter; match (i.next(), i.next(), i.next()) { (Some(first), Some(second), None) => (first, second), - _ => return Err(Error::InvalidToken) + _ => 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, Error> { - let (signature, payload) = expect_two!(token.rsplitn(2, '.')); +/// 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, '.')); - let is_valid = verify( - signature, - payload, - secret, - algorithm - ); - - if !is_valid { - return Err(Error::InvalidSignature); + if validation.validate_signature && !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 = try!(Header::from_base64(header)); + let header: Header = from_jwt_part(header)?; if header.alg != algorithm { - return Err(Error::WrongAlgorithmHeader); + return Err(ErrorKind::WrongAlgorithmHeader.into()); } - let decoded_claims = try!(T::from_base64(claims)); + let (decoded_claims, claims_map): (T, _) = from_jwt_part_claims(claims)?; - Ok(TokenData { header: header, claims: decoded_claims}) + validate(&claims_map, validation)?; + + 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()); - } -} +// To consider: +//pub mod prelude { +// pub use crypto::{Algorithm, encode, decode}; +// pub use validation::Validation; +// pub use header::Header; +//} diff --git a/src/serialization.rs b/src/serialization.rs new file mode 100644 index 0000000..fe7529e --- /dev/null +++ b/src/serialization.rs @@ -0,0 +1,40 @@ +use base64; +use serde::de::Deserialize; +use serde::ser::Serialize; +use serde_json::{from_str, to_string, Value}; +use serde_json::map::Map; + +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 to JSON and encodes to base64 +pub fn to_jwt_part(input: &T) -> Result { + let encoded = to_string(input)?; + Ok(base64::encode_config(encoded.as_bytes(), base64::URL_SAFE_NO_PAD)) +} + +/// 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(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..4a2cf4e --- /dev/null +++ b/src/validation.rs @@ -0,0 +1,367 @@ +use chrono::UTC; +use serde::ser::Serialize; +use serde_json::{Value, from_value, to_value}; +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()); + } +} + +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 new file mode 100644 index 0000000..88f2815 --- /dev/null +++ b/tests/lib.rs @@ -0,0 +1,105 @@ +extern crate jsonwebtoken; +#[macro_use] +extern crate serde_derive; + +use jsonwebtoken::{encode, decode, Algorithm, Header, sign, verify, Validation}; + + +#[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).unwrap(); + let expected = "c0zGLzKEFWj0VxWuufTXiRMk5tlI5MbGDAYhzaxIYjo"; + assert_eq!(result, expected); +} + +#[test] +fn verify_hs256() { + let sig = "c0zGLzKEFWj0VxWuufTXiRMk5tlI5MbGDAYhzaxIYjo"; + let valid = verify(sig, "hello world", b"secret", Algorithm::HS256).unwrap(); + 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, &Validation::default()).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, &Validation::default()).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, &Validation::default()); + claims.unwrap(); +} + +#[test] +#[should_panic(expected = "InvalidToken")] +fn decode_token_missing_parts() { + let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"; + let claims = decode::(token, "secret".as_ref(), Algorithm::HS256, &Validation::default()); + 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, &Validation::default()); + 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, &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()); + 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()); + 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()); + +} diff --git a/tests/notes.md b/tests/notes.md new file mode 100644 index 0000000..41680ba --- /dev/null +++ b/tests/notes.md @@ -0,0 +1,22 @@ +# 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` + +## Generating public key +`openssl rsa -in private_rsa_key.der -inform DER -RSAPublicKey_out -outform DER -out public_key.der` diff --git a/tests/private_rsa_key.der b/tests/private_rsa_key.der new file mode 100644 index 0000000..09ee0a8 Binary files /dev/null and b/tests/private_rsa_key.der differ diff --git a/tests/private_rsa_key.pem b/tests/private_rsa_key.pem new file mode 100644 index 0000000..f41295f --- /dev/null +++ b/tests/private_rsa_key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAyRE6rHuNR0QbHO3H3Kt2pOKGVhQqGZXInOduQNxXzuKlvQTL +UTv4l4sggh5/CYYi/cvI+SXVT9kPWSKXxJXBXd/4LkvcPuUakBoAkfh+eiFVMh2V +rUyWyj3MFl0HTVF9KwRXLAcwkREiS3npThHRyIxuy0ZMeZfxVL5arMhw1SRELB8H +oGfG/AtH89BIE9jDBHZ9dLelK9a184zAf8LwoPLxvJb3Il5nncqPcSfKDDodMFBI +Mc4lQzDKL5gvmiXLXB1AGLm8KBjfE8s3L5xqi+yUod+j8MtvIj812dkS4QMiRVN/ +by2h3ZY8LYVGrqZXZTcgn2ujn8uKjXLZVD5TdQIDAQABAoIBAHREk0I0O9DvECKd +WUpAmF3mY7oY9PNQiu44Yaf+AoSuyRpRUGTMIgc3u3eivOE8ALX0BmYUO5JtuRNZ +Dpvt4SAwqCnVUinIf6C+eH/wSurCpapSM0BAHp4aOA7igptyOMgMPYBHNA1e9A7j +E0dCxKWMl3DSWNyjQTk4zeRGEAEfbNjHrq6YCtjHSZSLmWiG80hnfnYos9hOr5Jn +LnyS7ZmFE/5P3XVrxLc/tQ5zum0R4cbrgzHiQP5RgfxGJaEi7XcgherCCOgurJSS +bYH29Gz8u5fFbS+Yg8s+OiCss3cs1rSgJ9/eHZuzGEdUZVARH6hVMjSuwvqVTFaE +8AgtleECgYEA+uLMn4kNqHlJS2A5uAnCkj90ZxEtNm3E8hAxUrhssktY5XSOAPBl +xyf5RuRGIImGtUVIr4HuJSa5TX48n3Vdt9MYCprO/iYl6moNRSPt5qowIIOJmIjY +2mqPDfDt/zw+fcDD3lmCJrFlzcnh0uea1CohxEbQnL3cypeLt+WbU6kCgYEAzSp1 +9m1ajieFkqgoB0YTpt/OroDx38vvI5unInJlEeOjQ+oIAQdN2wpxBvTrRorMU6P0 +7mFUbt1j+Co6CbNiw+X8HcCaqYLR5clbJOOWNR36PuzOpQLkfK8woupBxzW9B8gZ +mY8rB1mbJ+/WTPrEJy6YGmIEBkWylQ2VpW8O4O0CgYEApdbvvfFBlwD9YxbrcGz7 +MeNCFbMz+MucqQntIKoKJ91ImPxvtc0y6e/Rhnv0oyNlaUOwJVu0yNgNG117w0g4 +t/+Q38mvVC5xV7/cn7x9UMFk6MkqVir3dYGEqIl/OP1grY2Tq9HtB5iyG9L8NIam +QOLMyUqqMUILxdthHyFmiGkCgYEAn9+PjpjGMPHxL0gj8Q8VbzsFtou6b1deIRRA +2CHmSltltR1gYVTMwXxQeUhPMmgkMqUXzs4/WijgpthY44hK1TaZEKIuoxrS70nJ +4WQLf5a9k1065fDsFZD6yGjdGxvwEmlGMZgTwqV7t1I4X0Ilqhav5hcs5apYL7gn +PYPeRz0CgYALHCj/Ji8XSsDoF/MhVhnGdIs2P99NNdmo3R2Pv0CuZbDKMU559LJH +UvrKS8WkuWRDuKrz1W/EQKApFjDGpdqToZqriUFQzwy7mR3ayIiogzNtHcvbDHx8 +oFnGY0OFksX/ye0/XGpy2SFxYRwGU98HPYeBvAQQrVjdkzfy7BmXQQ== +-----END RSA PRIVATE KEY----- diff --git a/tests/public_rsa_key.der b/tests/public_rsa_key.der new file mode 100644 index 0000000..f548e15 Binary files /dev/null and b/tests/public_rsa_key.der differ 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..e1cb31c --- /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, Validation}; + + +#[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, &Validation::default()).unwrap(); + assert_eq!(my_claims, token_data.claims); + assert!(token_data.header.kid.is_none()); +}