Merge pull request #23 from Keats/v2

V2
This commit is contained in:
Vincent Prouillet 2017-04-20 12:32:23 +09:00 committed by GitHub
commit b1b2094cbe
20 changed files with 1069 additions and 413 deletions

19
CHANGELOG.md Normal file
View File

@ -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

View File

@ -1,6 +1,6 @@
[package]
name = "jsonwebtoken"
version = "1.1.7"
version = "2.0.0"
authors = ["Vincent Prouillet <vincent@wearewizards.io>"]
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"

View File

@ -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::<Claims>(&token, "secret", Algorithm::HS256).unwrap();
let token = decode::<Claims>(&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.

View File

@ -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::<Claims>(token, "secret".as_ref(), Algorithm::HS256));
b.iter(|| decode::<Claims>(token, "secret".as_ref(), Algorithm::HS256, &Validation::default()));
}

View File

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

View File

@ -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::<Claims>(&token, key.as_ref(), Algorithm::HS512) {
let token_data = match decode::<Claims>(&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!()
}
};

39
examples/validation.rs Normal file
View File

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

115
src/crypto.rs Normal file
View File

@ -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<String> {
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<String> {
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<String> {
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<bool> {
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<bool> {
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),
}
}

View File

@ -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 tokens `exp` claim indicates that it has expired
ExpiredSignature {
description("expired signature")
display("Expired Signature")
}
/// When a tokens `iss` claim does not match the expected issuer
InvalidIssuer {
description("invalid issuer")
display("Invalid Issuer")
}
/// When a tokens `aud` claim does not match one of the expected audience values
InvalidAudience {
description("invalid audience")
display("Invalid Audience")
}
/// When a tokens `aud` claim does not match one of the expected audience values
InvalidSubject {
description("invalid subject")
display("Invalid Subject")
}
/// When a tokens `iat` claim is in the future
InvalidIssuedAt {
description("invalid issued at")
display("Invalid Issued At")
}
/// When a tokens 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"];
}
}

63
src/header.rs Normal file
View File

@ -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<String>,
/// 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<String>,
/// 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<String>,
/// 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<String>,
/// 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<String>,
}
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)
}
}

View File

@ -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<str>;
use serde::de::Deserialize;
use serde::ser::Serialize;
fn from_base64<B: AsRef<[u8]>>(encoded: B) -> Result<Self, Error> where Self: Sized;
fn to_base64(&self) -> Result<Self::Encoded, Error>;
}
use errors::{Result, ErrorKind};
use serialization::{TokenData, from_jwt_part, from_jwt_part_claims, to_jwt_part};
use validation::{validate};
impl<T> Part for T where T: Encodable + Decodable {
type Encoded = String;
fn to_base64(&self) -> Result<Self::Encoded, Error> {
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<T: Serialize>(header: &Header, claims: &T, key: &[u8]) -> Result<String> {
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<B: AsRef<[u8]>>(encoded: B) -> Result<T, Error> {
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<String>,
pub kid: Option<String>,
pub x5u: Option<String>,
pub x5t: Option<String>
}
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<S: rustc_serialize::Encoder>(&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<T: Part> {
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<T: Part>(header: Header, claims: &T, secret: &[u8]) -> Result<String, Error> {
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<T: Part>(token: &str, secret: &[u8], algorithm: Algorithm) -> Result<TokenData<T>, 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::<Claims>(&token, "secret", Algorithm::HS256, &Validation::default());
/// ```
pub fn decode<T: Deserialize>(token: &str, key: &[u8], algorithm: Algorithm, validation: &Validation) -> Result<TokenData<T>> {
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::<Claims>(&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::<Claims>(&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::<Claims>(token, "secret".as_ref(), Algorithm::HS256);
claims.unwrap();
}
#[test]
#[should_panic(expected = "InvalidToken")]
fn decode_token_missing_parts() {
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9";
let claims = decode::<Claims>(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::<Claims>(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::<Claims>(token, "secret".as_ref(), Algorithm::HS256);
claims.unwrap();
}
#[test]
fn decode_token_with_bytes_secret() {
let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiY29tcGFueSI6Ikdvb2dvbCJ9.27QxgG96vpX4akKNpD1YdRGHE3_u2X35wR3EHA2eCrs";
let claims = decode::<Claims>(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::<Claims>(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;
//}

40
src/serialization.rs Normal file
View File

@ -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<T: Deserialize> {
pub header: Header,
pub claims: T
}
/// Serializes to JSON and encodes to base64
pub fn to_jwt_part<T: Serialize>(input: &T) -> Result<String> {
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<B: AsRef<str>, T: Deserialize>(encoded: B) -> Result<T> {
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<B: AsRef<str>, T: Deserialize>(encoded: B) -> Result<(T, Map<String, Value>)> {
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))
}

367
src/validation.rs Normal file
View File

@ -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<String> 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<Value>,
/// 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<String>,
/// 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<String>,
}
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<T: Serialize>(&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<String, Value>, options: &Validation) -> Result<()> {
let now = UTC::now().timestamp();
if let Some(iat) = claims.get("iat") {
if options.validate_iat && from_value::<i64>(iat.clone())? > now + options.leeway {
return Err(ErrorKind::InvalidIssuedAt.into());
}
}
if let Some(exp) = claims.get("exp") {
if options.validate_exp && from_value::<i64>(exp.clone())? < now - options.leeway {
return Err(ErrorKind::ExpiredSignature.into());
}
}
if let Some(nbf) = claims.get("nbf") {
if options.validate_nbf && from_value::<i64>(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::<String>(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::<String>(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),
};
}
}

105
tests/lib.rs Normal file
View File

@ -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::<Claims>(&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::<Claims>(&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::<Claims>(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::<Claims>(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::<Claims>(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::<Claims>(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::<Claims>(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::<Claims>(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::<Claims>(token, "secret".as_ref(), Algorithm::HS256, &Validation {validate_signature: false, ..Validation::default()});
assert!(claims.is_ok());
}

22
tests/notes.md Normal file
View File

@ -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`

BIN
tests/private_rsa_key.der Normal file

Binary file not shown.

27
tests/private_rsa_key.pem Normal file
View File

@ -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-----

BIN
tests/public_rsa_key.der Normal file

Binary file not shown.

9
tests/public_rsa_key.pem Normal file
View File

@ -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-----

32
tests/rsa.rs Normal file
View File

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