commit
b1b2094cbe
|
@ -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
|
12
Cargo.toml
12
Cargo.toml
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "jsonwebtoken"
|
name = "jsonwebtoken"
|
||||||
version = "1.1.7"
|
version = "2.0.0"
|
||||||
authors = ["Vincent Prouillet <vincent@wearewizards.io>"]
|
authors = ["Vincent Prouillet <vincent@wearewizards.io>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
@ -10,5 +10,11 @@ repository = "https://github.com/Keats/rust-jwt"
|
||||||
keywords = ["jwt", "web", "api", "token", "json"]
|
keywords = ["jwt", "web", "api", "token", "json"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rustc-serialize = "^0.3"
|
error-chain = { version = "0.10", default-features = false }
|
||||||
ring = "^0.7"
|
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"
|
||||||
|
|
83
README.md
83
README.md
|
@ -6,8 +6,8 @@
|
||||||
Add the following to Cargo.toml:
|
Add the following to Cargo.toml:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
jsonwebtoken = "1"
|
jsonwebtoken = "2"
|
||||||
rustc-serialize = "0.3"
|
serde_derive = "0.9"
|
||||||
```
|
```
|
||||||
|
|
||||||
## How to use
|
## 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:
|
In terms of imports:
|
||||||
```rust
|
```rust
|
||||||
extern crate jsonwebtoken as jwt;
|
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
|
Look at the examples directory for 2 examples: a basic one and one with a custom
|
||||||
|
@ -26,26 +27,45 @@ header.
|
||||||
|
|
||||||
### Encoding
|
### Encoding
|
||||||
```rust
|
```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.
|
The default algorithm is HS256.
|
||||||
Look at custom headers section to see how to change that.
|
Look at custom headers section to see how to change that.
|
||||||
|
|
||||||
### Decoding
|
### Decoding
|
||||||
```rust
|
```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
|
// 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
|
- the token or its signature is invalid
|
||||||
- **InvalidSignature**: if the signature doesn't match
|
- error while decoding base64 or the result of decoding base64 is not valid UTF-8
|
||||||
- **WrongAlgorithmHeader**: if the alg in the header doesn't match the one given to decode
|
- validation of at least one reserved claim failed
|
||||||
|
|
||||||
### Validation
|
### Validation
|
||||||
The library only validates the algorithm type used but does not verify claims such as expiration.
|
This library validates automatically the `iat`, `exp` and `nbf` claims if found. You can also validate the `sub`, `iss` and `aud` but
|
||||||
Feel free to add a `validate` method to your claims struct to handle that: there is an example of that in `examples/claims.rs`.
|
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
|
### 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.
|
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();
|
let mut header = Header::default();
|
||||||
header.kid = Some("blabla".to_owned());
|
header.kid = Some("blabla".to_owned());
|
||||||
header.alg = Algorithm::HS512;
|
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.
|
Look at `examples/custom_header.rs` for a full working example.
|
||||||
|
|
||||||
## Algorithms
|
## Algorithms
|
||||||
Right now, only HMAC SHA family is supported: HMAC SHA256, HMAC SHA384 and HMAC SHA512.
|
This library currently supports the following:
|
||||||
|
|
||||||
## Performance
|
- HS256
|
||||||
On my thinkpad 440s for a 2 claims struct using HMAC SHA256:
|
- HS384
|
||||||
|
- HS512
|
||||||
|
- RS256
|
||||||
|
- RS384
|
||||||
|
- RS512
|
||||||
|
|
||||||
```
|
### RSA
|
||||||
test bench_decode ... bench: 4,947 ns/iter (+/- 611)
|
`jsonwebtoken` can only read DER encoded keys currently. If you have openssl installed,
|
||||||
test bench_encode ... bench: 3,301 ns/iter (+/- 465)
|
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
|
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.
|
||||||
- 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
|
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
#![feature(test)]
|
#![feature(test)]
|
||||||
extern crate test;
|
extern crate test;
|
||||||
extern crate jsonwebtoken as jwt;
|
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 {
|
struct Claims {
|
||||||
sub: String,
|
sub: String,
|
||||||
company: String
|
company: String
|
||||||
|
@ -18,11 +19,11 @@ fn bench_encode(b: &mut test::Bencher) {
|
||||||
company: "ACME".to_owned()
|
company: "ACME".to_owned()
|
||||||
};
|
};
|
||||||
|
|
||||||
b.iter(|| encode(Header::default(), &claim, "secret".as_ref()));
|
b.iter(|| encode(&Header::default(), &claim, "secret".as_ref()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn bench_decode(b: &mut test::Bencher) {
|
fn bench_decode(b: &mut test::Bencher) {
|
||||||
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ";
|
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ";
|
||||||
b.iter(|| decode::<Claims>(token, "secret".as_ref(), Algorithm::HS256));
|
b.iter(|| decode::<Claims>(token, "secret".as_ref(), Algorithm::HS256, &Validation::default()));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
|
||||||
}
|
|
|
@ -1,11 +1,12 @@
|
||||||
extern crate jsonwebtoken as jwt;
|
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};
|
||||||
use jwt::errors::{Error};
|
use jwt::errors::{ErrorKind};
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, RustcEncodable, RustcDecodable)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
struct Claims {
|
struct Claims {
|
||||||
sub: String,
|
sub: String,
|
||||||
company: String
|
company: String
|
||||||
|
@ -22,15 +23,16 @@ fn main() {
|
||||||
header.kid = Some("signing_key".to_owned());
|
header.kid = Some("signing_key".to_owned());
|
||||||
header.alg = Algorithm::HS512;
|
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,
|
Ok(t) => t,
|
||||||
Err(_) => panic!() // in practice you would return the error
|
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,
|
Ok(c) => c,
|
||||||
Err(err) => match err {
|
Err(err) => match *err.kind() {
|
||||||
Error::InvalidToken => panic!(), // Example on how to handle a specific error
|
ErrorKind::InvalidToken => panic!(), // Example on how to handle a specific error
|
||||||
_ => panic!()
|
_ => panic!()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -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),
|
||||||
|
}
|
||||||
|
}
|
118
src/errors.rs
118
src/errors.rs
|
@ -1,68 +1,68 @@
|
||||||
use std::{string, fmt, error};
|
use base64;
|
||||||
use rustc_serialize::{json, base64};
|
use serde_json;
|
||||||
|
use ring;
|
||||||
|
|
||||||
#[derive(Debug)]
|
error_chain! {
|
||||||
/// All the errors we can encounter while signing/verifying tokens
|
errors {
|
||||||
/// and a couple of custom one for when the token we are trying
|
/// When a token doesn't have a valid token shape
|
||||||
/// to verify is invalid
|
InvalidToken {
|
||||||
pub enum Error {
|
description("invalid token")
|
||||||
EncodeJSON(json::EncoderError),
|
display("Invalid token")
|
||||||
DecodeBase64(base64::FromBase64Error),
|
}
|
||||||
DecodeJSON(json::DecoderError),
|
/// When the signature doesn't match
|
||||||
Utf8(string::FromUtf8Error),
|
InvalidSignature {
|
||||||
|
description("invalid signature")
|
||||||
InvalidToken,
|
display("Invalid signature")
|
||||||
InvalidSignature,
|
}
|
||||||
WrongAlgorithmHeader
|
/// When the algorithm in the header doesn't match the one passed to `decode`
|
||||||
}
|
WrongAlgorithmHeader {
|
||||||
|
description("wrong algorithm header")
|
||||||
macro_rules! impl_from_error {
|
display("Wrong Algorithm Header")
|
||||||
($f: ty, $e: expr) => {
|
}
|
||||||
impl From<$f> for Error {
|
/// When the secret given is not a valid RSA key
|
||||||
fn from(f: $f) -> Error { $e(f) }
|
InvalidKey {
|
||||||
|
description("invalid key")
|
||||||
|
display("Invalid Key")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl_from_error!(json::EncoderError, Error::EncodeJSON);
|
// Validation error
|
||||||
impl_from_error!(base64::FromBase64Error, Error::DecodeBase64);
|
|
||||||
impl_from_error!(json::DecoderError, Error::DecodeJSON);
|
|
||||||
impl_from_error!(string::FromUtf8Error, Error::Utf8);
|
|
||||||
|
|
||||||
impl error::Error for Error {
|
/// When a token’s `exp` claim indicates that it has expired
|
||||||
fn description(&self) -> &str {
|
ExpiredSignature {
|
||||||
match *self {
|
description("expired signature")
|
||||||
Error::EncodeJSON(ref err) => err.description(),
|
display("Expired Signature")
|
||||||
Error::DecodeBase64(ref err) => err.description(),
|
}
|
||||||
Error::DecodeJSON(ref err) => err.description(),
|
/// When a token’s `iss` claim does not match the expected issuer
|
||||||
Error::Utf8(ref err) => err.description(),
|
InvalidIssuer {
|
||||||
Error::InvalidToken => "Invalid Token",
|
description("invalid issuer")
|
||||||
Error::InvalidSignature => "Invalid Signature",
|
display("Invalid Issuer")
|
||||||
Error::WrongAlgorithmHeader => "Wrong Algorithm Header",
|
}
|
||||||
|
/// 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> {
|
foreign_links {
|
||||||
Some(match *self {
|
Unspecified(ring::error::Unspecified) #[doc = "An error happened while signing/verifying a token with RSA"];
|
||||||
Error::EncodeJSON(ref err) => err as &error::Error,
|
Base64(base64::Base64Error) #[doc = "An error happened while decoding some base64 text"];
|
||||||
Error::DecodeBase64(ref err) => err as &error::Error,
|
Json(serde_json::Error) #[doc = "An error happened while serializing/deserializing JSON"];
|
||||||
Error::DecodeJSON(ref err) => err as &error::Error,
|
Utf8(::std::string::FromUtf8Error) #[doc = "An error happened while trying to convert the result of base64 decoding to a String"];
|
||||||
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)),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
353
src/lib.rs
353
src/lib.rs
|
@ -1,292 +1,129 @@
|
||||||
//! Create and parses JWT (JSON Web Tokens)
|
//! 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))]
|
#[macro_use]
|
||||||
#![cfg_attr(feature = "dev", feature(plugin))]
|
extern crate error_chain;
|
||||||
#![cfg_attr(feature = "dev", plugin(clippy))]
|
#[macro_use]
|
||||||
|
extern crate serde_derive;
|
||||||
extern crate rustc_serialize;
|
extern crate serde_json;
|
||||||
|
extern crate serde;
|
||||||
|
extern crate base64;
|
||||||
extern crate ring;
|
extern crate ring;
|
||||||
|
extern crate untrusted;
|
||||||
|
extern crate chrono;
|
||||||
|
|
||||||
use ring::{digest, hmac};
|
/// All the errors, generated using error-chain
|
||||||
use ring::constant_time::verify_slices_are_equal;
|
|
||||||
|
|
||||||
use rustc_serialize::{json, Encodable, Decodable};
|
|
||||||
use rustc_serialize::base64::{self, ToBase64, FromBase64};
|
|
||||||
use rustc_serialize::json::{ToJson, Json};
|
|
||||||
|
|
||||||
pub mod errors;
|
pub mod errors;
|
||||||
use errors::Error;
|
mod header;
|
||||||
use std::collections::BTreeMap;
|
mod crypto;
|
||||||
|
mod serialization;
|
||||||
|
mod validation;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Copy, Clone, RustcDecodable, RustcEncodable)]
|
pub use header::{Header};
|
||||||
/// The algorithms supported for signing/verifying
|
pub use crypto::{
|
||||||
pub enum Algorithm {
|
Algorithm,
|
||||||
HS256,
|
sign,
|
||||||
HS384,
|
verify,
|
||||||
HS512
|
};
|
||||||
}
|
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
|
use serde::de::Deserialize;
|
||||||
/// Allows converting from/to struct with base64
|
use serde::ser::Serialize;
|
||||||
pub trait Part {
|
|
||||||
type Encoded: AsRef<str>;
|
|
||||||
|
|
||||||
fn from_base64<B: AsRef<[u8]>>(encoded: B) -> Result<Self, Error> where Self: Sized;
|
use errors::{Result, ErrorKind};
|
||||||
fn to_base64(&self) -> Result<Self::Encoded, Error>;
|
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> {
|
/// Encode the header and claims given and sign the payload using the algorithm from the header and the key
|
||||||
let encoded = try!(json::encode(&self));
|
///
|
||||||
Ok(encoded.as_bytes().to_base64(base64::URL_SAFE))
|
/// ```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> {
|
Ok([signing_input, signature].join("."))
|
||||||
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("."))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Used in decode: takes the result of a rsplit and ensure we only get 2 parts
|
/// Used in decode: takes the result of a rsplit and ensure we only get 2 parts
|
||||||
/// Errors if we don't
|
/// Errors if we don't
|
||||||
macro_rules! expect_two {
|
macro_rules! expect_two {
|
||||||
($iter:expr) => {{
|
($iter:expr) => {{
|
||||||
let mut i = $iter; // evaluate the expr
|
let mut i = $iter;
|
||||||
match (i.next(), i.next(), i.next()) {
|
match (i.next(), i.next(), i.next()) {
|
||||||
(Some(first), Some(second), None) => (first, second),
|
(Some(first), Some(second), None) => (first, second),
|
||||||
_ => return Err(Error::InvalidToken)
|
_ => return Err(ErrorKind::InvalidToken.into())
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decode a token into a Claims struct
|
/// Decode a token into a struct containing 2 fields: `claims` and `header`.
|
||||||
/// If the token or its signature is invalid, it will return an error
|
///
|
||||||
pub fn decode<T: Part>(token: &str, secret: &[u8], algorithm: Algorithm) -> Result<TokenData<T>, Error> {
|
/// If the token or its signature is invalid or the claims fail validation, it will return an error.
|
||||||
let (signature, payload) = expect_two!(token.rsplitn(2, '.'));
|
///
|
||||||
|
/// ```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(
|
if validation.validate_signature && !verify(signature, signing_input, key, algorithm)? {
|
||||||
signature,
|
return Err(ErrorKind::InvalidSignature.into());
|
||||||
payload,
|
|
||||||
secret,
|
|
||||||
algorithm
|
|
||||||
);
|
|
||||||
|
|
||||||
if !is_valid {
|
|
||||||
return Err(Error::InvalidSignature);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
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)]
|
// To consider:
|
||||||
mod tests {
|
//pub mod prelude {
|
||||||
use super::{encode, decode, Algorithm, Header, sign, verify};
|
// pub use crypto::{Algorithm, encode, decode};
|
||||||
|
// pub use validation::Validation;
|
||||||
#[derive(Debug, PartialEq, Clone, RustcEncodable, RustcDecodable)]
|
// pub use header::Header;
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
|
@ -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),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
|
||||||
|
}
|
|
@ -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`
|
Binary file not shown.
|
@ -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-----
|
Binary file not shown.
|
@ -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-----
|
|
@ -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());
|
||||||
|
}
|
Loading…
Reference in New Issue