parent
1b594ff60e
commit
7fcae534f2
6 changed files with 49 additions and 118 deletions
23
Cargo.toml
23
Cargo.toml
|
@ -2,23 +2,24 @@
|
||||||
authors = ["Thomas Gideon <cmdln@thecommandline.net>"]
|
authors = ["Thomas Gideon <cmdln@thecommandline.net>"]
|
||||||
categories = ["cryptography", "authentication", "web-programming", "data-structures"]
|
categories = ["cryptography", "authentication", "web-programming", "data-structures"]
|
||||||
description = "JWT library for rust using serde, serde_json and openssl"
|
description = "JWT library for rust using serde, serde_json and openssl"
|
||||||
documentation = "https://commandline.github.io/medallion/"
|
documentation = "https://cmdln.github.io/medallion/"
|
||||||
homepage = "http://github.com/commandline/medallion"
|
homepage = "http://github.com/cmdln/medallion"
|
||||||
keywords = ["JWT", "token", "web", "JSON", "RSA"]
|
keywords = ["JWT", "token", "web", "JSON", "RSA"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
name = "medallion"
|
name = "medallion"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
repository = "http://github.com/commandline/medallion"
|
repository = "http://github.com/cmdn/medallion"
|
||||||
version = "2.2.3"
|
version = "2.3.0"
|
||||||
[badges]
|
[badges]
|
||||||
[badges.travis-ci]
|
[badges.travis-ci]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
repository = "https://travis-ci.org/commandline/medallion"
|
repository = "https://travis-ci.org/cmdln/medallion"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
base64 = "0.9"
|
base64 = "~0.10.0"
|
||||||
openssl = "0.10"
|
failure = "~0.1.3"
|
||||||
serde = "1.0"
|
openssl = "~0.10.15"
|
||||||
serde_derive = "1.0"
|
serde = "^1.0.80"
|
||||||
serde_json = "1.0"
|
serde_derive = "^1.0.80"
|
||||||
time = "0.1"
|
serde_json = "^1.0.33"
|
||||||
|
time = "~0.1.40"
|
||||||
|
|
19
src/crypt.rs
19
src/crypt.rs
|
@ -1,10 +1,13 @@
|
||||||
use base64::{decode_config, encode_config, URL_SAFE_NO_PAD};
|
use base64::{decode_config, encode_config, URL_SAFE_NO_PAD};
|
||||||
use header::Algorithm;
|
use header::Algorithm;
|
||||||
use openssl::hash::MessageDigest;
|
use openssl::{
|
||||||
use openssl::memcmp;
|
hash::MessageDigest,
|
||||||
use openssl::pkey::PKey;
|
memcmp,
|
||||||
use openssl::rsa::Rsa;
|
pkey::PKey,
|
||||||
use openssl::sign::{Signer, Verifier};
|
rsa::Rsa,
|
||||||
|
sign::{Signer, Verifier},
|
||||||
|
};
|
||||||
|
|
||||||
use super::Result;
|
use super::Result;
|
||||||
|
|
||||||
pub fn sign(data: &str, key: &[u8], algorithm: &Algorithm) -> Result<String> {
|
pub fn sign(data: &str, key: &[u8], algorithm: &Algorithm) -> Result<String> {
|
||||||
|
@ -72,9 +75,9 @@ fn verify_rsa(signature: &str, data: &str, key: &[u8], digest: MessageDigest) ->
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub mod tests {
|
pub mod tests {
|
||||||
|
use super::{sign, verify};
|
||||||
use header::Algorithm;
|
use header::Algorithm;
|
||||||
use openssl;
|
use openssl;
|
||||||
use super::{sign, verify};
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
pub fn sign_data_hmac() {
|
pub fn sign_data_hmac() {
|
||||||
|
@ -83,7 +86,7 @@ pub mod tests {
|
||||||
let real_sig = "TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ";
|
let real_sig = "TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ";
|
||||||
let data = format!("{}.{}", header, claims);
|
let data = format!("{}.{}", header, claims);
|
||||||
|
|
||||||
let sig = sign(&*data, "secret".as_bytes(), &Algorithm::HS256);
|
let sig = sign(&*data, b"secret", &Algorithm::HS256);
|
||||||
|
|
||||||
assert_eq!(sig.unwrap(), real_sig);
|
assert_eq!(sig.unwrap(), real_sig);
|
||||||
}
|
}
|
||||||
|
@ -120,6 +123,6 @@ pub mod tests {
|
||||||
let target = "TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ";
|
let target = "TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ";
|
||||||
let data = format!("{}.{}", header, claims);
|
let data = format!("{}.{}", header, claims);
|
||||||
|
|
||||||
assert!(verify(target, &*data, "secret".as_bytes(), &Algorithm::HS256).unwrap());
|
assert!(verify(target, &*data, b"secret", &Algorithm::HS256).unwrap());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
68
src/error.rs
68
src/error.rs
|
@ -1,67 +1,3 @@
|
||||||
use base64::DecodeError;
|
use failure::Error;
|
||||||
use serde_json;
|
|
||||||
use std::error;
|
|
||||||
use std::fmt;
|
|
||||||
use std::string::FromUtf8Error;
|
|
||||||
use openssl::error::ErrorStack;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
pub enum Error {
|
|
||||||
/// Custom, Medallion specific errors.
|
|
||||||
Custom(String),
|
|
||||||
/// String encoding errors.
|
|
||||||
Utf8(FromUtf8Error),
|
|
||||||
/// Base64 encoding or decoding errors.
|
|
||||||
Base64(DecodeError),
|
|
||||||
/// JSON parsing or stringifying errors.
|
|
||||||
JSON(serde_json::Error),
|
|
||||||
/// Errors from RSA operations.
|
|
||||||
Crypto(ErrorStack),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl error::Error for Error {
|
|
||||||
fn description(&self) -> &str {
|
|
||||||
match *self {
|
|
||||||
Error::Custom(ref message) => message,
|
|
||||||
Error::Utf8(ref err) => err.description(),
|
|
||||||
Error::Base64(ref err) => err.description(),
|
|
||||||
Error::JSON(ref err) => err.description(),
|
|
||||||
Error::Crypto(ref err) => err.description(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cause(&self) -> Option<&error::Error> {
|
|
||||||
match *self {
|
|
||||||
Error::Custom(_) => None,
|
|
||||||
Error::Utf8(ref err) => Some(err),
|
|
||||||
Error::Base64(ref err) => Some(err),
|
|
||||||
Error::JSON(ref err) => Some(err),
|
|
||||||
Error::Crypto(ref err) => Some(err),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for Error {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
match *self {
|
|
||||||
Error::Custom(ref message) => f.write_str(message),
|
|
||||||
Error::Utf8(ref err) => err.fmt(f),
|
|
||||||
Error::Base64(ref err) => err.fmt(f),
|
|
||||||
Error::JSON(ref err) => err.fmt(f),
|
|
||||||
Error::Crypto(ref err) => err.fmt(f),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! error_wrap {
|
|
||||||
($f: ty, $e: expr) => {
|
|
||||||
impl From<$f> for Error {
|
|
||||||
fn from(f: $f) -> Error { $e(f) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
error_wrap!(FromUtf8Error, Error::Utf8);
|
|
||||||
error_wrap!(DecodeError, Error::Base64);
|
|
||||||
error_wrap!(serde_json::Error, Error::JSON);
|
|
||||||
error_wrap!(ErrorStack, Error::Crypto);
|
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
use base64::{decode_config, encode_config, URL_SAFE_NO_PAD};
|
use base64::{decode_config, encode_config, URL_SAFE_NO_PAD};
|
||||||
use serde::de::DeserializeOwned;
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
use serde::Serialize;
|
|
||||||
use serde_json::{self, Value};
|
use serde_json::{self, Value};
|
||||||
use std::default::Default;
|
|
||||||
|
|
||||||
use super::error::Error;
|
|
||||||
use super::Result;
|
use super::Result;
|
||||||
|
|
||||||
/// An extensible Header that provides only algorithm field and allows for additional fields to be
|
/// An extensible Header that provides only algorithm field and allows for additional fields to be
|
||||||
|
@ -55,9 +52,7 @@ impl<T: Serialize + DeserializeOwned> Header<T> {
|
||||||
let enc = encode_config((&*s).as_bytes(), URL_SAFE_NO_PAD);
|
let enc = encode_config((&*s).as_bytes(), URL_SAFE_NO_PAD);
|
||||||
Ok(enc)
|
Ok(enc)
|
||||||
} else {
|
} else {
|
||||||
Err(Error::Custom(
|
Err(format_err!("Could not access additional headers."))
|
||||||
"Could not access additional headers.".to_owned(),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
|
@ -67,7 +62,7 @@ impl<T: Serialize + DeserializeOwned> Header<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Err(Error::Custom("Could not access default header.".to_owned()))
|
Err(format_err!("Could not access default header."))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -113,7 +108,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn to_base64() {
|
fn to_base64() {
|
||||||
let enc = "eyJhbGciOiJIUzI1NiJ9";
|
let enc = "eyJhbGciOiJIUzI1NiJ9";
|
||||||
let header: Header<()> = Default::default();
|
let header: Header<()> = Header::default();
|
||||||
|
|
||||||
assert_eq!(enc, header.to_base64().unwrap());
|
assert_eq!(enc, header.to_base64().unwrap());
|
||||||
}
|
}
|
||||||
|
@ -126,7 +121,7 @@ mod tests {
|
||||||
kid: "1KSF3g".into(),
|
kid: "1KSF3g".into(),
|
||||||
typ: "JWT".into(),
|
typ: "JWT".into(),
|
||||||
}),
|
}),
|
||||||
..Default::default()
|
..Header::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(enc, header.to_base64().unwrap());
|
assert_eq!(enc, header.to_base64().unwrap());
|
||||||
|
@ -134,7 +129,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn roundtrip() {
|
fn roundtrip() {
|
||||||
let header: Header<()> = Default::default();
|
let header: Header<()> = Header::default();
|
||||||
let enc = header.to_base64().unwrap();
|
let enc = header.to_base64().unwrap();
|
||||||
assert_eq!(header, Header::from_base64(&*enc).unwrap());
|
assert_eq!(header, Header::from_base64(&*enc).unwrap());
|
||||||
}
|
}
|
||||||
|
|
23
src/lib.rs
23
src/lib.rs
|
@ -7,6 +7,8 @@
|
||||||
///! Tries to support the standard uses for JWTs while providing reasonable ways to extend,
|
///! Tries to support the standard uses for JWTs while providing reasonable ways to extend,
|
||||||
///! primarily by adding custom headers and claims to tokens.
|
///! primarily by adding custom headers and claims to tokens.
|
||||||
extern crate base64;
|
extern crate base64;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate failure;
|
||||||
extern crate openssl;
|
extern crate openssl;
|
||||||
extern crate serde;
|
extern crate serde;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
|
@ -14,19 +16,16 @@ extern crate serde_derive;
|
||||||
extern crate serde_json;
|
extern crate serde_json;
|
||||||
extern crate time;
|
extern crate time;
|
||||||
|
|
||||||
pub use error::Error;
|
pub use header::{Algorithm, Header};
|
||||||
pub use header::Algorithm;
|
|
||||||
pub use header::Header;
|
|
||||||
pub use payload::{DefaultPayload, Payload};
|
pub use payload::{DefaultPayload, Payload};
|
||||||
use serde::de::DeserializeOwned;
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
use serde::Serialize;
|
|
||||||
|
|
||||||
mod crypt;
|
mod crypt;
|
||||||
pub mod error;
|
mod error;
|
||||||
mod header;
|
mod header;
|
||||||
mod payload;
|
mod payload;
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
pub use error::Result;
|
||||||
|
|
||||||
/// A convenient type that binds the same type parameter for the custom claims, an empty tuple, as
|
/// A convenient type that binds the same type parameter for the custom claims, an empty tuple, as
|
||||||
/// `DefaultPayload` so that the two aliases may be used together to reduce boilerplate when no
|
/// `DefaultPayload` so that the two aliases may be used together to reduce boilerplate when no
|
||||||
|
@ -118,7 +117,7 @@ mod tests {
|
||||||
let token = DefaultToken::<()>::parse(raw).unwrap();
|
let token = DefaultToken::<()>::parse(raw).unwrap();
|
||||||
|
|
||||||
assert_eq!(token.header.alg, HS256);
|
assert_eq!(token.header.alg, HS256);
|
||||||
assert!(token.verify("secret".as_bytes()).unwrap());
|
assert!(token.verify(b"secret").unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -131,7 +130,7 @@ mod tests {
|
||||||
..DefaultPayload::default()
|
..DefaultPayload::default()
|
||||||
};
|
};
|
||||||
let token = Token::new(header, payload);
|
let token = Token::new(header, payload);
|
||||||
let key = "secret".as_bytes();
|
let key = b"secret";
|
||||||
let raw = token.sign(key).unwrap();
|
let raw = token.sign(key).unwrap();
|
||||||
let same = Token::parse(&*raw).unwrap();
|
let same = Token::parse(&*raw).unwrap();
|
||||||
|
|
||||||
|
@ -143,7 +142,7 @@ mod tests {
|
||||||
pub fn roundtrip_expired() {
|
pub fn roundtrip_expired() {
|
||||||
let now = time::now();
|
let now = time::now();
|
||||||
let token = create_for_range(now, now + Duration::minutes(-5));
|
let token = create_for_range(now, now + Duration::minutes(-5));
|
||||||
let key = "secret".as_bytes();
|
let key = b"secret";
|
||||||
let raw = token.sign(key).unwrap();
|
let raw = token.sign(key).unwrap();
|
||||||
let same = Token::parse(&*raw).unwrap();
|
let same = Token::parse(&*raw).unwrap();
|
||||||
|
|
||||||
|
@ -155,7 +154,7 @@ mod tests {
|
||||||
pub fn roundtrip_not_yet_valid() {
|
pub fn roundtrip_not_yet_valid() {
|
||||||
let now = time::now();
|
let now = time::now();
|
||||||
let token = create_for_range(now + Duration::minutes(5), now + Duration::minutes(10));
|
let token = create_for_range(now + Duration::minutes(5), now + Duration::minutes(10));
|
||||||
let key = "secret".as_bytes();
|
let key = b"secret";
|
||||||
let raw = token.sign(key).unwrap();
|
let raw = token.sign(key).unwrap();
|
||||||
let same = Token::parse(&*raw).unwrap();
|
let same = Token::parse(&*raw).unwrap();
|
||||||
|
|
||||||
|
@ -171,7 +170,7 @@ mod tests {
|
||||||
..Header::default()
|
..Header::default()
|
||||||
};
|
};
|
||||||
let token = DefaultToken {
|
let token = DefaultToken {
|
||||||
header: header,
|
header,
|
||||||
..Token::default()
|
..Token::default()
|
||||||
};
|
};
|
||||||
let raw = token
|
let raw = token
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
use super::Result;
|
use super::Result;
|
||||||
use base64::{decode_config, encode_config, URL_SAFE_NO_PAD};
|
use base64::{decode_config, encode_config, URL_SAFE_NO_PAD};
|
||||||
use error::Error;
|
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_json;
|
use serde_json;
|
||||||
|
@ -68,7 +67,7 @@ impl<T: Serialize + DeserializeOwned> Payload<T> {
|
||||||
let enc = encode_config((&*s).as_bytes(), URL_SAFE_NO_PAD);
|
let enc = encode_config((&*s).as_bytes(), URL_SAFE_NO_PAD);
|
||||||
Ok(enc)
|
Ok(enc)
|
||||||
} else {
|
} else {
|
||||||
Err(Error::Custom("Could not access custom claims.".to_owned()))
|
Err(format_err!("Could not access custom claims."))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
|
@ -78,9 +77,7 @@ impl<T: Serialize + DeserializeOwned> Payload<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Err(Error::Custom(
|
Err(format_err!("Could not access standard claims.",))
|
||||||
"Could not access standard claims.".to_owned(),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -234,10 +231,10 @@ mod tests {
|
||||||
fn create_default() -> DefaultPayload {
|
fn create_default() -> DefaultPayload {
|
||||||
DefaultPayload {
|
DefaultPayload {
|
||||||
aud: Some("login_service".into()),
|
aud: Some("login_service".into()),
|
||||||
iat: Some(1302317100),
|
iat: Some(1_302_317_100),
|
||||||
iss: Some("example.com".into()),
|
iss: Some("example.com".into()),
|
||||||
exp: Some(1302319100),
|
exp: Some(1_302_319_100),
|
||||||
nbf: Some(1302317100),
|
nbf: Some(1_302_317_100),
|
||||||
sub: Some("Random User".into()),
|
sub: Some("Random User".into()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
|
@ -246,8 +243,8 @@ mod tests {
|
||||||
fn create_custom() -> Payload<CustomClaims> {
|
fn create_custom() -> Payload<CustomClaims> {
|
||||||
Payload {
|
Payload {
|
||||||
iss: Some("example.com".into()),
|
iss: Some("example.com".into()),
|
||||||
iat: Some(1302317100),
|
iat: Some(1_302_317_100),
|
||||||
exp: Some(1302319100),
|
exp: Some(1_302_319_100),
|
||||||
claims: Some(CustomClaims {
|
claims: Some(CustomClaims {
|
||||||
user_id: "123456".into(),
|
user_id: "123456".into(),
|
||||||
is_admin: false,
|
is_admin: false,
|
||||||
|
|
Loading…
Reference in a new issue