Merge pull request #91 from Keats/next

7.0.0
This commit is contained in:
Vincent Prouillet 2020-01-28 23:00:26 -08:00 committed by GitHub
commit e89fa41b7a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 1743 additions and 767 deletions

37
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,37 @@
name: ci
on:
push:
branches:
- master
pull_request:
jobs:
tests:
name: Tests
runs-on: ${{ matrix.os }}
strategy:
matrix:
build: [pinned, stable, nightly]
include:
- build: pinned
os: ubuntu-18.04
rust: 1.36.0
- build: stable
os: ubuntu-18.04
rust: stable
- build: nightly
os: ubuntu-18.04
rust: nightly
steps:
- uses: actions/checkout@v1
- name: Install Rust
uses: hecrj/setup-rust-action@v1
with:
rust-version: ${{ matrix.rust }}
- name: Build System Info
run: rustc --version
- name: Run tests
run: cargo test

View File

@ -1,5 +1,15 @@
# Changelog
# 7.0.0 (2020-01-28)
- Add support for PS256, PS384 and PS512
- Add support for verifying with modulus/exponent components for RSA
- Update to 2018 edition
- Changed aud field type in Validation to `Option<HashSet<String>>`. Audience
validation now tests for "any-of-these" audience membership.
- Add support for keys in PEM format
- Add EncodingKey/DecodingKey API to improve performance and UX
## 6.0.1 (2019-05-10)
- Fix Algorithm mapping in FromStr for RSA

View File

@ -1,19 +1,27 @@
[package]
name = "jsonwebtoken"
version = "6.0.1"
authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"]
version = "7.0.0"
authors = ["Vincent Prouillet <hello@vincentprouillet.com>"]
license = "MIT"
readme = "README.md"
description = "Create and parse JWT in a strongly typed way."
homepage = "https://github.com/Keats/rust-jwt"
repository = "https://github.com/Keats/rust-jwt"
keywords = ["jwt", "web", "api", "token", "json"]
description = "Create and decode JWTs in a strongly typed way."
homepage = "https://github.com/Keats/jsonwebtoken"
repository = "https://github.com/Keats/jsonwebtoken"
keywords = ["jwt", "web", "api", "token", "jwk"]
edition = "2018"
[dependencies]
serde_json = "1.0"
serde_derive = "1.0"
serde = "1.0"
ring = "0.14.4"
base64 = "0.10"
untrusted = "0.6"
serde = {version = "1.0", features = ["derive"] }
ring = { version = "0.16.5", features = ["std"] }
base64 = "0.11"
# For PEM decoding
pem = "0.7"
simple_asn1 = "0.4"
[dev-dependencies]
# For the custom chrono example
chrono = "0.4"
[badges]
maintenance = { status = "passively-maintained" }

147
README.md
View File

@ -4,22 +4,32 @@
[API documentation on docs.rs](https://docs.rs/jsonwebtoken/)
See [JSON Web Tokens](https://en.wikipedia.org/wiki/JSON_Web_Token) for more information on what JSON Web Tokens are.
## Installation
Add the following to Cargo.toml:
```toml
jsonwebtoken = "6"
serde_derive = "1"
serde = "1"
jsonwebtoken = "7"
serde = {version = "1.0", features = ["derive"] }
```
## Help wanted for v7
The minimum required Rust version is 1.36.
v6 was released as a stopgap version to update Ring and add a couple of features like ES256/384.
The results are not very ergonomic once we factor in all the possible ways to load a RSA key for example.
A possible solution is to have decoder types as described in https://github.com/Keats/jsonwebtoken/issues/76
but I currently do not have the time to implement it myself.
I will take any better idea as well of course!
## Algorithms
This library currently supports the following:
- HS256
- HS384
- HS512
- RS256
- RS384
- RS512
- PS256
- PS384
- PS512
- ES256
- ES384
## How to use
@ -27,11 +37,8 @@ Complete examples are available in the examples directory: a basic one and one w
In terms of imports and structs:
```rust
extern crate jsonwebtoken as jwt;
#[macro_use]
extern crate serde_derive;
use jwt::{encode, decode, Header, Algorithm, Validation};
use serde::{Serialize, Deserialize};
use jsonwebtoken::{encode, decode, Header, Algorithm, Validation, EncodingKey, DecodingKey};
/// Our claims struct, it needs to derive `Serialize` and/or `Deserialize`
#[derive(Debug, Serialize, Deserialize)]
@ -42,11 +49,11 @@ struct Claims {
}
```
### Encoding
The default algorithm is HS256.
### Header
The default algorithm is HS256, which uses a shared secret.
```rust
let token = encode(&Header::default(), &my_claims, "secret".as_ref())?;
let token = encode(&Header::default(), &my_claims, &EncodingKey::from_secret("secret".as_ref()))?;
```
#### Custom headers & changing algorithm
@ -54,38 +61,90 @@ All the parameters from the RFC are supported but the default header only has `t
If you want to set the `kid` parameter or change the algorithm for example:
```rust
let mut header = Header::default();
let mut header = Header::new(Algorithm::HS512);
header.kid = Some("blabla".to_owned());
header.alg = Algorithm::HS512;
let token = encode(&header, &my_claims, "secret".as_ref())?;
let token = encode(&header, &my_claims, &EncodingKey::from_secret("secret".as_ref()))?;
```
Look at `examples/custom_header.rs` for a full working example.
### Decoding
### Encoding
```rust
let token = decode::<Claims>(&token, "secret".as_ref(), &Validation::default())?;
// token is a struct with 2 params: header and claims
// HS256
let token = encode(&Header::default(), &my_claims, &EncodingKey::from_secret("secret".as_ref()))?;
// RSA
let token = encode(&Header::new(Algorithm::RS256), &my_claims, &EncodingKey::from_rsa_pem(include_bytes!("privkey.pem"))?)?;
```
Encoding a JWT takes 3 parameters:
- a header: the `Header` struct
- some claims: your own struct
- a key/secret
When using HS256, HS2384 or HS512, the key is always a shared secret like in the example above. When using
RSA/EC, the key should always be the content of the private key in the PEM or DER format.
If your key is in PEM format, it is better performance wise to generate the `EncodingKey` once in a `lazy_static` or
something similar and reuse it.
### Decoding
```rust
// `token` is a struct with 2 fields: `header` and `claims` where `claims` is your own struct.
let token = decode::<Claims>(&token, &DecodingKey::from_secret("secret".as_ref()), &Validation::default())?;
```
`decode` can error for a variety of reasons:
- the token or its signature is invalid
- error while decoding base64 or the result of decoding base64 is not valid UTF-8
- the token had invalid base64
- validation of at least one reserved claim failed
In some cases, for example if you don't know the algorithm used, you will want to only decode the header:
As with encoding, when using HS256, HS2384 or HS512, the key is always a shared secret like in the example above. When using
RSA/EC, the key should always be the content of the public key in the PEM or DER format.
In some cases, for example if you don't know the algorithm used or need to grab the `kid`, you can choose to decode only the header:
```rust
let header = decode_header(&token)?;
```
This does not perform any validation on the token.
This does not perform any signature verification or validate the token claims.
#### Validation
This library validates automatically the `exp` and `nbf` claims if present. You can also validate the `sub`, `iss` and `aud` but
You can also decode a token using the public key components of a RSA key in base64 format.
The main use-case is for JWK where your public key is in a JSON format like so:
```json
{
"kty":"RSA",
"e":"AQAB",
"kid":"6a7a119f-0876-4f7e-8d0f-bf3ea1391dd8",
"n":"yRE6rHuNR0QbHO3H3Kt2pOKGVhQqGZXInOduQNxXzuKlvQTLUTv4l4sggh5_CYYi_cvI-SXVT9kPWSKXxJXBXd_4LkvcPuUakBoAkfh-eiFVMh2VrUyWyj3MFl0HTVF9KwRXLAcwkREiS3npThHRyIxuy0ZMeZfxVL5arMhw1SRELB8HoGfG_AtH89BIE9jDBHZ9dLelK9a184zAf8LwoPLxvJb3Il5nncqPcSfKDDodMFBIMc4lQzDKL5gvmiXLXB1AGLm8KBjfE8s3L5xqi-yUod-j8MtvIj812dkS4QMiRVN_by2h3ZY8LYVGrqZXZTcgn2ujn8uKjXLZVD5TdQ"
}
```
```rust
// `token` is a struct with 2 fields: `header` and `claims` where `claims` is your own struct.
let token = decode::<Claims>(&token, &DecodingKey::from_rsa_components(jwk["n"], jwk["e"]), &Validation::new(Algorithm::RS256))?;
```
If your key is in PEM format, it is better performance wise to generate the `DecodingKey` once in a `lazy_static` or
something similar and reuse it.
### Convert SEC1 private key to PKCS8
`jsonwebtoken` currently only supports PKCS8 format for private EC keys. If your key has `BEGIN EC PRIVATE KEY` at the top,
this is a SEC1 type and can be converted to PKCS8 like so:
```bash
openssl pkcs8 -topk8 -nocrypt -in sec1.pem -out pkcs8.pem
```
## Validation
This library validates automatically the `exp` claim and `nbf` is validated if present. You can also validate the `sub`, `iss` and `aud` but
those require setting the expected value in the `Validation` struct.
Since validating time fields is always a bit tricky due to clock skew,
you can add some leeway to the `iat`, `exp` and `nbf` validation by setting a `leeway` parameter.
Since validating time fields is always a bit tricky due to clock skew,
you can add some leeway to the `iat`, `exp` and `nbf` validation by setting the `leeway` field.
Last but not least, you will need to set the algorithm(s) allowed for this token if you are not using `HS256`.
@ -106,30 +165,4 @@ validation.set_audience(&"Me"); // string
validation.set_audience(&["Me", "You"]); // array of strings
```
## Algorithms
This library currently supports the following:
- HS256
- HS384
- HS512
- RS256
- RS384
- RS512
- ES256
- ES384
### 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 PKCS#1 (ie with `BEGIN RSA PUBLIC KEY`) .pem.
If you have a PKCS#8 pem file (ie starting with `BEGIN PUBLIC KEY`), you will need to first convert it to PKCS#1:
`openssl rsa -pubin -in <filename> -RSAPublicKey_out -out <filename>`.
```bash
// private key
$ openssl rsa -in private_rsa_key.pem -outform DER -out private_rsa_key.der
// public key
$ openssl rsa -in private_rsa_key.der -inform DER -RSAPublicKey_out -outform DER -out public_key.der
```
If you are getting an error with your public key, make sure you get it by using the command above to ensure
it is in the right format.
Look at `examples/validation.rs` for a full working example.

View File

@ -1,10 +1,8 @@
#![feature(test)]
extern crate jsonwebtoken as jwt;
extern crate test;
#[macro_use]
extern crate serde_derive;
use jwt::{decode, encode, Header, Validation};
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
struct Claims {
@ -15,12 +13,15 @@ struct Claims {
#[bench]
fn bench_encode(b: &mut test::Bencher) {
let claim = Claims { sub: "b@b.com".to_owned(), company: "ACME".to_owned() };
let key = EncodingKey::from_secret("secret".as_ref());
b.iter(|| encode(&Header::default(), &claim, "secret".as_ref()));
b.iter(|| encode(&Header::default(), &claim, &key));
}
#[bench]
fn bench_decode(b: &mut test::Bencher) {
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ";
b.iter(|| decode::<Claims>(token, "secret".as_ref(), &Validation::default()));
let key = DecodingKey::from_secret("secret".as_ref());
b.iter(|| decode::<Claims>(token, &key, &Validation::default()));
}

106
examples/custom_chrono.rs Normal file
View File

@ -0,0 +1,106 @@
use chrono::prelude::*;
use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
const SECRET: &str = "some-secret";
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct Claims {
sub: String,
#[serde(with = "jwt_numeric_date")]
iat: DateTime<Utc>,
#[serde(with = "jwt_numeric_date")]
exp: DateTime<Utc>,
}
mod jwt_numeric_date {
//! Custom serialization of DateTime<Utc> to conform with the JWT spec (RFC 7519 section 2, "Numeric Date")
use chrono::{DateTime, TimeZone, Utc};
use serde::{self, Deserialize, Deserializer, Serializer};
/// Serializes a DateTime<Utc> to a Unix timestamp (milliseconds since 1970/1/1T00:00:00T)
pub fn serialize<S>(date: &DateTime<Utc>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let timestamp = date.timestamp();
serializer.serialize_i64(timestamp)
}
/// Attempts to deserialize an i64 and use as a Unix timestamp
pub fn deserialize<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
where
D: Deserializer<'de>,
{
Utc.timestamp_opt(i64::deserialize(deserializer)?, 0)
.single() // If there are multiple or no valid DateTimes from timestamp, return None
.ok_or_else(|| serde::de::Error::custom("invalid Unix timestamp value"))
}
#[cfg(test)]
mod tests {
const EXPECTED_TOKEN: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJDdXN0b20gRGF0ZVRpbWUgc2VyL2RlIiwiaWF0IjowLCJleHAiOjMyNTAzNjgwMDAwfQ.RTgha0S53MjPC2pMA4e2oMzaBxSY3DMjiYR2qFfV55A";
use super::super::{Claims, SECRET};
#[test]
fn round_trip() {
let sub = "Custom DateTime ser/de".to_string();
let iat = Utc.timestamp(0, 0);
let exp = Utc.timestamp(32503680000, 0);
let claims = Claims { sub: sub.clone(), iat, exp };
let token =
encode(&Header::default(), &claims, &EncodingKey::from_secret(SECRET.as_ref()))
.expect("Failed to encode claims");
assert_eq!(&token, EXPECTED_TOKEN);
let decoded = decode::<Claims>(
&token,
&DecodingKey::from_secret(SECRET.as_ref()),
&Validation::default(),
)
.expect("Failed to decode token");
assert_eq!(decoded.claims, claims);
}
#[test]
fn should_fail_on_invalid_timestamp() {
// A token with the expiry of i64::MAX + 1
let overflow_token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJDdXN0b20gRGF0ZVRpbWUgc2VyL2RlIiwiaWF0IjowLCJleHAiOjkyMjMzNzIwMzY4NTQ3NzYwMDB9.G2PKreA27U8_xOwuIeCYXacFYeR46f9FyENIZfCrvEc";
let decode_result =
decode::<Claims>(&overflow_token, SECRET.as_ref(), &Validation::default());
assert!(decode_result.is_err());
}
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let sub = "Custom DateTime ser/de".to_string();
let iat = Utc::now();
let exp = iat + chrono::Duration::days(1);
let claims = Claims { sub: sub.clone(), iat, exp };
let token = jsonwebtoken::encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(SECRET.as_ref()),
)?;
println!("serialized token: {}", &token);
let token_data = jsonwebtoken::decode::<Claims>(
&token,
&DecodingKey::from_secret(SECRET.as_ref()),
&Validation::default(),
)?;
println!("token data:\n{:#?}", &token_data);
Ok(())
}

View File

@ -1,9 +1,7 @@
extern crate jsonwebtoken as jwt;
#[macro_use]
extern crate serde_derive;
use serde::{Deserialize, Serialize};
use jwt::errors::ErrorKind;
use jwt::{decode, encode, Algorithm, Header, Validation};
use jsonwebtoken::errors::ErrorKind;
use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
@ -15,26 +13,29 @@ struct Claims {
fn main() {
let my_claims =
Claims { sub: "b@b.com".to_owned(), company: "ACME".to_owned(), exp: 10000000000 };
let key = "secret";
let key = b"secret";
let mut header = Header::default();
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, &EncodingKey::from_secret(key)) {
Ok(t) => t,
Err(_) => panic!(), // in practice you would return the error
};
println!("{:?}", token);
let token_data =
match decode::<Claims>(&token, key.as_ref(), &Validation::new(Algorithm::HS512)) {
Ok(c) => c,
Err(err) => match *err.kind() {
ErrorKind::InvalidToken => panic!(), // Example on how to handle a specific error
_ => panic!(),
},
};
let token_data = match decode::<Claims>(
&token,
&DecodingKey::from_secret(key),
&Validation::new(Algorithm::HS512),
) {
Ok(c) => c,
Err(err) => match *err.kind() {
ErrorKind::InvalidToken => panic!(), // Example on how to handle a specific error
_ => panic!(),
},
};
println!("{:?}", token_data.claims);
println!("{:?}", token_data.header);
}

View File

@ -1,9 +1,6 @@
extern crate jsonwebtoken as jwt;
#[macro_use]
extern crate serde_derive;
use jwt::errors::ErrorKind;
use jwt::{decode, encode, Header, Validation};
use jsonwebtoken::errors::ErrorKind;
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
@ -13,16 +10,16 @@ struct Claims {
}
fn main() {
let key = b"secret";
let my_claims =
Claims { sub: "b@b.com".to_owned(), company: "ACME".to_owned(), exp: 10000000000 };
let key = "secret";
let token = match encode(&Header::default(), &my_claims, key.as_ref()) {
let token = match encode(&Header::default(), &my_claims, &EncodingKey::from_secret(key)) {
Ok(t) => t,
Err(_) => panic!(), // in practice you would return the error
};
let validation = Validation { sub: Some("b@b.com".to_string()), ..Validation::default() };
let token_data = match decode::<Claims>(&token, key.as_ref(), &validation) {
let token_data = match decode::<Claims>(&token, &DecodingKey::from_secret(key), &validation) {
Ok(c) => c,
Err(err) => match *err.kind() {
ErrorKind::InvalidToken => panic!("Token is invalid"), // Example on how to handle a specific error

100
src/algorithms.rs Normal file
View File

@ -0,0 +1,100 @@
use crate::errors::{Error, ErrorKind, Result};
use serde::{Deserialize, Serialize};
use std::str::FromStr;
#[derive(Debug, Eq, PartialEq, Copy, Clone, Serialize, Deserialize)]
pub(crate) enum AlgorithmFamily {
Hmac,
Rsa,
Ec,
}
/// The algorithms supported for signing/verifying JWTs
#[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,
/// ECDSA using SHA-256
ES256,
/// ECDSA using SHA-384
ES384,
/// RSASSA-PKCS1-v1_5 using SHA-256
RS256,
/// RSASSA-PKCS1-v1_5 using SHA-384
RS384,
/// RSASSA-PKCS1-v1_5 using SHA-512
RS512,
/// RSASSA-PSS using SHA-256
PS256,
/// RSASSA-PSS using SHA-384
PS384,
/// RSASSA-PSS using SHA-512
PS512,
}
impl Default for Algorithm {
fn default() -> Self {
Algorithm::HS256
}
}
impl FromStr for Algorithm {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
match s {
"HS256" => Ok(Algorithm::HS256),
"HS384" => Ok(Algorithm::HS384),
"HS512" => Ok(Algorithm::HS512),
"ES256" => Ok(Algorithm::ES256),
"ES384" => Ok(Algorithm::ES384),
"RS256" => Ok(Algorithm::RS256),
"RS384" => Ok(Algorithm::RS384),
"PS256" => Ok(Algorithm::PS256),
"PS384" => Ok(Algorithm::PS384),
"PS512" => Ok(Algorithm::PS512),
"RS512" => Ok(Algorithm::RS512),
_ => Err(ErrorKind::InvalidAlgorithmName.into()),
}
}
}
impl Algorithm {
pub(crate) fn family(self) -> AlgorithmFamily {
match self {
Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => AlgorithmFamily::Hmac,
Algorithm::RS256
| Algorithm::RS384
| Algorithm::RS512
| Algorithm::PS256
| Algorithm::PS384
| Algorithm::PS512 => AlgorithmFamily::Rsa,
Algorithm::ES256 | Algorithm::ES384 => AlgorithmFamily::Ec,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generate_algorithm_enum_from_str() {
assert!(Algorithm::from_str("HS256").is_ok());
assert!(Algorithm::from_str("HS384").is_ok());
assert!(Algorithm::from_str("HS512").is_ok());
assert!(Algorithm::from_str("RS256").is_ok());
assert!(Algorithm::from_str("RS384").is_ok());
assert!(Algorithm::from_str("RS512").is_ok());
assert!(Algorithm::from_str("PS256").is_ok());
assert!(Algorithm::from_str("PS384").is_ok());
assert!(Algorithm::from_str("PS512").is_ok());
assert!(Algorithm::from_str("").is_err());
}
}

View File

@ -1,162 +0,0 @@
use std::sync::Arc;
use base64;
use ring::constant_time::verify_slices_are_equal;
use ring::{digest, hmac, rand, signature};
use std::str::FromStr;
use untrusted;
use errors::{new_error, Error, ErrorKind, Result};
/// 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,
/// ECDSA using SHA-256
ES256,
/// ECDSA using SHA-384
ES384,
/// RSASSA-PKCS1-v1_5 using SHA-256
RS256,
/// RSASSA-PKCS1-v1_5 using SHA-384
RS384,
/// RSASSA-PKCS1-v1_5 using SHA-512
RS512,
}
impl Default for Algorithm {
fn default() -> Self {
Algorithm::HS256
}
}
impl FromStr for Algorithm {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
match s {
"HS256" => Ok(Algorithm::HS256),
"HS384" => Ok(Algorithm::HS384),
"HS512" => Ok(Algorithm::HS512),
"ES256" => Ok(Algorithm::ES256),
"ES384" => Ok(Algorithm::ES384),
"RS256" => Ok(Algorithm::RS256),
"RS384" => Ok(Algorithm::RS384),
"RS512" => Ok(Algorithm::RS512),
_ => Err(new_error(ErrorKind::InvalidAlgorithmName)),
}
}
}
/// 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);
let digest = hmac::sign(&signing_key, signing_input.as_bytes());
Ok(base64::encode_config::<hmac::Signature>(&digest, base64::URL_SAFE_NO_PAD))
}
/// The actual ECDSA signing + encoding
fn sign_ecdsa(alg: &'static signature::EcdsaSigningAlgorithm, key: &[u8], signing_input: &str) -> Result<String> {
let signing_key = signature::EcdsaKeyPair::from_pkcs8(alg, untrusted::Input::from(key))?;
let rng = rand::SystemRandom::new();
let sig = signing_key.sign(&rng, untrusted::Input::from(signing_input.as_bytes()))?;
Ok(base64::encode_config(&sig, 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: &'static signature::RsaEncoding, key: &[u8], signing_input: &str) -> Result<String> {
let key_pair = Arc::new(
signature::RsaKeyPair::from_der(untrusted::Input::from(key))
.map_err(|_| ErrorKind::InvalidRsaKey)?,
);
let mut signature = vec![0; key_pair.public_modulus_len()];
let rng = rand::SystemRandom::new();
key_pair
.sign(alg, &rng, signing_input.as_bytes(), &mut signature)
.map_err(|_| ErrorKind::InvalidRsaKey)?;
Ok(base64::encode_config::<[u8]>(&signature, 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::ES256 => sign_ecdsa(&signature::ECDSA_P256_SHA256_FIXED_SIGNING, key, signing_input),
Algorithm::ES384 => sign_ecdsa(&signature::ECDSA_P384_SHA384_FIXED_SIGNING, key, signing_input),
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 docs for more details
fn verify_ring(
alg: &dyn signature::VerificationAlgorithm,
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::ES256 => {
verify_ring(&signature::ECDSA_P256_SHA256_FIXED, signature, signing_input, key)
}
Algorithm::ES384 => {
verify_ring(&signature::ECDSA_P384_SHA384_FIXED, signature, signing_input, key)
}
Algorithm::RS256 => {
verify_ring(&signature::RSA_PKCS1_2048_8192_SHA256, signature, signing_input, key)
}
Algorithm::RS384 => {
verify_ring(&signature::RSA_PKCS1_2048_8192_SHA384, signature, signing_input, key)
}
Algorithm::RS512 => {
verify_ring(&signature::RSA_PKCS1_2048_8192_SHA512, signature, signing_input, key)
}
}
}

38
src/crypto/ecdsa.rs Normal file
View File

@ -0,0 +1,38 @@
use ring::{rand, signature};
use crate::algorithms::Algorithm;
use crate::errors::Result;
use crate::serialization::b64_encode;
/// Only used internally when validating EC, to map from our enum to the Ring EcdsaVerificationAlgorithm structs.
pub(crate) fn alg_to_ec_verification(
alg: Algorithm,
) -> &'static signature::EcdsaVerificationAlgorithm {
match alg {
Algorithm::ES256 => &signature::ECDSA_P256_SHA256_FIXED,
Algorithm::ES384 => &signature::ECDSA_P384_SHA384_FIXED,
_ => unreachable!("Tried to get EC alg for a non-EC algorithm"),
}
}
/// Only used internally when signing EC, to map from our enum to the Ring EcdsaVerificationAlgorithm structs.
pub(crate) fn alg_to_ec_signing(alg: Algorithm) -> &'static signature::EcdsaSigningAlgorithm {
match alg {
Algorithm::ES256 => &signature::ECDSA_P256_SHA256_FIXED_SIGNING,
Algorithm::ES384 => &signature::ECDSA_P384_SHA384_FIXED_SIGNING,
_ => unreachable!("Tried to get EC alg for a non-EC algorithm"),
}
}
/// The actual ECDSA signing + encoding
/// The key needs to be in PKCS8 format
pub fn sign(
alg: &'static signature::EcdsaSigningAlgorithm,
key: &[u8],
message: &str,
) -> Result<String> {
let signing_key = signature::EcdsaKeyPair::from_pkcs8(alg, key)?;
let rng = rand::SystemRandom::new();
let out = signing_key.sign(&rng, message.as_bytes())?;
Ok(b64_encode(out.as_ref()))
}

98
src/crypto/mod.rs Normal file
View File

@ -0,0 +1,98 @@
use ring::constant_time::verify_slices_are_equal;
use ring::{hmac, signature};
use crate::algorithms::Algorithm;
use crate::decoding::{DecodingKey, DecodingKeyKind};
use crate::encoding::EncodingKey;
use crate::errors::Result;
use crate::serialization::{b64_decode, b64_encode};
pub(crate) mod ecdsa;
pub(crate) mod rsa;
/// The actual HS signing + encoding
/// Could be in its own file to match RSA/EC but it's 2 lines...
pub(crate) fn sign_hmac(alg: hmac::Algorithm, key: &[u8], message: &str) -> Result<String> {
let digest = hmac::sign(&hmac::Key::new(alg, key), message.as_bytes());
Ok(b64_encode(digest.as_ref()))
}
/// Take the payload of a JWT, sign it using the algorithm given and return
/// the base64 url safe encoded of the result.
///
/// If you just want to encode a JWT, use `encode` instead.
pub fn sign(message: &str, key: &EncodingKey, algorithm: Algorithm) -> Result<String> {
match algorithm {
Algorithm::HS256 => sign_hmac(hmac::HMAC_SHA256, key.inner(), message),
Algorithm::HS384 => sign_hmac(hmac::HMAC_SHA384, key.inner(), message),
Algorithm::HS512 => sign_hmac(hmac::HMAC_SHA512, key.inner(), message),
Algorithm::ES256 | Algorithm::ES384 => {
ecdsa::sign(ecdsa::alg_to_ec_signing(algorithm), key.inner(), message)
}
Algorithm::RS256
| Algorithm::RS384
| Algorithm::RS512
| Algorithm::PS256
| Algorithm::PS384
| Algorithm::PS512 => rsa::sign(rsa::alg_to_rsa_signing(algorithm), key.inner(), message),
}
}
/// See Ring docs for more details
fn verify_ring(
alg: &'static dyn signature::VerificationAlgorithm,
signature: &str,
message: &str,
key: &[u8],
) -> Result<bool> {
let signature_bytes = b64_decode(signature)?;
let public_key = signature::UnparsedPublicKey::new(alg, key);
let res = public_key.verify(message.as_bytes(), &signature_bytes);
Ok(res.is_ok())
}
/// Compares the signature given with a re-computed signature for HMAC or using the public key
/// for RSA/EC.
///
/// If you just want to decode a JWT, use `decode` instead.
///
/// `signature` is the signature part of a jwt (text after the second '.')
///
/// `message` is base64(header) + "." + base64(claims)
pub fn verify(
signature: &str,
message: &str,
key: &DecodingKey,
algorithm: Algorithm,
) -> Result<bool> {
match algorithm {
Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => {
// we just re-sign the message with the key and compare if they are equal
let signed = sign(message, &EncodingKey::from_secret(key.as_bytes()), algorithm)?;
Ok(verify_slices_are_equal(signature.as_ref(), signed.as_ref()).is_ok())
}
Algorithm::ES256 | Algorithm::ES384 => verify_ring(
ecdsa::alg_to_ec_verification(algorithm),
signature,
message,
key.as_bytes(),
),
Algorithm::RS256
| Algorithm::RS384
| Algorithm::RS512
| Algorithm::PS256
| Algorithm::PS384
| Algorithm::PS512 => {
let alg = rsa::alg_to_rsa_parameters(algorithm);
match &key.kind {
DecodingKeyKind::SecretOrDer(bytes) => verify_ring(alg, signature, message, bytes),
DecodingKeyKind::RsaModulusExponent { n, e } => {
rsa::verify_from_components(alg, signature, message, (n, e))
}
}
}
}
}

66
src/crypto/rsa.rs Normal file
View File

@ -0,0 +1,66 @@
use ring::{rand, signature};
use simple_asn1::BigUint;
use crate::algorithms::Algorithm;
use crate::errors::{ErrorKind, Result};
use crate::serialization::{b64_decode, b64_encode};
/// Only used internally when validating RSA, to map from our enum to the Ring param structs.
pub(crate) fn alg_to_rsa_parameters(alg: Algorithm) -> &'static signature::RsaParameters {
match alg {
Algorithm::RS256 => &signature::RSA_PKCS1_2048_8192_SHA256,
Algorithm::RS384 => &signature::RSA_PKCS1_2048_8192_SHA384,
Algorithm::RS512 => &signature::RSA_PKCS1_2048_8192_SHA512,
Algorithm::PS256 => &signature::RSA_PSS_2048_8192_SHA256,
Algorithm::PS384 => &signature::RSA_PSS_2048_8192_SHA384,
Algorithm::PS512 => &signature::RSA_PSS_2048_8192_SHA512,
_ => unreachable!("Tried to get RSA signature for a non-rsa algorithm"),
}
}
/// Only used internally when signing with RSA, to map from our enum to the Ring signing structs.
pub(crate) fn alg_to_rsa_signing(alg: Algorithm) -> &'static dyn signature::RsaEncoding {
match alg {
Algorithm::RS256 => &signature::RSA_PKCS1_SHA256,
Algorithm::RS384 => &signature::RSA_PKCS1_SHA384,
Algorithm::RS512 => &signature::RSA_PKCS1_SHA512,
Algorithm::PS256 => &signature::RSA_PSS_SHA256,
Algorithm::PS384 => &signature::RSA_PSS_SHA384,
Algorithm::PS512 => &signature::RSA_PSS_SHA512,
_ => unreachable!("Tried to get RSA signature for a non-rsa algorithm"),
}
}
/// The actual RSA signing + encoding
/// The key needs to be in PKCS8 format
/// Taken from Ring doc https://briansmith.org/rustdoc/ring/signature/index.html
pub(crate) fn sign(
alg: &'static dyn signature::RsaEncoding,
key: &[u8],
message: &str,
) -> Result<String> {
let key_pair = signature::RsaKeyPair::from_der(key).map_err(|_| ErrorKind::InvalidRsaKey)?;
let mut signature = vec![0; key_pair.public_modulus_len()];
let rng = rand::SystemRandom::new();
key_pair
.sign(alg, &rng, message.as_bytes(), &mut signature)
.map_err(|_| ErrorKind::InvalidRsaKey)?;
Ok(b64_encode(&signature))
}
/// Checks that a signature is valid based on the (n, e) RSA pubkey components
pub(crate) fn verify_from_components(
alg: &'static signature::RsaParameters,
signature: &str,
message: &str,
components: (&str, &str),
) -> Result<bool> {
let signature_bytes = b64_decode(signature)?;
let n = BigUint::from_bytes_be(&b64_decode(components.0)?).to_bytes_be();
let e = BigUint::from_bytes_be(&b64_decode(components.1)?).to_bytes_be();
let pubkey = signature::RsaPublicKeyComponents { n, e };
let res = pubkey.verify(alg, message.as_ref(), &signature_bytes);
Ok(res.is_ok())
}

207
src/decoding.rs Normal file
View File

@ -0,0 +1,207 @@
use std::borrow::Cow;
use serde::de::DeserializeOwned;
use crate::algorithms::AlgorithmFamily;
use crate::crypto::verify;
use crate::errors::{new_error, ErrorKind, Result};
use crate::header::Header;
use crate::pem::decoder::PemEncodedKey;
use crate::serialization::from_jwt_part_claims;
use crate::validation::{validate, Validation};
/// The return type of a successful call to [decode](fn.decode.html).
#[derive(Debug)]
pub struct TokenData<T> {
/// The decoded JWT header
pub header: Header,
/// The decoded JWT claims
pub claims: T,
}
/// Takes the result of a rsplit and ensure we only get 2 parts
/// Errors if we don't
macro_rules! expect_two {
($iter:expr) => {{
let mut i = $iter;
match (i.next(), i.next(), i.next()) {
(Some(first), Some(second), None) => (first, second),
_ => return Err(new_error(ErrorKind::InvalidToken)),
}
}};
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum DecodingKeyKind<'a> {
SecretOrDer(Cow<'a, [u8]>),
RsaModulusExponent { n: &'a str, e: &'a str },
}
/// All the different kind of keys we can use to decode a JWT
/// This key can be re-used so make sure you only initialize it once if you can for better performance
#[derive(Debug, Clone, PartialEq)]
pub struct DecodingKey<'a> {
pub(crate) family: AlgorithmFamily,
pub(crate) kind: DecodingKeyKind<'a>,
}
impl<'a> DecodingKey<'a> {
/// If you're using HMAC, use this.
pub fn from_secret(secret: &'a [u8]) -> Self {
DecodingKey {
family: AlgorithmFamily::Hmac,
kind: DecodingKeyKind::SecretOrDer(Cow::Borrowed(secret)),
}
}
/// If you're using HMAC with a base64 encoded, use this.
pub fn from_base64_secret(secret: &str) -> Result<Self> {
let out = base64::decode(&secret)?;
Ok(DecodingKey {
family: AlgorithmFamily::Hmac,
kind: DecodingKeyKind::SecretOrDer(Cow::Owned(out)),
})
}
/// If you are loading a public RSA key in a PEM format, use this.
pub fn from_rsa_pem(key: &'a [u8]) -> Result<Self> {
let pem_key = PemEncodedKey::new(key)?;
let content = pem_key.as_rsa_key()?;
Ok(DecodingKey {
family: AlgorithmFamily::Rsa,
kind: DecodingKeyKind::SecretOrDer(Cow::Owned(content.to_vec())),
})
}
/// If you have (n, e) RSA public key components, use this.
pub fn from_rsa_components(modulus: &'a str, exponent: &'a str) -> Self {
DecodingKey {
family: AlgorithmFamily::Rsa,
kind: DecodingKeyKind::RsaModulusExponent { n: modulus, e: exponent },
}
}
/// If you have a ECDSA public key in PEM format, use this.
pub fn from_ec_pem(key: &'a [u8]) -> Result<Self> {
let pem_key = PemEncodedKey::new(key)?;
let content = pem_key.as_ec_public_key()?;
Ok(DecodingKey {
family: AlgorithmFamily::Ec,
kind: DecodingKeyKind::SecretOrDer(Cow::Owned(content.to_vec())),
})
}
/// If you know what you're doing and have a RSA DER encoded public key, use this.
pub fn from_rsa_der(der: &'a [u8]) -> Self {
DecodingKey {
family: AlgorithmFamily::Rsa,
kind: DecodingKeyKind::SecretOrDer(Cow::Borrowed(der)),
}
}
/// If you know what you're doing and have a RSA EC encoded public key, use this.
pub fn from_ec_der(der: &'a [u8]) -> Self {
DecodingKey {
family: AlgorithmFamily::Ec,
kind: DecodingKeyKind::SecretOrDer(Cow::Borrowed(der)),
}
}
pub(crate) fn as_bytes(&self) -> &[u8] {
match &self.kind {
DecodingKeyKind::SecretOrDer(b) => &b,
DecodingKeyKind::RsaModulusExponent { .. } => unreachable!(),
}
}
}
/// Decode and validate a JWT
///
/// If the token or its signature is invalid or the claims fail validation, it will return an error.
///
/// ```rust
/// use serde::{Deserialize, Serialize};
/// use jsonwebtoken::{decode, DecodingKey, Validation, Algorithm};
///
/// #[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_message = decode::<Claims>(&token, &DecodingKey::from_secret("secret".as_ref()), &Validation::new(Algorithm::HS256));
/// ```
pub fn decode<T: DeserializeOwned>(
token: &str,
key: &DecodingKey,
validation: &Validation,
) -> Result<TokenData<T>> {
for alg in &validation.algorithms {
if key.family != alg.family() {
return Err(new_error(ErrorKind::InvalidAlgorithm));
}
}
let (signature, message) = expect_two!(token.rsplitn(2, '.'));
let (claims, header) = expect_two!(message.rsplitn(2, '.'));
let header = Header::from_encoded(header)?;
if !validation.algorithms.contains(&header.alg) {
return Err(new_error(ErrorKind::InvalidAlgorithm));
}
if !verify(signature, message, key, header.alg)? {
return Err(new_error(ErrorKind::InvalidSignature));
}
let (decoded_claims, claims_map): (T, _) = from_jwt_part_claims(claims)?;
validate(&claims_map, validation)?;
Ok(TokenData { header, claims: decoded_claims })
}
/// Decode a JWT without any signature verification/validations.
///
/// NOTE: Do not use this unless you know what you are doing! If the token's signature is invalid, it will *not* return an error.
///
/// ```rust
/// use serde::{Deserialize, Serialize};
/// use jsonwebtoken::{dangerous_unsafe_decode, Validation, Algorithm};
///
/// #[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_message = dangerous_unsafe_decode::<Claims>(&token);
/// ```
pub fn dangerous_unsafe_decode<T: DeserializeOwned>(token: &str) -> Result<TokenData<T>> {
let (_, message) = expect_two!(token.rsplitn(2, '.'));
let (claims, header) = expect_two!(message.rsplitn(2, '.'));
let header = Header::from_encoded(header)?;
let (decoded_claims, _): (T, _) = from_jwt_part_claims(claims)?;
Ok(TokenData { header, claims: decoded_claims })
}
/// Decode a JWT without any signature verification/validations and return its [Header](struct.Header.html).
///
/// If the token has an invalid format (ie 3 parts separated by a `.`), it will return an error.
///
/// ```rust
/// use jsonwebtoken::decode_header;
///
/// let token = "a.jwt.token".to_string();
/// let header = decode_header(&token);
/// ```
pub fn decode_header(token: &str) -> Result<Header> {
let (_, message) = expect_two!(token.rsplitn(2, '.'));
let (_, header) = expect_two!(message.rsplitn(2, '.'));
Header::from_encoded(header)
}

93
src/encoding.rs Normal file
View File

@ -0,0 +1,93 @@
use serde::ser::Serialize;
use crate::algorithms::AlgorithmFamily;
use crate::crypto;
use crate::errors::{new_error, ErrorKind, Result};
use crate::header::Header;
use crate::pem::decoder::PemEncodedKey;
use crate::serialization::b64_encode_part;
/// A key to encode a JWT with. Can be a secret, a PEM-encoded key or a DER-encoded key.
/// This key can be re-used so make sure you only initialize it once if you can for better performance
#[derive(Debug, Clone, PartialEq)]
pub struct EncodingKey {
pub(crate) family: AlgorithmFamily,
content: Vec<u8>,
}
impl EncodingKey {
/// If you're using a HMAC secret that is not base64, use that.
pub fn from_secret(secret: &[u8]) -> Self {
EncodingKey { family: AlgorithmFamily::Hmac, content: secret.to_vec() }
}
/// If you have a base64 HMAC secret, use that.
pub fn from_base64_secret(secret: &str) -> Result<Self> {
let out = base64::decode(&secret)?;
Ok(EncodingKey { family: AlgorithmFamily::Hmac, content: out })
}
/// If you are loading a RSA key from a .pem file.
/// This errors if the key is not a valid RSA key.
pub fn from_rsa_pem(key: &[u8]) -> Result<Self> {
let pem_key = PemEncodedKey::new(key)?;
let content = pem_key.as_rsa_key()?;
Ok(EncodingKey { family: AlgorithmFamily::Rsa, content: content.to_vec() })
}
/// If you are loading a ECDSA key from a .pem file
/// This errors if the key is not a valid private EC key
pub fn from_ec_pem(key: &[u8]) -> Result<Self> {
let pem_key = PemEncodedKey::new(key)?;
let content = pem_key.as_ec_private_key()?;
Ok(EncodingKey { family: AlgorithmFamily::Ec, content: content.to_vec() })
}
/// If you know what you're doing and have the DER-encoded key, for RSA only
pub fn from_rsa_der(der: &[u8]) -> Self {
EncodingKey { family: AlgorithmFamily::Rsa, content: der.to_vec() }
}
/// If you know what you're doing and have the DER-encoded key, for ECDSA
pub fn from_ec_der(der: &[u8]) -> Self {
EncodingKey { family: AlgorithmFamily::Ec, content: der.to_vec() }
}
pub(crate) fn inner(&self) -> &[u8] {
&self.content
}
}
/// Encode the header and claims given and sign the payload using the algorithm from the header and the key.
/// If the algorithm given is RSA or EC, the key needs to be in the PEM format.
///
/// ```rust
/// use serde::{Deserialize, Serialize};
/// use jsonwebtoken::{encode, Algorithm, Header, EncodingKey};
///
/// #[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, &EncodingKey::from_secret("secret".as_ref())).unwrap();
/// ```
pub fn encode<T: Serialize>(header: &Header, claims: &T, key: &EncodingKey) -> Result<String> {
if key.family != header.alg.family() {
return Err(new_error(ErrorKind::InvalidAlgorithm));
}
let encoded_header = b64_encode_part(&header)?;
let encoded_claims = b64_encode_part(&claims)?;
let message = [encoded_header.as_ref(), encoded_claims.as_ref()].join(".");
let signature = crypto::sign(&*message, key, header.alg)?;
Ok([message, signature].join("."))
}

View File

@ -42,8 +42,10 @@ pub enum ErrorKind {
InvalidRsaKey,
/// When the algorithm from string doesn't match the one passed to `from_str`
InvalidAlgorithmName,
/// When a key is provided with an invalid format
InvalidKeyFormat,
// validation error
// Validation errors
/// When a tokens `exp` claim indicates that it has expired
ExpiredSignature,
/// When a tokens `iss` claim does not match the expected issuer
@ -54,7 +56,8 @@ pub enum ErrorKind {
InvalidSubject,
/// When a tokens nbf claim represents a time in the future
ImmatureSignature,
/// When the algorithm in the header doesn't match the one passed to `decode`
/// When the algorithm in the header doesn't match the one passed to `decode` or the encoding/decoding key
/// used doesn't match the alg requested
InvalidAlgorithm,
// 3rd party errors
@ -77,27 +80,7 @@ pub enum ErrorKind {
}
impl StdError for Error {
fn description(&self) -> &str {
match *self.0 {
ErrorKind::InvalidToken => "invalid token",
ErrorKind::InvalidSignature => "invalid signature",
ErrorKind::InvalidEcdsaKey => "invalid ECDSA key",
ErrorKind::InvalidRsaKey => "invalid RSA key",
ErrorKind::ExpiredSignature => "expired signature",
ErrorKind::InvalidIssuer => "invalid issuer",
ErrorKind::InvalidAudience => "invalid audience",
ErrorKind::InvalidSubject => "invalid subject",
ErrorKind::ImmatureSignature => "immature signature",
ErrorKind::InvalidAlgorithm => "algorithms don't match",
ErrorKind::Base64(ref err) => err.description(),
ErrorKind::Json(ref err) => err.description(),
ErrorKind::Utf8(ref err) => err.description(),
ErrorKind::Crypto(ref err) => err.description(),
_ => unreachable!(),
}
}
fn cause(&self) -> Option<&StdError> {
fn cause(&self) -> Option<&dyn StdError> {
match *self.0 {
ErrorKind::InvalidToken => None,
ErrorKind::InvalidSignature => None,
@ -109,11 +92,13 @@ impl StdError for Error {
ErrorKind::InvalidSubject => None,
ErrorKind::ImmatureSignature => None,
ErrorKind::InvalidAlgorithm => None,
ErrorKind::InvalidAlgorithmName => None,
ErrorKind::InvalidKeyFormat => None,
ErrorKind::Base64(ref err) => Some(err),
ErrorKind::Json(ref err) => Some(err),
ErrorKind::Utf8(ref err) => Some(err),
ErrorKind::Crypto(ref err) => Some(err),
_ => unreachable!(),
ErrorKind::__Nonexhaustive => None,
}
}
}
@ -121,21 +106,23 @@ impl StdError for Error {
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self.0 {
ErrorKind::InvalidToken => write!(f, "invalid token"),
ErrorKind::InvalidSignature => write!(f, "invalid signature"),
ErrorKind::InvalidEcdsaKey => write!(f, "invalid ECDSA key"),
ErrorKind::InvalidRsaKey => write!(f, "invalid RSA key"),
ErrorKind::ExpiredSignature => write!(f, "expired signature"),
ErrorKind::InvalidIssuer => write!(f, "invalid issuer"),
ErrorKind::InvalidAudience => write!(f, "invalid audience"),
ErrorKind::InvalidSubject => write!(f, "invalid subject"),
ErrorKind::ImmatureSignature => write!(f, "immature signature"),
ErrorKind::InvalidAlgorithm => write!(f, "algorithms don't match"),
ErrorKind::Base64(ref err) => write!(f, "base64 error: {}", err),
ErrorKind::InvalidToken
| ErrorKind::InvalidSignature
| ErrorKind::InvalidEcdsaKey
| ErrorKind::InvalidRsaKey
| ErrorKind::ExpiredSignature
| ErrorKind::InvalidIssuer
| ErrorKind::InvalidAudience
| ErrorKind::InvalidSubject
| ErrorKind::ImmatureSignature
| ErrorKind::InvalidAlgorithm
| ErrorKind::InvalidKeyFormat
| ErrorKind::InvalidAlgorithmName => write!(f, "{}", self),
ErrorKind::Json(ref err) => write!(f, "JSON error: {}", err),
ErrorKind::Utf8(ref err) => write!(f, "UTF-8 error: {}", err),
ErrorKind::Crypto(ref err) => write!(f, "Crypto error: {}", err),
_ => unreachable!(),
ErrorKind::Base64(ref err) => write!(f, "Base64 error: {}", err),
ErrorKind::__Nonexhaustive => write!(f, "Unknown error"),
}
}
}

View File

@ -1,4 +1,8 @@
use crypto::Algorithm;
use serde::{Deserialize, Serialize};
use crate::algorithms::Algorithm;
use crate::errors::Result;
use crate::serialization::b64_decode;
/// A basic JWT header, the alg defaults to HS256 and typ is automatically
/// set to `JWT`. All the other fields are optional.
@ -42,7 +46,7 @@ pub struct Header {
impl Header {
/// Returns a JWT header with the algorithm given
pub fn new(algorithm: Algorithm) -> Header {
pub fn new(algorithm: Algorithm) -> Self {
Header {
typ: Some("JWT".to_string()),
alg: algorithm,
@ -53,6 +57,14 @@ impl Header {
x5t: None,
}
}
/// Converts an encoded part into the Header struct if possible
pub(crate) fn from_encoded(encoded_part: &str) -> Result<Self> {
let decoded = b64_decode(encoded_part)?;
let s = String::from_utf8(decoded)?;
Ok(serde_json::from_str(&s)?)
}
}
impl Default for Header {

View File

@ -1,164 +1,22 @@
//! Create and parses JWT (JSON Web Tokens)
//!
//! Documentation: [stable](https://docs.rs/jsonwebtoken/)
#![recursion_limit = "300"]
#![deny(missing_docs)]
#[macro_use]
extern crate serde_derive;
extern crate base64;
extern crate chrono;
extern crate ring;
extern crate serde;
extern crate serde_json;
extern crate untrusted;
mod crypto;
/// All the errors, generated using error-chain
mod algorithms;
/// Lower level functions, if you want to do something other than JWTs
pub mod crypto;
mod decoding;
mod encoding;
/// All the errors that can be encountered while encoding/decoding JWTs
pub mod errors;
mod header;
mod pem;
mod serialization;
mod validation;
pub use crypto::{sign, verify, Algorithm};
pub use algorithms::Algorithm;
pub use decoding::{dangerous_unsafe_decode, decode, decode_header, DecodingKey, TokenData};
pub use encoding::{encode, EncodingKey};
pub use header::Header;
pub use serialization::TokenData;
pub use validation::Validation;
use serde::de::DeserializeOwned;
use serde::ser::Serialize;
use errors::{new_error, ErrorKind, Result};
use serialization::{from_jwt_part, from_jwt_part_claims, to_jwt_part};
use validation::validate;
/// Encode the header and claims given and sign the payload using the algorithm from the header and the key
///
/// ```rust,ignore
/// #[macro_use]
/// extern crate serde_derive;
/// use jsonwebtoken::{encode, Algorithm, Header};
///
/// /// #[derive(Debug, Serialize, Deserialize)]
/// struct Claims {
/// sub: String,
/// company: String
/// }
///
/// let my_claims = Claims {
/// sub: "b@b.com".to_owned(),
/// company: "ACME".to_owned()
/// };
///
/// // my_claims is a struct that implements Serialize
/// // This will create a JWT using HS256 as algorithm
/// let token = encode(&Header::default(), &my_claims, "secret".as_ref()).unwrap();
/// ```
pub fn encode<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, header.alg)?;
Ok([signing_input, signature].join("."))
}
/// Used in decode: takes the result of a rsplit and ensure we only get 2 parts
/// Errors if we don't
macro_rules! expect_two {
($iter:expr) => {{
let mut i = $iter;
match (i.next(), i.next(), i.next()) {
(Some(first), Some(second), None) => (first, second),
_ => return Err(new_error(ErrorKind::InvalidToken)),
}
}};
}
/// 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, Validation, Algorithm};
///
/// #[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", &Validation::new(Algorithm::HS256));
/// ```
pub fn decode<T: DeserializeOwned>(
token: &str,
key: &[u8],
validation: &Validation,
) -> Result<TokenData<T>> {
let (signature, signing_input) = expect_two!(token.rsplitn(2, '.'));
let (claims, header) = expect_two!(signing_input.rsplitn(2, '.'));
let header: Header = from_jwt_part(header)?;
if !verify(signature, signing_input, key, header.alg)? {
return Err(new_error(ErrorKind::InvalidSignature));
}
if !validation.algorithms.contains(&header.alg) {
return Err(new_error(ErrorKind::InvalidAlgorithm));
}
let (decoded_claims, claims_map): (T, _) = from_jwt_part_claims(claims)?;
validate(&claims_map, validation)?;
Ok(TokenData { header, claims: decoded_claims })
}
/// Decode a token without any signature validation into a struct containing 2 fields: `claims` and `header`.
///
/// NOTE: Do not use this unless you know what you are doing! If the token's signature is invalid, it will *not* return an error.
///
/// ```rust,ignore
/// #[macro_use]
/// extern crate serde_derive;
/// use jsonwebtoken::{dangerous_unsafe_decode, Validation, Algorithm};
///
/// #[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 = dangerous_unsafe_decode::<Claims>(&token, &Validation::new(Algorithm::HS256));
/// ```
pub fn dangerous_unsafe_decode<T: DeserializeOwned>(token: &str) -> Result<TokenData<T>> {
let (_, signing_input) = expect_two!(token.rsplitn(2, '.'));
let (claims, header) = expect_two!(signing_input.rsplitn(2, '.'));
let header: Header = from_jwt_part(header)?;
let (decoded_claims, _): (T, _) = from_jwt_part_claims(claims)?;
Ok(TokenData { header, claims: decoded_claims })
}
/// Decode a token and return the Header. This is not doing any kind of validation: it is meant to be
/// used when you don't know which `alg` the token is using and want to find out.
///
/// If the token has an invalid format, it will return an error.
///
/// ```rust,ignore
/// use jsonwebtoken::decode_header;
///
/// let token = "a.jwt.token".to_string();
/// let header = decode_header(&token);
/// ```
pub fn decode_header(token: &str) -> Result<Header> {
let (_, signing_input) = expect_two!(token.rsplitn(2, '.'));
let (_, header) = expect_two!(signing_input.rsplitn(2, '.'));
from_jwt_part(header)
}

205
src/pem/decoder.rs Normal file
View File

@ -0,0 +1,205 @@
use crate::errors::{ErrorKind, Result};
use simple_asn1::{BigUint, OID};
/// Supported PEM files for EC and RSA Public and Private Keys
#[derive(Debug, PartialEq)]
enum PemType {
EcPublic,
EcPrivate,
RsaPublic,
RsaPrivate,
}
#[derive(Debug, PartialEq)]
enum Standard {
// Only for RSA
Pkcs1,
// RSA/EC
Pkcs8,
}
#[derive(Debug, PartialEq)]
enum Classification {
Ec,
Rsa,
}
/// The return type of a successful PEM encoded key with `decode_pem`
///
/// This struct gives a way to parse a string to a key for use in jsonwebtoken.
/// A struct is necessary as it provides the lifetime of the key
///
/// PEM public private keys are encoded PKCS#1 or PKCS#8
/// You will find that with PKCS#8 RSA keys that the PKCS#1 content
/// is embedded inside. This is what is provided to ring via `Key::Der`
/// For EC keys, they are always PKCS#8 on the outside but like RSA keys
/// EC keys contain a section within that ultimately has the configuration
/// that ring uses.
/// Documentation about these formats is at
/// PKCS#1: https://tools.ietf.org/html/rfc8017
/// PKCS#8: https://tools.ietf.org/html/rfc5958
#[derive(Debug)]
pub(crate) struct PemEncodedKey {
content: Vec<u8>,
asn1: Vec<simple_asn1::ASN1Block>,
pem_type: PemType,
standard: Standard,
}
impl PemEncodedKey {
/// Read the PEM file for later key use
pub fn new(input: &[u8]) -> Result<PemEncodedKey> {
match pem::parse(input) {
Ok(content) => {
let pem_contents = content.contents;
let asn1_content = match simple_asn1::from_der(pem_contents.as_slice()) {
Ok(asn1) => asn1,
Err(_) => return Err(ErrorKind::InvalidKeyFormat.into()),
};
match content.tag.as_ref() {
// This handles a PKCS#1 RSA Private key
"RSA PRIVATE KEY" => Ok(PemEncodedKey {
content: pem_contents,
asn1: asn1_content,
pem_type: PemType::RsaPrivate,
standard: Standard::Pkcs1,
}),
"RSA PUBLIC KEY" => Ok(PemEncodedKey {
content: pem_contents,
asn1: asn1_content,
pem_type: PemType::RsaPublic,
standard: Standard::Pkcs1,
}),
// No "EC PRIVATE KEY"
// https://security.stackexchange.com/questions/84327/converting-ecc-private-key-to-pkcs1-format
// "there is no such thing as a "PKCS#1 format" for elliptic curve (EC) keys"
// This handles PKCS#8 public & private keys
tag @ "PRIVATE KEY" | tag @ "PUBLIC KEY" => match classify_pem(&asn1_content) {
Some(c) => {
let is_private = tag == "PRIVATE KEY";
let pem_type = match c {
Classification::Ec => {
if is_private {
PemType::EcPrivate
} else {
PemType::EcPublic
}
}
Classification::Rsa => {
if is_private {
PemType::RsaPrivate
} else {
PemType::RsaPublic
}
}
};
Ok(PemEncodedKey {
content: pem_contents,
asn1: asn1_content,
pem_type,
standard: Standard::Pkcs8,
})
}
None => Err(ErrorKind::InvalidKeyFormat.into()),
},
// Unknown/unsupported type
_ => Err(ErrorKind::InvalidKeyFormat.into()),
}
}
Err(_) => Err(ErrorKind::InvalidKeyFormat.into()),
}
}
/// Can only be PKCS8
pub fn as_ec_private_key(&self) -> Result<&[u8]> {
match self.standard {
Standard::Pkcs1 => Err(ErrorKind::InvalidKeyFormat.into()),
Standard::Pkcs8 => match self.pem_type {
PemType::EcPrivate => Ok(self.content.as_slice()),
_ => Err(ErrorKind::InvalidKeyFormat.into()),
},
}
}
/// Can only be PKCS8
pub fn as_ec_public_key(&self) -> Result<&[u8]> {
match self.standard {
Standard::Pkcs1 => Err(ErrorKind::InvalidKeyFormat.into()),
Standard::Pkcs8 => match self.pem_type {
PemType::EcPublic => extract_first_bitstring(&self.asn1),
_ => Err(ErrorKind::InvalidKeyFormat.into()),
},
}
}
/// Can be PKCS1 or PKCS8
pub fn as_rsa_key(&self) -> Result<&[u8]> {
match self.standard {
Standard::Pkcs1 => Ok(self.content.as_slice()),
Standard::Pkcs8 => match self.pem_type {
PemType::RsaPrivate => extract_first_bitstring(&self.asn1),
PemType::RsaPublic => extract_first_bitstring(&self.asn1),
_ => Err(ErrorKind::InvalidKeyFormat.into()),
},
}
}
}
// This really just finds and returns the first bitstring or octet string
// Which is the x coordinate for EC public keys
// And the DER contents of an RSA key
// Though PKCS#11 keys shouldn't have anything else.
// It will get confusing with certificates.
fn extract_first_bitstring(asn1: &[simple_asn1::ASN1Block]) -> Result<&[u8]> {
for asn1_entry in asn1.iter() {
match asn1_entry {
simple_asn1::ASN1Block::Sequence(_, entries) => {
if let Ok(result) = extract_first_bitstring(entries) {
return Ok(result);
}
}
simple_asn1::ASN1Block::BitString(_, _, value) => {
return Ok(value.as_ref());
}
simple_asn1::ASN1Block::OctetString(_, value) => {
return Ok(value.as_ref());
}
_ => (),
}
}
Err(ErrorKind::InvalidEcdsaKey.into())
}
/// Find whether this is EC or RSA
fn classify_pem(asn1: &[simple_asn1::ASN1Block]) -> Option<Classification> {
// These should be constant but the macro requires
// #![feature(const_vec_new)]
let ec_public_key_oid = simple_asn1::oid!(1, 2, 840, 10_045, 2, 1);
let rsa_public_key_oid = simple_asn1::oid!(1, 2, 840, 113_549, 1, 1, 1);
for asn1_entry in asn1.iter() {
match asn1_entry {
simple_asn1::ASN1Block::Sequence(_, entries) => {
if let Some(classification) = classify_pem(entries) {
return Some(classification);
}
}
simple_asn1::ASN1Block::ObjectIdentifier(_, oid) => {
if oid == ec_public_key_oid {
return Some(Classification::Ec);
}
if oid == rsa_public_key_oid {
return Some(Classification::Rsa);
}
}
_ => {}
}
}
None
}

1
src/pem/mod.rs Normal file
View File

@ -0,0 +1 @@
pub(crate) mod decoder;

View File

@ -1,43 +1,32 @@
use base64;
use serde::de::DeserializeOwned;
use serde::ser::Serialize;
use serde_json::map::Map;
use serde_json::{from_str, to_string, Value};
use errors::Result;
use header::Header;
use crate::errors::Result;
/// The return type of a successful call to decode
#[derive(Debug)]
pub struct TokenData<T> {
/// The decoded JWT header
pub header: Header,
/// The decoded JWT claims
pub claims: T,
pub(crate) fn b64_encode(input: &[u8]) -> String {
base64::encode_config(input, base64::URL_SAFE_NO_PAD)
}
/// 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))
pub(crate) fn b64_decode(input: &str) -> Result<Vec<u8>> {
base64::decode_config(input, base64::URL_SAFE_NO_PAD).map_err(|e| e.into())
}
/// Decodes from base64 and deserializes from JSON to a struct
pub fn from_jwt_part<B: AsRef<str>, T: DeserializeOwned>(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)?)
/// Serializes a struct to JSON and encodes it in base64
pub(crate) fn b64_encode_part<T: Serialize>(input: &T) -> Result<String> {
let json = to_string(input)?;
Ok(b64_encode(json.as_bytes()))
}
/// Decodes from base64 and deserializes from JSON to a struct AND a hashmap
pub fn from_jwt_part_claims<B: AsRef<str>, T: DeserializeOwned>(
/// Decodes from base64 and deserializes from JSON to a struct AND a hashmap of Value so we can
/// run validation on it
pub(crate) fn from_jwt_part_claims<B: AsRef<str>, T: DeserializeOwned>(
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 s = String::from_utf8(b64_decode(encoded.as_ref())?)?;
let claims: T = from_str(&s)?;
let map: Map<_, _> = from_str(&s)?;
Ok((claims, map))
let validation_map: Map<_, _> = from_str(&s)?;
Ok((claims, validation_map))
}

View File

@ -1,14 +1,15 @@
use chrono::Utc;
use serde::ser::Serialize;
use std::collections::HashSet;
use std::time::{SystemTime, UNIX_EPOCH};
use serde_json::map::Map;
use serde_json::{from_value, to_value, Value};
use serde_json::{from_value, Value};
use crypto::Algorithm;
use errors::{new_error, ErrorKind, Result};
use crate::algorithms::Algorithm;
use crate::errors::{new_error, ErrorKind, Result};
/// Contains the various validations that are applied after decoding a token.
/// Contains the various validations that are applied after decoding a JWT.
///
/// All time validation happen on UTC timestamps.
/// All time validation happen on UTC timestamps as seconds.
///
/// ```rust
/// use jsonwebtoken::Validation;
@ -21,7 +22,7 @@ use errors::{new_error, ErrorKind, Result};
///
/// // Setting audience
/// let mut validation = Validation::default();
/// validation.set_audience(&"Me"); // string
/// validation.set_audience(&["Me"]); // a single string
/// validation.set_audience(&["Me", "You"]); // array of strings
/// ```
#[derive(Debug, Clone, PartialEq)]
@ -30,7 +31,7 @@ pub struct Validation {
/// account for clock skew.
///
/// Defaults to `0`.
pub leeway: i64,
pub leeway: u64,
/// Whether to validate the `exp` field.
///
/// It will return an error if the time in the `exp` field is past.
@ -43,13 +44,11 @@ pub struct Validation {
///
/// Defaults to `false`.
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.
/// If it contains a value, the validation will check that the `aud` field is a member of the
/// audience provided and will error otherwise.
///
/// Defaults to `None`.
pub aud: Option<Value>,
pub aud: Option<HashSet<String>>,
/// If it contains a value, the validation will check that the `iss` field is the same as the
/// one provided and will error otherwise.
///
@ -75,10 +74,9 @@ impl Validation {
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());
/// `aud` is a collection of one or more acceptable audience members
pub fn set_audience<T: ToString>(&mut self, items: &[T]) {
self.aud = Some(items.iter().map(|x| x.to_string()).collect())
}
}
@ -99,12 +97,17 @@ impl Default for Validation {
}
}
fn get_current_timestamp() -> u64 {
let start = SystemTime::now();
start.duration_since(UNIX_EPOCH).expect("Time went backwards").as_secs()
}
pub fn validate(claims: &Map<String, Value>, options: &Validation) -> Result<()> {
let now = Utc::now().timestamp();
let now = get_current_timestamp();
if options.validate_exp {
if let Some(exp) = claims.get("exp") {
if from_value::<i64>(exp.clone())? < now - options.leeway {
if from_value::<u64>(exp.clone())? < now - options.leeway {
return Err(new_error(ErrorKind::ExpiredSignature));
}
} else {
@ -114,7 +117,7 @@ pub fn validate(claims: &Map<String, Value>, options: &Validation) -> Result<()>
if options.validate_nbf {
if let Some(nbf) = claims.get("nbf") {
if from_value::<i64>(nbf.clone())? > now + options.leeway {
if from_value::<u64>(nbf.clone())? > now + options.leeway {
return Err(new_error(ErrorKind::ImmatureSignature));
}
} else {
@ -144,9 +147,20 @@ pub fn validate(claims: &Map<String, Value>, options: &Validation) -> Result<()>
if let Some(ref correct_aud) = options.aud {
if let Some(aud) = claims.get("aud") {
if aud != correct_aud {
return Err(new_error(ErrorKind::InvalidAudience));
}
match aud {
Value::String(aud_found) => {
if !correct_aud.contains(aud_found) {
return Err(new_error(ErrorKind::InvalidAudience));
}
}
Value::Array(_) => {
let provided_aud: HashSet<String> = from_value(aud.clone())?;
if provided_aud.intersection(correct_aud).count() == 0 {
return Err(new_error(ErrorKind::InvalidAudience));
}
}
_ => return Err(new_error(ErrorKind::InvalidAudience)),
};
} else {
return Err(new_error(ErrorKind::InvalidAudience));
}
@ -157,18 +171,17 @@ pub fn validate(claims: &Map<String, Value>, options: &Validation) -> Result<()>
#[cfg(test)]
mod tests {
use chrono::Utc;
use serde_json::map::Map;
use serde_json::to_value;
use super::{validate, Validation};
use super::{get_current_timestamp, validate, Validation};
use errors::ErrorKind;
use crate::errors::ErrorKind;
#[test]
fn exp_in_future_ok() {
let mut claims = Map::new();
claims.insert("exp".to_string(), to_value(Utc::now().timestamp() + 10000).unwrap());
claims.insert("exp".to_string(), to_value(get_current_timestamp() + 10000).unwrap());
let res = validate(&claims, &Validation::default());
assert!(res.is_ok());
}
@ -176,7 +189,7 @@ mod tests {
#[test]
fn exp_in_past_fails() {
let mut claims = Map::new();
claims.insert("exp".to_string(), to_value(Utc::now().timestamp() - 100000).unwrap());
claims.insert("exp".to_string(), to_value(get_current_timestamp() - 100000).unwrap());
let res = validate(&claims, &Validation::default());
assert!(res.is_err());
@ -189,7 +202,7 @@ mod tests {
#[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());
claims.insert("exp".to_string(), to_value(get_current_timestamp() - 500).unwrap());
let validation = Validation { leeway: 1000 * 60, ..Default::default() };
let res = validate(&claims, &validation);
assert!(res.is_ok());
@ -210,7 +223,7 @@ mod tests {
#[test]
fn nbf_in_past_ok() {
let mut claims = Map::new();
claims.insert("nbf".to_string(), to_value(Utc::now().timestamp() - 10000).unwrap());
claims.insert("nbf".to_string(), to_value(get_current_timestamp() - 10000).unwrap());
let validation =
Validation { validate_exp: false, validate_nbf: true, ..Validation::default() };
let res = validate(&claims, &validation);
@ -220,7 +233,7 @@ mod tests {
#[test]
fn nbf_in_future_fails() {
let mut claims = Map::new();
claims.insert("nbf".to_string(), to_value(Utc::now().timestamp() + 100000).unwrap());
claims.insert("nbf".to_string(), to_value(get_current_timestamp() + 100000).unwrap());
let validation =
Validation { validate_exp: false, validate_nbf: true, ..Validation::default() };
let res = validate(&claims, &validation);
@ -235,7 +248,7 @@ mod tests {
#[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());
claims.insert("nbf".to_string(), to_value(get_current_timestamp() + 500).unwrap());
let validation = Validation {
leeway: 1000 * 60,
validate_nbf: true,
@ -345,9 +358,9 @@ mod tests {
#[test]
fn aud_string_ok() {
let mut claims = Map::new();
claims.insert("aud".to_string(), to_value("Everyone").unwrap());
claims.insert("aud".to_string(), to_value(["Everyone"]).unwrap());
let mut validation = Validation { validate_exp: false, ..Validation::default() };
validation.set_audience(&"Everyone");
validation.set_audience(&["Everyone"]);
let res = validate(&claims, &validation);
assert!(res.is_ok());
}
@ -365,7 +378,7 @@ mod tests {
#[test]
fn aud_type_mismatch_fails() {
let mut claims = Map::new();
claims.insert("aud".to_string(), to_value("Everyone").unwrap());
claims.insert("aud".to_string(), to_value(["Everyone"]).unwrap());
let mut validation = Validation { validate_exp: false, ..Validation::default() };
validation.set_audience(&["UserA", "UserB"]);
let res = validate(&claims, &validation);
@ -380,9 +393,9 @@ mod tests {
#[test]
fn aud_correct_type_not_matching_fails() {
let mut claims = Map::new();
claims.insert("aud".to_string(), to_value("Everyone").unwrap());
claims.insert("aud".to_string(), to_value(["Everyone"]).unwrap());
let mut validation = Validation { validate_exp: false, ..Validation::default() };
validation.set_audience(&"None");
validation.set_audience(&["None"]);
let res = validate(&claims, &validation);
assert!(res.is_err());
@ -396,7 +409,7 @@ mod tests {
fn aud_missing_fails() {
let claims = Map::new();
let mut validation = Validation { validate_exp: false, ..Validation::default() };
validation.set_audience(&"None");
validation.set_audience(&["None"]);
let res = validate(&claims, &validation);
assert!(res.is_err());
@ -405,4 +418,48 @@ mod tests {
_ => assert!(false),
};
}
// https://github.com/Keats/jsonwebtoken/issues/51
#[test]
fn does_validation_in_right_order() {
let mut claims = Map::new();
claims.insert("exp".to_string(), to_value(get_current_timestamp() + 10000).unwrap());
let v = Validation {
leeway: 5,
validate_exp: true,
iss: Some("iss no check".to_string()),
sub: Some("sub no check".to_string()),
..Validation::default()
};
let res = validate(&claims, &v);
// It errors because it needs to validate iss/sub which are missing
assert!(res.is_err());
match res.unwrap_err().kind() {
&ErrorKind::InvalidIssuer => (),
t @ _ => {
println!("{:?}", t);
assert!(false)
}
};
}
// https://github.com/Keats/jsonwebtoken/issues/110
#[test]
fn aud_use_validation_struct() {
let mut claims = Map::new();
claims.insert(
"aud".to_string(),
to_value("my-googleclientid1234.apps.googleusercontent.com").unwrap(),
);
let aud = "my-googleclientid1234.apps.googleusercontent.com".to_string();
let mut aud_hashset = std::collections::HashSet::new();
aud_hashset.insert(aud);
let validation =
Validation { aud: Some(aud_hashset), validate_exp: false, ..Validation::default() };
let res = validate(&claims, &validation);
println!("{:?}", res);
assert!(res.is_ok());
}
}

View File

@ -1,38 +0,0 @@
extern crate jsonwebtoken;
#[macro_use]
extern crate serde_derive;
extern crate chrono;
use chrono::Utc;
use jsonwebtoken::{decode, encode, sign, verify, Algorithm, Header, Validation};
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
struct Claims {
sub: String,
company: String,
exp: i64,
}
#[test]
fn round_trip_sign_verification() {
let privkey = include_bytes!("private_ecdsa_key.pk8");
let encrypted = sign("hello world", privkey, Algorithm::ES256).unwrap();
let pubkey = include_bytes!("public_ecdsa_key.pk8");
let is_valid = verify(&encrypted, "hello world", pubkey, Algorithm::ES256).unwrap();
assert!(is_valid);
}
#[test]
fn round_trip_claim() {
let my_claims = Claims {
sub: "b@b.com".to_string(),
company: "ACME".to_string(),
exp: Utc::now().timestamp() + 10000,
};
let privkey = include_bytes!("private_ecdsa_key.pk8");
let token = encode(&Header::new(Algorithm::ES256), &my_claims, privkey).unwrap();
let pubkey = include_bytes!("public_ecdsa_key.pk8");
let token_data = decode::<Claims>(&token, pubkey, &Validation::new(Algorithm::ES256)).unwrap();
assert_eq!(my_claims, token_data.claims);
assert!(token_data.header.kid.is_none());
}

93
tests/ecdsa/mod.rs Normal file
View File

@ -0,0 +1,93 @@
use chrono::Utc;
use jsonwebtoken::{
crypto::{sign, verify},
decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation,
};
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub struct Claims {
sub: String,
company: String,
exp: i64,
}
#[test]
fn round_trip_sign_verification_pk8() {
let privkey = include_bytes!("private_ecdsa_key.pk8");
let pubkey = include_bytes!("public_ecdsa_key.pk8");
let encrypted =
sign("hello world", &EncodingKey::from_ec_der(privkey), Algorithm::ES256).unwrap();
let is_valid =
verify(&encrypted, "hello world", &DecodingKey::from_ec_der(pubkey), Algorithm::ES256)
.unwrap();
assert!(is_valid);
}
#[test]
fn round_trip_sign_verification_pem() {
let privkey_pem = include_bytes!("private_ecdsa_key.pem");
let pubkey_pem = include_bytes!("public_ecdsa_key.pem");
let encrypted =
sign("hello world", &EncodingKey::from_ec_pem(privkey_pem).unwrap(), Algorithm::ES256)
.unwrap();
let is_valid = verify(
&encrypted,
"hello world",
&DecodingKey::from_ec_pem(pubkey_pem).unwrap(),
Algorithm::ES256,
)
.unwrap();
assert!(is_valid);
}
#[test]
fn round_trip_claim() {
let privkey_pem = include_bytes!("private_ecdsa_key.pem");
let pubkey_pem = include_bytes!("public_ecdsa_key.pem");
let my_claims = Claims {
sub: "b@b.com".to_string(),
company: "ACME".to_string(),
exp: Utc::now().timestamp() + 10000,
};
let token = encode(
&Header::new(Algorithm::ES256),
&my_claims,
&EncodingKey::from_ec_pem(privkey_pem).unwrap(),
)
.unwrap();
let token_data = decode::<Claims>(
&token,
&DecodingKey::from_ec_pem(pubkey_pem).unwrap(),
&Validation::new(Algorithm::ES256),
)
.unwrap();
assert_eq!(my_claims, token_data.claims);
}
// https://jwt.io/ is often used for examples so ensure their example works with jsonwebtoken
#[test]
fn roundtrip_with_jwtio_example() {
// We currently do not support SEC1 so we use the converted PKCS8 formatted
let privkey_pem = include_bytes!("private_jwtio_pkcs8.pem");
let pubkey_pem = include_bytes!("public_jwtio.pem");
let my_claims = Claims {
sub: "b@b.com".to_string(),
company: "ACME".to_string(),
exp: Utc::now().timestamp() + 10000,
};
let token = encode(
&Header::new(Algorithm::ES384),
&my_claims,
&EncodingKey::from_ec_pem(privkey_pem).unwrap(),
)
.unwrap();
let token_data = decode::<Claims>(
&token,
&DecodingKey::from_ec_pem(pubkey_pem).unwrap(),
&Validation::new(Algorithm::ES384),
)
.unwrap();
assert_eq!(my_claims, token_data.claims);
}

View File

@ -0,0 +1,5 @@
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgWTFfCGljY6aw3Hrt
kHmPRiazukxPLb6ilpRAewjW8nihRANCAATDskChT+Altkm9X7MI69T3IUmrQU0L
950IxEzvw/x5BMEINRMrXLBJhqzO9Bm+d6JbqA21YQmd1Kt4RzLJR1W+
-----END PRIVATE KEY-----

View File

@ -0,0 +1,6 @@
-----BEGIN EC PRIVATE KEY-----
MIGkAgEBBDCAHpFQ62QnGCEvYh/pE9QmR1C9aLcDItRbslbmhen/h1tt8AyMhske
enT+rAyyPhGgBwYFK4EEACKhZANiAAQLW5ZJePZzMIPAxMtZXkEWbDF0zo9f2n4+
T1h/2sh/fviblc/VTyrv10GEtIi5qiOy85Pf1RRw8lE5IPUWpgu553SteKigiKLU
PeNpbqmYZUkWGh3MLfVzLmx85ii2vMU=
-----END EC PRIVATE KEY-----

View File

@ -0,0 +1,6 @@
-----BEGIN PRIVATE KEY-----
MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDCAHpFQ62QnGCEvYh/p
E9QmR1C9aLcDItRbslbmhen/h1tt8AyMhskeenT+rAyyPhGhZANiAAQLW5ZJePZz
MIPAxMtZXkEWbDF0zo9f2n4+T1h/2sh/fviblc/VTyrv10GEtIi5qiOy85Pf1RRw
8lE5IPUWpgu553SteKigiKLUPeNpbqmYZUkWGh3MLfVzLmx85ii2vMU=
-----END PRIVATE KEY-----

View File

@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEw7JAoU/gJbZJvV+zCOvU9yFJq0FN
C/edCMRM78P8eQTBCDUTK1ywSYaszvQZvneiW6gNtWEJndSreEcyyUdVvg==
-----END PUBLIC KEY-----

View File

@ -0,0 +1,5 @@
-----BEGIN PUBLIC KEY-----
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEC1uWSXj2czCDwMTLWV5BFmwxdM6PX9p+
Pk9Yf9rIf374m5XP1U8q79dBhLSIuaojsvOT39UUcPJROSD1FqYLued0rXiooIii
1D3jaW6pmGVJFhodzC31cy5sfOYotrzF
-----END PUBLIC KEY-----

160
tests/hmac.rs Normal file
View File

@ -0,0 +1,160 @@
use chrono::Utc;
use jsonwebtoken::{
crypto::{sign, verify},
dangerous_unsafe_decode, decode, decode_header, encode, Algorithm, DecodingKey, EncodingKey,
Header, Validation,
};
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub struct Claims {
sub: String,
company: String,
exp: i64,
}
#[test]
fn sign_hs256() {
let result =
sign("hello world", &EncodingKey::from_secret(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", &DecodingKey::from_secret(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(),
exp: Utc::now().timestamp() + 10000,
};
let mut header = Header::default();
header.kid = Some("kid".to_string());
let token = encode(&header, &my_claims, &EncodingKey::from_secret(b"secret")).unwrap();
let token_data =
decode::<Claims>(&token, &DecodingKey::from_secret(b"secret"), &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(),
exp: Utc::now().timestamp() + 10000,
};
let token =
encode(&Header::default(), &my_claims, &EncodingKey::from_secret(b"secret")).unwrap();
let token_data =
decode::<Claims>(&token, &DecodingKey::from_secret(b"secret"), &Validation::default())
.unwrap();
assert_eq!(my_claims, token_data.claims);
assert!(token_data.header.kid.is_none());
}
#[test]
fn decode_token() {
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjI1MzI1MjQ4OTF9.9r56oF7ZliOBlOAyiOFperTGxBtPykRQiWNFxhDCW98";
let claims =
decode::<Claims>(token, &DecodingKey::from_secret(b"secret"), &Validation::default());
println!("{:?}", claims);
claims.unwrap();
}
#[test]
#[should_panic(expected = "InvalidToken")]
fn decode_token_missing_parts() {
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9";
let claims =
decode::<Claims>(token, &DecodingKey::from_secret(b"secret"), &Validation::default());
claims.unwrap();
}
#[test]
#[should_panic(expected = "InvalidSignature")]
fn decode_token_invalid_signature() {
let token =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUifQ.wrong";
let claims =
decode::<Claims>(token, &DecodingKey::from_secret(b"secret"), &Validation::default());
claims.unwrap();
}
#[test]
#[should_panic(expected = "InvalidAlgorithm")]
fn decode_token_wrong_algorithm() {
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUifQ.I1BvFoHe94AFf09O6tDbcSB8-jp8w6xZqmyHIwPeSdY";
let claims = decode::<Claims>(
token,
&DecodingKey::from_secret(b"secret"),
&Validation::new(Algorithm::RS512),
);
claims.unwrap();
}
#[test]
#[should_panic(expected = "InvalidAlgorithm")]
fn encode_wrong_alg_family() {
let my_claims = Claims {
sub: "b@b.com".to_string(),
company: "ACME".to_string(),
exp: Utc::now().timestamp() + 10000,
};
let claims = encode(&Header::default(), &my_claims, &EncodingKey::from_rsa_der(b"secret"));
claims.unwrap();
}
#[test]
fn decode_token_with_bytes_secret() {
let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjI1MzI1MjQ4OTF9.Hm0yvKH25TavFPz7J_coST9lZFYH1hQo0tvhvImmaks";
let claims =
decode::<Claims>(token, &DecodingKey::from_secret(b"\x01\x02\x03"), &Validation::default());
assert!(claims.is_ok());
}
#[test]
fn decode_header_only() {
let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjb21wYW55IjoiMTIzNDU2Nzg5MCIsInN1YiI6IkpvaG4gRG9lIn0.S";
let header = decode_header(token).unwrap();
assert_eq!(header.alg, Algorithm::HS256);
assert_eq!(header.typ, Some("JWT".to_string()));
}
#[test]
fn dangerous_unsafe_decode_token() {
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjI1MzI1MjQ4OTF9.9r56oF7ZliOBlOAyiOFperTGxBtPykRQiWNFxhDCW98";
let claims = dangerous_unsafe_decode::<Claims>(token);
claims.unwrap();
}
#[test]
#[should_panic(expected = "InvalidToken")]
fn dangerous_unsafe_decode_token_missing_parts() {
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9";
let claims = dangerous_unsafe_decode::<Claims>(token);
claims.unwrap();
}
#[test]
fn dangerous_unsafe_decode_token_invalid_signature() {
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjI1MzI1MjQ4OTF9.wrong";
let claims = dangerous_unsafe_decode::<Claims>(token);
claims.unwrap();
}
#[test]
fn dangerous_unsafe_decode_token_wrong_algorithm() {
let token = "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjI1MzI1MjQ4OTF9.fLxey-hxAKX5rNHHIx1_Ch0KmrbiuoakDVbsJjLWrx8fbjKjrPuWMYEJzTU3SBnYgnZokC-wqSdqckXUOunC-g";
let claims = dangerous_unsafe_decode::<Claims>(token);
claims.unwrap();
}

View File

@ -1,170 +1,2 @@
extern crate jsonwebtoken;
#[macro_use]
extern crate serde_derive;
extern crate chrono;
use chrono::Utc;
use jsonwebtoken::{
dangerous_unsafe_decode, decode, decode_header, encode, sign, verify, Algorithm, Header,
Validation,
};
use std::str::FromStr;
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
struct Claims {
sub: String,
company: String,
exp: i64,
}
#[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(),
exp: Utc::now().timestamp() + 10000,
};
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(), &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(),
exp: Utc::now().timestamp() + 10000,
};
let token = encode(&Header::default(), &my_claims, "secret".as_ref()).unwrap();
let token_data = decode::<Claims>(&token, "secret".as_ref(), &Validation::default()).unwrap();
assert_eq!(my_claims, token_data.claims);
assert!(token_data.header.kid.is_none());
}
#[test]
fn decode_token() {
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjI1MzI1MjQ4OTF9.9r56oF7ZliOBlOAyiOFperTGxBtPykRQiWNFxhDCW98";
let claims = decode::<Claims>(token, "secret".as_ref(), &Validation::default());
println!("{:?}", claims);
claims.unwrap();
}
#[test]
#[should_panic(expected = "InvalidToken")]
fn decode_token_missing_parts() {
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9";
let claims = decode::<Claims>(token, "secret".as_ref(), &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(), &Validation::default());
claims.unwrap();
}
#[test]
#[should_panic(expected = "InvalidAlgorithm")]
fn decode_token_wrong_algorithm() {
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUifQ.I1BvFoHe94AFf09O6tDbcSB8-jp8w6xZqmyHIwPeSdY";
let claims = decode::<Claims>(token, "secret".as_ref(), &Validation::new(Algorithm::RS512));
claims.unwrap();
}
#[test]
fn decode_token_with_bytes_secret() {
let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjI1MzI1MjQ4OTF9.Hm0yvKH25TavFPz7J_coST9lZFYH1hQo0tvhvImmaks";
let claims = decode::<Claims>(token, b"\x01\x02\x03", &Validation::default());
assert!(claims.is_ok());
}
#[test]
fn decode_header_only() {
let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjb21wYW55IjoiMTIzNDU2Nzg5MCIsInN1YiI6IkpvaG4gRG9lIn0.S";
let header = decode_header(token).unwrap();
assert_eq!(header.alg, Algorithm::HS256);
assert_eq!(header.typ, Some("JWT".to_string()));
}
#[test]
fn dangerous_unsafe_decode_token() {
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjI1MzI1MjQ4OTF9.9r56oF7ZliOBlOAyiOFperTGxBtPykRQiWNFxhDCW98";
let claims = dangerous_unsafe_decode::<Claims>(token);
claims.unwrap();
}
#[test]
#[should_panic(expected = "InvalidToken")]
fn dangerous_unsafe_decode_token_missing_parts() {
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9";
let claims = dangerous_unsafe_decode::<Claims>(token);
claims.unwrap();
}
#[test]
fn dangerous_unsafe_decode_token_invalid_signature() {
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjI1MzI1MjQ4OTF9.wrong";
let claims = dangerous_unsafe_decode::<Claims>(token);
claims.unwrap();
}
#[test]
fn dangerous_unsafe_decode_token_wrong_algorithm() {
let token = "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjI1MzI1MjQ4OTF9.fLxey-hxAKX5rNHHIx1_Ch0KmrbiuoakDVbsJjLWrx8fbjKjrPuWMYEJzTU3SBnYgnZokC-wqSdqckXUOunC-g";
let claims = dangerous_unsafe_decode::<Claims>(token);
claims.unwrap();
}
// https://github.com/Keats/jsonwebtoken/issues/51
#[test]
fn does_validation_in_right_order() {
let my_claims = Claims {
sub: "b@b.com".to_string(),
company: "ACME".to_string(),
exp: Utc::now().timestamp() + 10000,
};
let token = encode(&Header::default(), &my_claims, "secret".as_ref()).unwrap();
let v = Validation {
leeway: 5,
validate_exp: true,
iss: Some("iss no check".to_string()),
sub: Some("sub no check".to_string()),
..Validation::default()
};
let res = decode::<Claims>(&token, "secret".as_ref(), &v);
assert!(res.is_err());
println!("{:?}", res);
//assert!(res.is_ok());
}
#[test]
fn generate_algorithm_enum_from_str() {
assert!(Algorithm::from_str("HS256").is_ok());
assert!(Algorithm::from_str("HS384").is_ok());
assert!(Algorithm::from_str("HS512").is_ok());
assert!(Algorithm::from_str("RS256").is_ok());
assert!(Algorithm::from_str("RS384").is_ok());
assert!(Algorithm::from_str("RS512").is_ok());
assert!(Algorithm::from_str("").is_err());
}
mod ecdsa;
mod rsa;

View File

@ -1,44 +0,0 @@
extern crate jsonwebtoken;
#[macro_use]
extern crate serde_derive;
extern crate chrono;
use chrono::Utc;
use jsonwebtoken::{decode, encode, sign, verify, Algorithm, Header, Validation};
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
struct Claims {
sub: String,
company: String,
exp: i64,
}
#[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(),
exp: Utc::now().timestamp() + 10000,
};
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"),
&Validation::new(Algorithm::RS256),
)
.unwrap();
assert_eq!(my_claims, token_data.claims);
assert!(token_data.header.kid.is_none());
}

141
tests/rsa/mod.rs Normal file
View File

@ -0,0 +1,141 @@
use chrono::Utc;
use jsonwebtoken::{
crypto::{sign, verify},
decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation,
};
use serde::{Deserialize, Serialize};
const RSA_ALGORITHMS: &[Algorithm] = &[
Algorithm::RS256,
Algorithm::RS384,
Algorithm::RS512,
Algorithm::PS256,
Algorithm::PS384,
Algorithm::PS512,
];
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub struct Claims {
sub: String,
company: String,
exp: i64,
}
#[test]
fn round_trip_sign_verification_pem_pkcs1() {
let privkey_pem = include_bytes!("private_rsa_key_pkcs1.pem");
let pubkey_pem = include_bytes!("public_rsa_key_pkcs1.pem");
for &alg in RSA_ALGORITHMS {
let encrypted =
sign("hello world", &EncodingKey::from_rsa_pem(privkey_pem).unwrap(), alg).unwrap();
let is_valid =
verify(&encrypted, "hello world", &DecodingKey::from_rsa_pem(pubkey_pem).unwrap(), alg)
.unwrap();
assert!(is_valid);
}
}
#[test]
fn round_trip_sign_verification_pem_pkcs8() {
let privkey_pem = include_bytes!("private_rsa_key_pkcs8.pem");
let pubkey_pem = include_bytes!("public_rsa_key_pkcs8.pem");
for &alg in RSA_ALGORITHMS {
let encrypted =
sign("hello world", &EncodingKey::from_rsa_pem(privkey_pem).unwrap(), alg).unwrap();
let is_valid =
verify(&encrypted, "hello world", &DecodingKey::from_rsa_pem(pubkey_pem).unwrap(), alg)
.unwrap();
assert!(is_valid);
}
}
#[test]
fn round_trip_sign_verification_der() {
let privkey_der = include_bytes!("private_rsa_key.der");
let pubkey_der = include_bytes!("public_rsa_key.der");
for &alg in RSA_ALGORITHMS {
let encrypted = sign("hello world", &EncodingKey::from_rsa_der(privkey_der), alg).unwrap();
let is_valid =
verify(&encrypted, "hello world", &DecodingKey::from_rsa_der(pubkey_der), alg).unwrap();
assert!(is_valid);
}
}
#[test]
fn round_trip_claim() {
let my_claims = Claims {
sub: "b@b.com".to_string(),
company: "ACME".to_string(),
exp: Utc::now().timestamp() + 10000,
};
let privkey_pem = include_bytes!("private_rsa_key_pkcs1.pem");
let pubkey_pem = include_bytes!("public_rsa_key_pkcs1.pem");
for &alg in RSA_ALGORITHMS {
let token =
encode(&Header::new(alg), &my_claims, &EncodingKey::from_rsa_pem(privkey_pem).unwrap())
.unwrap();
let token_data = decode::<Claims>(
&token,
&DecodingKey::from_rsa_pem(pubkey_pem).unwrap(),
&Validation::new(alg),
)
.unwrap();
assert_eq!(my_claims, token_data.claims);
assert!(token_data.header.kid.is_none());
}
}
#[test]
fn rsa_modulus_exponent() {
let privkey = include_str!("private_rsa_key_pkcs1.pem");
let my_claims = Claims {
sub: "b@b.com".to_string(),
company: "ACME".to_string(),
exp: Utc::now().timestamp() + 10000,
};
let n = "yRE6rHuNR0QbHO3H3Kt2pOKGVhQqGZXInOduQNxXzuKlvQTLUTv4l4sggh5_CYYi_cvI-SXVT9kPWSKXxJXBXd_4LkvcPuUakBoAkfh-eiFVMh2VrUyWyj3MFl0HTVF9KwRXLAcwkREiS3npThHRyIxuy0ZMeZfxVL5arMhw1SRELB8HoGfG_AtH89BIE9jDBHZ9dLelK9a184zAf8LwoPLxvJb3Il5nncqPcSfKDDodMFBIMc4lQzDKL5gvmiXLXB1AGLm8KBjfE8s3L5xqi-yUod-j8MtvIj812dkS4QMiRVN_by2h3ZY8LYVGrqZXZTcgn2ujn8uKjXLZVD5TdQ";
let e = "AQAB";
let encrypted = encode(
&Header::new(Algorithm::RS256),
&my_claims,
&EncodingKey::from_rsa_pem(privkey.as_ref()).unwrap(),
)
.unwrap();
let res = decode::<Claims>(
&encrypted,
&DecodingKey::from_rsa_components(n, e),
&Validation::new(Algorithm::RS256),
);
assert!(res.is_ok());
}
// https://jwt.io/ is often used for examples so ensure their example works with jsonwebtoken
#[test]
fn roundtrip_with_jwtio_example_jey() {
let privkey_pem = include_bytes!("private_jwtio.pem");
let pubkey_pem = include_bytes!("public_jwtio.pem");
let my_claims = Claims {
sub: "b@b.com".to_string(),
company: "ACME".to_string(),
exp: Utc::now().timestamp() + 10000,
};
for &alg in RSA_ALGORITHMS {
let token =
encode(&Header::new(alg), &my_claims, &EncodingKey::from_rsa_pem(privkey_pem).unwrap())
.unwrap();
let token_data = decode::<Claims>(
&token,
&DecodingKey::from_rsa_pem(pubkey_pem).unwrap(),
&Validation::new(alg),
)
.unwrap();
assert_eq!(my_claims, token_data.claims);
}
}

View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEAnzyis1ZjfNB0bBgKFMSvvkTtwlvBsaJq7S5wA+kzeVOVpVWw
kWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHcaT92whREFpLv9cj5lTeJSibyr/Mr
m/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIytvHWTxZYEcXLgAXFuUuaS3uF9gEi
NQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0e+lf4s4OxQawWD79J9/5d3Ry0vbV
3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWbV6L11BWkpzGXSW4Hv43qa+GSYOD2
QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9MwIDAQABAoIBACiARq2wkltjtcjs
kFvZ7w1JAORHbEufEO1Eu27zOIlqbgyAcAl7q+/1bip4Z/x1IVES84/yTaM8p0go
amMhvgry/mS8vNi1BN2SAZEnb/7xSxbflb70bX9RHLJqKnp5GZe2jexw+wyXlwaM
+bclUCrh9e1ltH7IvUrRrQnFJfh+is1fRon9Co9Li0GwoN0x0byrrngU8Ak3Y6D9
D8GjQA4Elm94ST3izJv8iCOLSDBmzsPsXfcCUZfmTfZ5DbUDMbMxRnSo3nQeoKGC
0Lj9FkWcfmLcpGlSXTO+Ww1L7EGq+PT3NtRae1FZPwjddQ1/4V905kyQFLamAA5Y
lSpE2wkCgYEAy1OPLQcZt4NQnQzPz2SBJqQN2P5u3vXl+zNVKP8w4eBv0vWuJJF+
hkGNnSxXQrTkvDOIUddSKOzHHgSg4nY6K02ecyT0PPm/UZvtRpWrnBjcEVtHEJNp
bU9pLD5iZ0J9sbzPU/LxPmuAP2Bs8JmTn6aFRspFrP7W0s1Nmk2jsm0CgYEAyH0X
+jpoqxj4efZfkUrg5GbSEhf+dZglf0tTOA5bVg8IYwtmNk/pniLG/zI7c+GlTc9B
BwfMr59EzBq/eFMI7+LgXaVUsM/sS4Ry+yeK6SJx/otIMWtDfqxsLD8CPMCRvecC
2Pip4uSgrl0MOebl9XKp57GoaUWRWRHqwV4Y6h8CgYAZhI4mh4qZtnhKjY4TKDjx
QYufXSdLAi9v3FxmvchDwOgn4L+PRVdMwDNms2bsL0m5uPn104EzM6w1vzz1zwKz
5pTpPI0OjgWN13Tq8+PKvm/4Ga2MjgOgPWQkslulO/oMcXbPwWC3hcRdr9tcQtn9
Imf9n2spL/6EDFId+Hp/7QKBgAqlWdiXsWckdE1Fn91/NGHsc8syKvjjk1onDcw0
NvVi5vcba9oGdElJX3e9mxqUKMrw7msJJv1MX8LWyMQC5L6YNYHDfbPF1q5L4i8j
8mRex97UVokJQRRA452V2vCO6S5ETgpnad36de3MUxHgCOX3qL382Qx9/THVmbma
3YfRAoGAUxL/Eu5yvMK8SAt/dJK6FedngcM3JEFNplmtLYVLWhkIlNRGDwkg3I5K
y18Ae9n7dHVueyslrb6weq7dTkYDi3iOYRW8HRkIQh06wEdbxt0shTzAJvvCQfrB
jg/3747WSsf/zBTcHihTRBdAv6OmdhV4/dD5YBfLAkLrd+mX7iE=
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDJETqse41HRBsc
7cfcq3ak4oZWFCoZlcic525A3FfO4qW9BMtRO/iXiyCCHn8JhiL9y8j5JdVP2Q9Z
IpfElcFd3/guS9w+5RqQGgCR+H56IVUyHZWtTJbKPcwWXQdNUX0rBFcsBzCRESJL
eelOEdHIjG7LRkx5l/FUvlqsyHDVJEQsHwegZ8b8C0fz0EgT2MMEdn10t6Ur1rXz
jMB/wvCg8vG8lvciXmedyo9xJ8oMOh0wUEgxziVDMMovmC+aJctcHUAYubwoGN8T
yzcvnGqL7JSh36Pwy28iPzXZ2RLhAyJFU39vLaHdljwthUaupldlNyCfa6Ofy4qN
ctlUPlN1AgMBAAECggEAdESTQjQ70O8QIp1ZSkCYXeZjuhj081CK7jhhp/4ChK7J
GlFQZMwiBze7d6K84TwAtfQGZhQ7km25E1kOm+3hIDCoKdVSKch/oL54f/BK6sKl
qlIzQEAenho4DuKCm3I4yAw9gEc0DV70DuMTR0LEpYyXcNJY3KNBOTjN5EYQAR9s
2MeurpgK2MdJlIuZaIbzSGd+diiz2E6vkmcufJLtmYUT/k/ddWvEtz+1DnO6bRHh
xuuDMeJA/lGB/EYloSLtdyCF6sII6C6slJJtgfb0bPy7l8VtL5iDyz46IKyzdyzW
tKAn394dm7MYR1RlUBEfqFUyNK7C+pVMVoTwCC2V4QKBgQD64syfiQ2oeUlLYDm4
CcKSP3RnES02bcTyEDFSuGyyS1jldI4A8GXHJ/lG5EYgiYa1RUivge4lJrlNfjyf
dV230xgKms7+JiXqag1FI+3mqjAgg4mYiNjaao8N8O3/PD59wMPeWYImsWXNyeHS
55rUKiHERtCcvdzKl4u35ZtTqQKBgQDNKnX2bVqOJ4WSqCgHRhOm386ugPHfy+8j
m6cicmUR46ND6ggBB03bCnEG9OtGisxTo/TuYVRu3WP4KjoJs2LD5fwdwJqpgtHl
yVsk45Y1Hfo+7M6lAuR8rzCi6kHHNb0HyBmZjysHWZsn79ZM+sQnLpgaYgQGRbKV
DZWlbw7g7QKBgQCl1u+98UGXAP1jFutwbPsx40IVszP4y5ypCe0gqgon3UiY/G+1
zTLp79GGe/SjI2VpQ7AlW7TI2A0bXXvDSDi3/5Dfya9ULnFXv9yfvH1QwWToySpW
Kvd1gYSoiX84/WCtjZOr0e0HmLIb0vw0hqZA4szJSqoxQgvF22EfIWaIaQKBgQCf
34+OmMYw8fEvSCPxDxVvOwW2i7pvV14hFEDYIeZKW2W1HWBhVMzBfFB5SE8yaCQy
pRfOzj9aKOCm2FjjiErVNpkQoi6jGtLvScnhZAt/lr2TXTrl8OwVkPrIaN0bG/AS
aUYxmBPCpXu3UjhfQiWqFq/mFyzlqlgvuCc9g95HPQKBgAscKP8mLxdKwOgX8yFW
GcZ0izY/30012ajdHY+/QK5lsMoxTnn0skdS+spLxaS5ZEO4qvPVb8RAoCkWMMal
2pOhmquJQVDPDLuZHdrIiKiDM20dy9sMfHygWcZjQ4WSxf/J7T9canLZIXFhHAZT
3wc9h4G8BBCtWN2TN/LsGZdB
-----END PRIVATE KEY-----

View File

@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnzyis1ZjfNB0bBgKFMSv
vkTtwlvBsaJq7S5wA+kzeVOVpVWwkWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHc
aT92whREFpLv9cj5lTeJSibyr/Mrm/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIy
tvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0
e+lf4s4OxQawWD79J9/5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWb
V6L11BWkpzGXSW4Hv43qa+GSYOD2QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9
MwIDAQAB
-----END PUBLIC KEY-----