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]
|
||||
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"
|
||||
|
|
83
README.md
83
README.md
|
@ -6,8 +6,8 @@
|
|||
Add the following to Cargo.toml:
|
||||
|
||||
```toml
|
||||
jsonwebtoken = "1"
|
||||
rustc-serialize = "0.3"
|
||||
jsonwebtoken = "2"
|
||||
serde_derive = "0.9"
|
||||
```
|
||||
|
||||
## How to use
|
||||
|
@ -16,9 +16,10 @@ There is a complete example in `examples/claims.rs` but here's a quick one.
|
|||
In terms of imports:
|
||||
```rust
|
||||
extern crate jsonwebtoken as jwt;
|
||||
extern crate rustc_serialize;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
|
||||
use jwt::{encode, decode, Header, Algorithm};
|
||||
use jwt::{encode, decode, Header, Algorithm, Validation};
|
||||
```
|
||||
|
||||
Look at the examples directory for 2 examples: a basic one and one with a custom
|
||||
|
@ -26,26 +27,45 @@ header.
|
|||
|
||||
### Encoding
|
||||
```rust
|
||||
let token = encode(Header::default(), &my_claims, "secret".as_ref()).unwrap();
|
||||
let token = encode(&Header::default(), &my_claims, "secret".as_ref()).unwrap();
|
||||
```
|
||||
In that example, `my_claims` is an instance of a Claims struct that derives `RustcEncodable` and `RustcDecodable`.
|
||||
In that example, `my_claims` is an instance of a Claims struct that derives `Serialize` and `Deserialize`.
|
||||
The default algorithm is HS256.
|
||||
Look at custom headers section to see how to change that.
|
||||
|
||||
### Decoding
|
||||
```rust
|
||||
let token = decode::<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.
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
|
|
|
@ -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 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!()
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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 rustc_serialize::{json, base64};
|
||||
use base64;
|
||||
use serde_json;
|
||||
use ring;
|
||||
|
||||
#[derive(Debug)]
|
||||
/// All the errors we can encounter while signing/verifying tokens
|
||||
/// and a couple of custom one for when the token we are trying
|
||||
/// to verify is invalid
|
||||
pub enum Error {
|
||||
EncodeJSON(json::EncoderError),
|
||||
DecodeBase64(base64::FromBase64Error),
|
||||
DecodeJSON(json::DecoderError),
|
||||
Utf8(string::FromUtf8Error),
|
||||
|
||||
InvalidToken,
|
||||
InvalidSignature,
|
||||
WrongAlgorithmHeader
|
||||
}
|
||||
|
||||
macro_rules! impl_from_error {
|
||||
($f: ty, $e: expr) => {
|
||||
impl From<$f> for Error {
|
||||
fn from(f: $f) -> Error { $e(f) }
|
||||
error_chain! {
|
||||
errors {
|
||||
/// When a token doesn't have a valid token shape
|
||||
InvalidToken {
|
||||
description("invalid token")
|
||||
display("Invalid token")
|
||||
}
|
||||
/// When the signature doesn't match
|
||||
InvalidSignature {
|
||||
description("invalid signature")
|
||||
display("Invalid signature")
|
||||
}
|
||||
/// When the algorithm in the header doesn't match the one passed to `decode`
|
||||
WrongAlgorithmHeader {
|
||||
description("wrong algorithm header")
|
||||
display("Wrong Algorithm Header")
|
||||
}
|
||||
/// When the secret given is not a valid RSA key
|
||||
InvalidKey {
|
||||
description("invalid key")
|
||||
display("Invalid Key")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl_from_error!(json::EncoderError, Error::EncodeJSON);
|
||||
impl_from_error!(base64::FromBase64Error, Error::DecodeBase64);
|
||||
impl_from_error!(json::DecoderError, Error::DecodeJSON);
|
||||
impl_from_error!(string::FromUtf8Error, Error::Utf8);
|
||||
// Validation error
|
||||
|
||||
impl error::Error for Error {
|
||||
fn description(&self) -> &str {
|
||||
match *self {
|
||||
Error::EncodeJSON(ref err) => err.description(),
|
||||
Error::DecodeBase64(ref err) => err.description(),
|
||||
Error::DecodeJSON(ref err) => err.description(),
|
||||
Error::Utf8(ref err) => err.description(),
|
||||
Error::InvalidToken => "Invalid Token",
|
||||
Error::InvalidSignature => "Invalid Signature",
|
||||
Error::WrongAlgorithmHeader => "Wrong Algorithm Header",
|
||||
/// When a token’s `exp` claim indicates that it has expired
|
||||
ExpiredSignature {
|
||||
description("expired signature")
|
||||
display("Expired Signature")
|
||||
}
|
||||
/// When a token’s `iss` claim does not match the expected issuer
|
||||
InvalidIssuer {
|
||||
description("invalid issuer")
|
||||
display("Invalid Issuer")
|
||||
}
|
||||
/// When a token’s `aud` claim does not match one of the expected audience values
|
||||
InvalidAudience {
|
||||
description("invalid audience")
|
||||
display("Invalid Audience")
|
||||
}
|
||||
/// When a token’s `aud` claim does not match one of the expected audience values
|
||||
InvalidSubject {
|
||||
description("invalid subject")
|
||||
display("Invalid Subject")
|
||||
}
|
||||
/// When a token’s `iat` claim is in the future
|
||||
InvalidIssuedAt {
|
||||
description("invalid issued at")
|
||||
display("Invalid Issued At")
|
||||
}
|
||||
/// When a token’s nbf claim represents a time in the future
|
||||
ImmatureSignature {
|
||||
description("immature signature")
|
||||
display("Immature Signature")
|
||||
}
|
||||
}
|
||||
|
||||
fn cause(&self) -> Option<&error::Error> {
|
||||
Some(match *self {
|
||||
Error::EncodeJSON(ref err) => err as &error::Error,
|
||||
Error::DecodeBase64(ref err) => err as &error::Error,
|
||||
Error::DecodeJSON(ref err) => err as &error::Error,
|
||||
Error::Utf8(ref err) => err as &error::Error,
|
||||
ref e => e as &error::Error,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
Error::EncodeJSON(ref err) => fmt::Display::fmt(err, f),
|
||||
Error::DecodeBase64(ref err) => fmt::Display::fmt(err, f),
|
||||
Error::DecodeJSON(ref err) => fmt::Display::fmt(err, f),
|
||||
Error::Utf8(ref err) => fmt::Display::fmt(err, f),
|
||||
Error::InvalidToken => write!(f, "{}", error::Error::description(self)),
|
||||
Error::InvalidSignature => write!(f, "{}", error::Error::description(self)),
|
||||
Error::WrongAlgorithmHeader => write!(f, "{}", error::Error::description(self)),
|
||||
}
|
||||
foreign_links {
|
||||
Unspecified(ring::error::Unspecified) #[doc = "An error happened while signing/verifying a token with RSA"];
|
||||
Base64(base64::Base64Error) #[doc = "An error happened while decoding some base64 text"];
|
||||
Json(serde_json::Error) #[doc = "An error happened while serializing/deserializing JSON"];
|
||||
Utf8(::std::string::FromUtf8Error) #[doc = "An error happened while trying to convert the result of base64 decoding to a String"];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
//!
|
||||
//! 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;
|
||||
//}
|
||||
|
|
|
@ -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