medallion/src/lib.rs

188 lines
5.9 KiB
Rust
Raw Normal View History

2017-02-15 20:42:06 +00:00
#![crate_name = "medallion"]
#![crate_type = "lib"]
#![doc(html_root_url = "https://commandline.github.io/medallion/")]
2018-03-12 13:43:57 +00:00
///! A crate for working with JSON `WebTokens` that use OpenSSL for RSA signing and encryption and
///! `serde` and `serde_json` for JSON encoding and decoding.
///!
///! Tries to support the standard uses for JWTs while providing reasonable ways to extend,
///! primarily by adding custom headers and claims to tokens.
pub use header::{Algorithm, Header};
2018-03-12 13:43:57 +00:00
pub use payload::{DefaultPayload, Payload};
use serde::{de::DeserializeOwned, Serialize};
2017-02-13 23:40:07 +00:00
mod crypt;
mod header;
mod payload;
2017-02-13 23:40:07 +00:00
pub use anyhow::Result;
2017-02-17 16:53:12 +00:00
/// A convenient type that binds the same type parameter for the custom claims, an empty tuple, as
2018-03-12 13:43:57 +00:00
/// `DefaultPayload` so that the two aliases may be used together to reduce boilerplate when no
/// custom claims are needed.
pub type DefaultToken<H> = Token<H, ()>;
2017-02-15 20:41:52 +00:00
/// Main struct representing a JSON Web Token, composed of a header and a set of claims.
2017-02-13 23:40:07 +00:00
#[derive(Debug, Default)]
pub struct Token<H = (), C = ()> {
2017-02-13 23:40:07 +00:00
raw: Option<String>,
pub header: Header<H>,
pub payload: Payload<C>,
2017-02-13 23:40:07 +00:00
}
2017-02-15 20:41:52 +00:00
/// Provide the ability to parse a token, verify it and sign/serialize it.
2017-02-13 23:40:07 +00:00
impl<H, C> Token<H, C>
2018-03-12 13:43:57 +00:00
where
H: Serialize + DeserializeOwned,
C: Serialize + DeserializeOwned,
{
pub fn new(header: Header<H>, payload: Payload<C>) -> Token<H, C> {
2017-02-13 23:40:07 +00:00
Token {
raw: None,
2018-03-12 13:43:57 +00:00
header,
payload,
2017-02-13 23:40:07 +00:00
}
}
/// Parse a token from a string.
2017-02-17 16:53:12 +00:00
pub fn parse(raw: &str) -> Result<Token<H, C>> {
2017-02-13 23:40:07 +00:00
let pieces: Vec<_> = raw.split('.').collect();
Ok(Token {
raw: Some(raw.to_owned()),
header: Header::from_base64(pieces[0])?,
payload: Payload::from_base64(pieces[1])?,
2017-02-13 23:40:07 +00:00
})
}
2017-02-15 20:41:52 +00:00
/// Verify a token with a key and the token's specific algorithm.
2017-02-17 16:53:12 +00:00
pub fn verify(&self, key: &[u8]) -> Result<bool> {
2017-02-13 23:40:07 +00:00
let raw = match self.raw {
Some(ref s) => s,
2017-02-17 16:53:12 +00:00
None => return Ok(false),
2017-02-13 23:40:07 +00:00
};
let pieces: Vec<_> = raw.rsplitn(2, '.').collect();
let sig = pieces[0];
let data = pieces[1];
Ok(self.payload.verify() && crypt::verify(sig, data, key, &self.header.alg)?)
2017-02-13 23:40:07 +00:00
}
2017-02-15 20:41:52 +00:00
/// Generate the signed token from a key with the specific algorithm as a url-safe, base64
/// string.
pub fn sign(&self, key: &[u8]) -> Result<String> {
let header = self.header.to_base64()?;
let payload = self.payload.to_base64()?;
let data = format!("{}.{}", header, payload);
2017-02-13 23:40:07 +00:00
let sig = crypt::sign(&*data, key, &self.header.alg)?;
2017-02-13 23:40:07 +00:00
Ok(format!("{}.{}", data, sig))
}
}
impl<H, C> PartialEq for Token<H, C>
2018-03-12 13:43:57 +00:00
where
H: PartialEq,
C: PartialEq,
{
2017-02-13 23:40:07 +00:00
fn eq(&self, other: &Token<H, C>) -> bool {
self.header == other.header && self.payload == other.payload
2017-02-13 23:40:07 +00:00
}
}
#[cfg(test)]
mod tests {
use super::Algorithm::{HS256, RS512};
use crate::{DefaultPayload, DefaultToken, Header, Payload, Token};
use anyhow::Result;
use std::convert::TryInto;
use time::{Duration, OffsetDateTime};
2017-02-14 18:33:00 +00:00
2017-02-13 23:40:07 +00:00
#[test]
pub fn raw_data() {
let raw = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.\
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.\
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ";
let token = DefaultToken::<()>::parse(raw).unwrap();
2017-02-13 23:40:07 +00:00
assert_eq!(token.header.alg, HS256);
assert!(token.verify(b"secret").unwrap());
2017-02-13 23:40:07 +00:00
}
#[test]
pub fn roundtrip_hmac() {
let now = OffsetDateTime::now_utc();
let header: Header<()> = Header::default();
let payload = DefaultPayload {
nbf: Some(now.unix_timestamp().try_into().unwrap()),
exp: Some((now + Duration::minutes(5)).unix_timestamp().try_into().unwrap()),
..DefaultPayload::default()
};
let token = Token::new(header, payload);
let key = b"secret";
let raw = token.sign(key).unwrap();
let same = Token::parse(&*raw).unwrap();
2017-02-13 23:40:07 +00:00
assert_eq!(token, same);
2017-02-17 16:53:12 +00:00
assert!(same.verify(key).unwrap());
2017-02-13 23:40:07 +00:00
}
#[test]
pub fn roundtrip_expired() -> Result<()> {
let now = OffsetDateTime::now_utc();
let token = create_for_range(now, now + Duration::minutes(-5))?;
let key = b"secret";
let raw = token.sign(key)?;
let same = Token::parse(&*raw).unwrap();
assert_eq!(token, same);
assert_eq!(false, same.verify(key).unwrap());
Ok(())
}
#[test]
pub fn roundtrip_not_yet_valid() -> Result<()> {
let now = OffsetDateTime::now_utc();
let token = create_for_range(now + Duration::minutes(5), now + Duration::minutes(10))?;
let key = b"secret";
let raw = token.sign(key)?;
let same = Token::parse(&*raw).unwrap();
assert_eq!(token, same);
assert_eq!(false, same.verify(key).unwrap());
Ok(())
}
2017-02-13 23:40:07 +00:00
#[test]
pub fn roundtrip_rsa() {
let rsa_keypair = openssl::rsa::Rsa::generate(2048).unwrap();
2018-03-12 13:43:57 +00:00
let header: Header<()> = Header {
alg: RS512,
..Header::default()
2018-03-12 13:43:57 +00:00
};
let token = DefaultToken {
header,
..Token::default()
2018-03-12 13:43:57 +00:00
};
let raw = token
.sign(&rsa_keypair.private_key_to_pem().unwrap())
.unwrap();
let same = Token::parse(&*raw).unwrap();
2017-02-13 23:40:07 +00:00
assert_eq!(token, same);
assert!(same
.verify(&rsa_keypair.public_key_to_pem().unwrap())
.unwrap());
2017-02-13 23:40:07 +00:00
}
fn create_for_range(nbf: OffsetDateTime, exp: OffsetDateTime) -> Result<Token> {
let header: Header = Header::default();
let payload = Payload {
nbf: Some(nbf.unix_timestamp().try_into()?),
exp: Some(exp.unix_timestamp().try_into()?),
..Payload::default()
};
Ok(Token::new(header, payload))
}
2017-02-13 23:40:07 +00:00
}