Refactor header claims (#1)

Simplify customization, bump to 2.0.0.
This commit is contained in:
Thomas Gideon 2017-03-07 14:03:24 -05:00 committed by GitHub
parent 9df2ac741e
commit 3c9fd6b13b
11 changed files with 522 additions and 292 deletions

View file

@ -1,6 +1,6 @@
[package] [package]
name = "medallion" name = "medallion"
version = "1.1.1" version = "2.0.0"
authors = ["Thomas Gideon <cmdln@thecommandline.net>"] authors = ["Thomas Gideon <cmdln@thecommandline.net>"]
description = "JWT library for rust using serde, serde_json and openssl" description = "JWT library for rust using serde, serde_json and openssl"
homepage = "http://github.com/commandline/medallion" homepage = "http://github.com/commandline/medallion"

109
README.md
View file

@ -13,6 +13,113 @@ A JWT library for rust using serde, serde_json and openssl.
## Usage ## Usage
The library provides a `Token` type that wraps a header and claims. The claims can be any type that implements the `Component` trait, which is automatically implemented for types that implement the `Sized`, and must have the attribute `#[derive(Serialize, Deserialize)`. Header can be any type that implements `Component` and `Header`. `Header` ensures that the required algorithm is available for signing and verification. `HS256`, `HS384`, `HS512`, `RS256`, `RS384`, and `RS512` are supported. See the examples. The library provides a `Token` type that wraps headers and claims.
```rust
extern crate medallion;
use std::default::Default;
use medallion::{
Header,
DefaultPayload,
Token,
};
fn main() {
// will default to Algorithm::HS256
let header: Header<()> = Default::default();
let payload = DefaultPayload {
iss: Some("example.com".into()),
sub: Some("Random User".into()),
..Default::default()
};
let token = Token::new(header, payload);
token.sign(b"secret_key").unwrap();
}
```
The `Header` struct contains all of the headers of the JWT. It requires that a supported algorithm (`HS256`, `HS384`, `HS512`, `RS256`, `RS384`, and `RS512`) be specified. It requires a type for additional header fields. That type must implement serde's `Serialize` and `Deserialize` as well as `PartialEq`. These traits can usually be derived, e.g. `#[derive(PartialEq, Serialize, Deserialize)`.
```rust
extern crate medallion;
use std::default::Default;
use serde::{Serialize, Deserialize};
use medallion::{Header, DefaultPayload, Token};
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct CustomHeaders {
kid: String,
typ: String,
}
fn main() {
let header = Header {
headers: CustomHeaders {
kid: "0001",)
typ: "JWT",)
}
..Default::default()
}
let payload = DefaultPayload {
iss: Some("example.com".into()),
sub: Some("Random User".into()),
..Default::default()
};
let token = Token::new(header, payload);
token.sign(b"secret_key").unwrap();
}
```
The `Payload` struct contains all of the claims of the JWT. It provides the set of registered, public claims. Additional claims can be added by constructing the `Payload` with a generically typed value. That value's type must implement serde's `Serialize` and `Deserialize` as well as `PartialEq`. These traits can usually be derived, e.g. `#[derive(PartialEq, Serialize, Deserialize)`. A convenience type, `DefaultPayload`, is provided that binds the generic parameter of `Payload` to an empty tuple type.
```rust
extern crate medallion;
use std::default::Default;
use serde::{Serialize, Deserialize};
use medallion::{Header, DefaultPayload, Token};
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct CustomHeaders {
kid: String,
typ: String,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct CustomClaims {
user_id: u64,
email: String,
}
fn main() {
let header = Header {
headers: CustomHeaders {
kid: "0001",)
typ: "JWT",)
}
..Default::default()
}
let payload = DefaultPayload {
iss: Some("example.com".into()),
sub: Some("Random User".into()),
claims: CustomClaims {
user_id: 1234,
email: "random@example.com",
}
..Default::default()
};
let token = Token::new(header, payload);
token.sign(b"secret_key").unwrap();
}
```
See the examples for more detailed usage.
This library was originally forked from @mikkyang's rust-jwt. This library was originally forked from @mikkyang's rust-jwt.

View file

@ -4,39 +4,42 @@ extern crate serde_derive;
extern crate medallion; extern crate medallion;
use std::default::Default; use std::default::Default;
use medallion::{ use medallion::{Payload, Header, Token};
DefaultHeader,
Token,
};
#[derive(Default, Serialize, Deserialize)] #[derive(Default, Serialize, Deserialize, PartialEq, Debug)]
struct Custom { struct Custom {
sub: String, user_id: String,
// useful if you want a None to not appear in the serialized JSON
#[serde(skip_serializing_if = "Option::is_none")]
email: Option<String>,
rhino: bool, rhino: bool,
} }
fn new_token(user_id: &str, password: &str) -> Option<String> { fn new_token(user_id: &str, password: &str) -> Option<String> {
// Dummy auth // Dummy auth
if password != "password" { if password != "password" {
return None return None;
} }
let header: DefaultHeader = Default::default(); let header: Header<()> = Default::default();
let claims = Custom { let payload = Payload {
sub: user_id.into(), claims: Some(Custom {
rhino: true, user_id: user_id.into(),
rhino: true,
..Default::default()
}),
..Default::default() ..Default::default()
}; };
let token = Token::new(header, claims); let token = Token::new(header, payload);
token.signed(b"secret_key").ok() token.sign(b"secret_key").ok()
} }
fn login(token: &str) -> Option<String> { fn login(token: &str) -> Option<String> {
let token = Token::<DefaultHeader, Custom>::parse(token).unwrap(); let token = Token::<(), Custom>::parse(token).unwrap();
if token.verify(b"secret_key").unwrap() { if token.verify(b"secret_key").unwrap() {
Some(token.claims.sub) Some(token.payload.claims.unwrap().user_id)
} else { } else {
None None
} }

View file

@ -0,0 +1,49 @@
// need this for custom derivation
#[macro_use]
extern crate serde_derive;
extern crate medallion;
use std::default::Default;
use medallion::{DefaultPayload, Header, DefaultToken};
#[derive(Default, Serialize, Deserialize, PartialEq, Debug)]
struct Custom {
// useful if you want a None to not appear in the serialized JSON
#[serde(skip_serializing_if = "Option::is_none")]
kid: Option<String>,
typ: String,
}
fn new_token(sub: &str, password: &str) -> Option<String> {
// Dummy auth
if password != "password" {
return None;
}
let header = Header {
headers: Some(Custom { typ: "JWT".into(), ..Default::default() }),
..Default::default()
};
let payload = DefaultPayload { sub: Some(sub.into()), ..Default::default() };
let token = DefaultToken::new(header, payload);
token.sign(b"secret_key").ok()
}
fn login(token: &str) -> Option<String> {
let token = DefaultToken::<Custom>::parse(token).unwrap();
if token.verify(b"secret_key").unwrap() {
Some(token.payload.sub.unwrap())
} else {
None
}
}
fn main() {
let token = new_token("Random User", "password").unwrap();
let logged_in_user = login(&*token).unwrap();
assert_eq!(logged_in_user, "Random User");
}

View file

@ -1,34 +1,31 @@
extern crate medallion; extern crate medallion;
use std::default::Default; use std::default::Default;
use medallion::{ use medallion::{Header, DefaultPayload, DefaultToken};
DefaultHeader,
Registered,
Token,
};
fn new_token(user_id: &str, password: &str) -> Option<String> { fn new_token(user_id: &str, password: &str) -> Option<String> {
// Dummy auth // Dummy auth
if password != "password" { if password != "password" {
return None return None;
} }
let header: DefaultHeader = Default::default(); // can satisfy Header's generic parameter with an empty type
let claims = Registered { let header: Header<()> = Default::default();
let payload = DefaultPayload {
iss: Some("example.com".into()), iss: Some("example.com".into()),
sub: Some(user_id.into()), sub: Some(user_id.into()),
..Default::default() ..Default::default()
}; };
let token = Token::new(header, claims); let token = DefaultToken::new(header, payload);
token.signed(b"secret_key").ok() token.sign(b"secret_key").ok()
} }
fn login(token: &str) -> Option<String> { fn login(token: &str) -> Option<String> {
let token = Token::<DefaultHeader, Registered>::parse(token).unwrap(); let token: DefaultToken<()> = DefaultToken::parse(token).unwrap();
if token.verify(b"secret_key").unwrap() { if token.verify(b"secret_key").unwrap() {
token.claims.sub token.payload.sub
} else { } else {
None None
} }

View file

@ -3,12 +3,7 @@ extern crate medallion;
use std::default::Default; use std::default::Default;
use std::fs::File; use std::fs::File;
use std::io::{Error, Read}; use std::io::{Error, Read};
use medallion::{ use medallion::{Algorithm, Header, DefaultPayload, DefaultToken};
Algorithm,
DefaultHeader,
Registered,
Token,
};
fn load_pem(keypath: &str) -> Result<String, Error> { fn load_pem(keypath: &str) -> Result<String, Error> {
let mut key_file = File::open(keypath)?; let mut key_file = File::open(keypath)?;
@ -20,28 +15,28 @@ fn load_pem(keypath: &str) -> Result<String, Error> {
fn new_token(user_id: &str, password: &str) -> Option<String> { fn new_token(user_id: &str, password: &str) -> Option<String> {
// Dummy auth // Dummy auth
if password != "password" { if password != "password" {
return None return None;
} }
let header: DefaultHeader = DefaultHeader { // can satisfy Header's generic parameter with an empty type
alg: Algorithm::RS256, let header: Header<()> = Header { alg: Algorithm::RS256, ..Default::default() };
..Default::default() let payload: DefaultPayload = DefaultPayload {
};
let claims = Registered {
iss: Some("example.com".into()), iss: Some("example.com".into()),
sub: Some(user_id.into()), sub: Some(user_id.into()),
..Default::default() ..Default::default()
}; };
let token = Token::new(header, claims); let token = DefaultToken::new(header, payload);
token.signed(load_pem("./privateKey.pem").unwrap().as_bytes()).ok() // this key was generated explicitly for these examples and is not used anywhere else
token.sign(load_pem("./privateKey.pem").unwrap().as_bytes()).ok()
} }
fn login(token: &str) -> Option<String> { fn login(token: &str) -> Option<String> {
let token = Token::<DefaultHeader, Registered>::parse(token).unwrap(); let token: DefaultToken<()> = DefaultToken::parse(token).unwrap();
// this key was generated explicitly for these examples and is not used anywhere else
if token.verify(load_pem("./publicKey.pub").unwrap().as_bytes()).unwrap() { if token.verify(load_pem("./publicKey.pub").unwrap().as_bytes()).unwrap() {
token.claims.sub token.payload.sub
} else { } else {
None None
} }

View file

@ -1,129 +0,0 @@
use base64::{decode_config, encode_config, URL_SAFE_NO_PAD};
use Component;
use error::Error;
use serde::{Deserialize, Serialize};
use serde_json;
use serde_json::value::{Value};
use super::Result;
/// A default claim set, including the standard, or registered, claims and the ability to specify
/// your own as private claims.
#[derive(Debug, Default, PartialEq)]
pub struct Claims<T: Serialize + Deserialize> {
pub reg: Registered,
pub private: T
}
/// The registered claims from the spec.
#[derive(Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct Registered {
pub iss: Option<String>,
pub sub: Option<String>,
pub aud: Option<String>,
pub exp: Option<u64>,
pub nbf: Option<u64>,
pub iat: Option<u64>,
pub jti: Option<String>,
}
impl<T: Serialize + Deserialize> Claims<T>{
/// Convenience factory method
pub fn new(reg: Registered, private: T) -> Claims<T> {
Claims {
reg: reg,
private: private
}
}
}
impl<T: Serialize + Deserialize> Component for Claims<T> {
/// This implementation simply parses the base64 data twice, each time applying it to the
/// registered and private claims.
fn from_base64(raw: &str) -> Result<Claims<T>> {
let data = decode_config(raw, URL_SAFE_NO_PAD)?;
let reg_claims: Registered = serde_json::from_slice(&data)?;
let pri_claims: T = serde_json::from_slice(&data)?;
Ok(Claims {
reg: reg_claims,
private: pri_claims
})
}
/// Renders both the registered and private claims into a single consolidated JSON
/// representation before encoding.
fn to_base64(&self) -> Result<String> {
if let Value::Object(mut reg_map) = serde_json::to_value(&self.reg)? {
if let Value::Object(pri_map) = serde_json::to_value(&self.private)? {
reg_map.extend(pri_map);
let s = serde_json::to_string(&reg_map)?;
let enc = encode_config((&*s).as_bytes(), URL_SAFE_NO_PAD);
Ok(enc)
} else {
Err(Error::Custom("Could not access registered claims.".to_owned()))
}
} else {
Err(Error::Custom("Could not access private claims.".to_owned()))
}
}
}
#[cfg(test)]
mod tests {
use std::default::Default;
use claims::{Claims, Registered};
use Component;
#[derive(Default, Debug, Serialize, Deserialize, PartialEq)]
struct EmptyClaim { }
#[derive(Default, Debug, Serialize, Deserialize, PartialEq)]
struct NonEmptyClaim {
user_id: String,
is_admin: bool,
first_name: Option<String>,
last_name: Option<String>
}
#[test]
fn from_base64() {
let enc = "eyJpc3MiOiJleGFtcGxlLmNvbSIsImV4cCI6MTMwMjMxOTEwMH0";
let claims: Claims<EmptyClaim> = Claims::from_base64(enc).unwrap();
assert_eq!(claims.reg.iss.unwrap(), "example.com");
assert_eq!(claims.reg.exp.unwrap(), 1302319100);
}
#[test]
fn multiple_types() {
let enc = "eyJpc3MiOiJleGFtcGxlLmNvbSIsImV4cCI6MTMwMjMxOTEwMH0";
let claims = Registered::from_base64(enc).unwrap();
assert_eq!(claims.iss.unwrap(), "example.com");
assert_eq!(claims.exp.unwrap(), 1302319100);
}
#[test]
fn roundtrip() {
let mut claims: Claims<EmptyClaim> = Default::default();
claims.reg.iss = Some("example.com".into());
claims.reg.exp = Some(1302319100);
let enc = claims.to_base64().unwrap();
assert_eq!(claims, Claims::from_base64(&*enc).unwrap());
}
#[test]
fn roundtrip_custom() {
let mut claims: Claims<NonEmptyClaim> = Default::default();
claims.reg.iss = Some("example.com".into());
claims.reg.exp = Some(1302319100);
claims.private.user_id = "123456".into();
claims.private.is_admin = false;
claims.private.first_name = Some("Random".into());
let enc = claims.to_base64().unwrap();
assert_eq!(claims, Claims::<NonEmptyClaim>::from_base64(&*enc).unwrap());
}
}

View file

@ -77,9 +77,6 @@ pub mod tests {
use std::fs::File; use std::fs::File;
use super::{sign, verify}; use super::{sign, verify};
#[derive(Default, Debug, Serialize, Deserialize, PartialEq)]
struct EmptyClaim { }
#[test] #[test]
pub fn sign_data_hmac() { pub fn sign_data_hmac() {
let header = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"; let header = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9";

View file

@ -1,19 +1,21 @@
use base64::{encode_config, decode_config, URL_SAFE_NO_PAD};
use serde::{Serialize, Deserialize};
use serde_json::{self, Value};
use std::default::Default; use std::default::Default;
use Header;
/// A default Header providing the type, key id and algorithm fields. use super::error::Error;
use super::Result;
/// A extensible Header that provides only algorithm field and allows for additional fields to be
/// passed in via a struct that can be serialized and deserialized. Unlike the Claims struct, there
/// is no convenience type alias because headers seem to vary much more greatly in practice
/// depending on the application whereas claims seem to be shared as a function of registerest and
/// public claims.
#[derive(Debug, PartialEq, Serialize, Deserialize)] #[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct DefaultHeader { pub struct Header<T: Serialize + Deserialize> {
pub typ: Option<HeaderType>,
pub kid: Option<String>,
pub alg: Algorithm, pub alg: Algorithm,
} #[serde(skip_serializing)]
pub headers: Option<T>,
/// Default value for the header type field.
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub enum HeaderType {
JWT,
} }
/// Supported algorithms, each representing a valid signature and digest combination. /// Supported algorithms, each representing a valid signature and digest combination.
@ -24,56 +26,126 @@ pub enum Algorithm {
HS512, HS512,
RS256, RS256,
RS384, RS384,
RS512 RS512,
} }
impl Default for DefaultHeader { impl<T: Serialize + Deserialize> Header<T> {
fn default() -> DefaultHeader { pub fn from_base64(raw: &str) -> Result<Header<T>> {
DefaultHeader { let data = decode_config(raw, URL_SAFE_NO_PAD)?;
typ: Some(HeaderType::JWT), let own: Header<T> = serde_json::from_slice(&data)?;
kid: None,
alg: Algorithm::HS256, let headers: Option<T> = serde_json::from_slice(&data).ok();
Ok(Header {
alg: own.alg,
headers: headers,
})
}
/// Encode to a string.
pub fn to_base64(&self) -> Result<String> {
if let Value::Object(mut own_map) = serde_json::to_value(&self)? {
match self.headers {
Some(ref headers) => {
if let Value::Object(extra_map) = serde_json::to_value(&headers)? {
own_map.extend(extra_map);
let s = serde_json::to_string(&own_map)?;
let enc = encode_config((&*s).as_bytes(), URL_SAFE_NO_PAD);
Ok(enc)
} else {
Err(Error::Custom("Could not access additional headers.".to_owned()))
}
}
None => {
let s = serde_json::to_string(&own_map)?;
let enc = encode_config((&*s).as_bytes(), URL_SAFE_NO_PAD);
Ok(enc)
}
}
} else {
Err(Error::Custom("Could not access default header.".to_owned()))
} }
} }
} }
/// Allow the rest of the library to access the configured algorithm without having to know the impl<T: Serialize + Deserialize> Default for Header<T> {
/// specific type for the header. fn default() -> Header<T> {
impl Header for DefaultHeader { Header {
fn alg(&self) -> &Algorithm { alg: Algorithm::HS256,
&(self.alg) headers: None,
}
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use Component; use super::{Algorithm, Header};
use header::{
Algorithm, #[derive(Debug, Serialize, Deserialize, PartialEq)]
DefaultHeader, struct CustomHeaders {
HeaderType, kid: String,
}; typ: String,
}
#[test] #[test]
fn from_base64() { fn from_base64() {
let enc = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"; let enc = "eyJhbGciOiJIUzI1NiJ9";
let header = DefaultHeader::from_base64(enc).unwrap(); let header: Header<()> = Header::from_base64(enc).unwrap();
assert_eq!(header.typ.unwrap(), HeaderType::JWT);
assert_eq!(header.alg, Algorithm::HS256); assert_eq!(header.alg, Algorithm::HS256);
}
#[test]
fn custom_from_base64() {
let enc = "eyJhbGciOiJIUzI1NiIsImtpZCI6IjFLU0YzZyIsInR5cCI6IkpXVCJ9";
let header: Header<CustomHeaders> = Header::from_base64(enc).unwrap();
let enc = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjFLU0YzZyJ9"; let headers = header.headers.unwrap();
let header = DefaultHeader::from_base64(enc).unwrap(); assert_eq!(headers.kid, "1KSF3g".to_string());
assert_eq!(headers.typ, "JWT".to_string());
assert_eq!(header.alg, Algorithm::HS256);
}
assert_eq!(header.kid.unwrap(), "1KSF3g".to_string()); #[test]
assert_eq!(header.alg, Algorithm::RS256); fn to_base64() {
let enc = "eyJhbGciOiJIUzI1NiJ9";
let header: Header<()> = Default::default();
assert_eq!(enc, header.to_base64().unwrap());
}
#[test]
fn custom_to_base64() {
let enc = "eyJhbGciOiJIUzI1NiIsImtpZCI6IjFLU0YzZyIsInR5cCI6IkpXVCJ9";
let header: Header<CustomHeaders> = Header {
headers: Some(CustomHeaders {
kid: "1KSF3g".into(),
typ: "JWT".into(),
}),
..Default::default()
};
assert_eq!(enc, header.to_base64().unwrap());
} }
#[test] #[test]
fn roundtrip() { fn roundtrip() {
let header: DefaultHeader = Default::default(); let header: Header<()> = Default::default();
let enc = Component::to_base64(&header).unwrap(); let enc = header.to_base64().unwrap();
assert_eq!(header, DefaultHeader::from_base64(&*enc).unwrap()); assert_eq!(header, Header::from_base64(&*enc).unwrap());
}
#[test]
fn roundtrip_custom() {
let header: Header<CustomHeaders> = Header {
alg: Algorithm::RS512,
headers: Some(CustomHeaders {
kid: "1KSF3g".into(),
typ: "JWT".into(),
}),
};
let enc = header.to_base64().unwrap();
assert_eq!(header, Header::from_base64(&*enc).unwrap());
} }
} }

View file

@ -8,69 +8,45 @@ extern crate serde;
extern crate serde_derive; extern crate serde_derive;
extern crate serde_json; extern crate serde_json;
use base64::{decode_config, encode_config, URL_SAFE_NO_PAD};
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
pub use error::Error; pub use error::Error;
pub use header::DefaultHeader; pub use header::Header;
pub use header::Algorithm; pub use header::Algorithm;
pub use claims::Claims; pub use payload::{Payload, DefaultPayload};
pub use claims::Registered;
pub mod error; pub mod error;
pub mod header; mod header;
pub mod claims; mod payload;
mod crypt; mod crypt;
pub type Result<T> = std::result::Result<T, Error>; pub type Result<T> = std::result::Result<T, Error>;
/// A convenient type that bins 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 not
/// custom claims are needed.
pub type DefaultToken<H> = Token<H, ()>;
/// Main struct representing a JSON Web Token, composed of a header and a set of claims. /// Main struct representing a JSON Web Token, composed of a header and a set of claims.
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct Token<H, C> pub struct Token<H, C>
where H: Component, C: Component { where H: Serialize + Deserialize + PartialEq,
C: Serialize + Deserialize + PartialEq
{
raw: Option<String>, raw: Option<String>,
pub header: H, pub header: Header<H>,
pub claims: C, pub payload: Payload<C>,
}
/// Any header type must implement this trait so that signing and verification work.
pub trait Header {
fn alg(&self) -> &header::Algorithm;
}
/// Any header or claims type must implement this trait in order to serialize and deserialize
/// correctly.
pub trait Component: Sized {
fn from_base64(raw: &str) -> Result<Self>;
fn to_base64(&self) -> Result<String>;
}
/// Provide a default implementation that should work in almost all cases.
impl<T> Component for T
where T: Serialize + Deserialize + Sized {
/// Parse from a string.
fn from_base64(raw: &str) -> Result<T> {
let data = decode_config(raw, URL_SAFE_NO_PAD)?;
let s = String::from_utf8(data)?;
Ok(serde_json::from_str(&*s)?)
}
/// Encode to a string.
fn to_base64(&self) -> Result<String> {
let s = serde_json::to_string(&self)?;
let enc = encode_config((&*s).as_bytes(), URL_SAFE_NO_PAD);
Ok(enc)
}
} }
/// Provide the ability to parse a token, verify it and sign/serialize it. /// Provide the ability to parse a token, verify it and sign/serialize it.
impl<H, C> Token<H, C> impl<H, C> Token<H, C>
where H: Component + Header, C: Component { where H: Serialize + Deserialize + PartialEq,
pub fn new(header: H, claims: C) -> Token<H, C> { C: Serialize + Deserialize + PartialEq
{
pub fn new(header: Header<H>, payload: Payload<C>) -> Token<H, C> {
Token { Token {
raw: None, raw: None,
header: header, header: header,
claims: claims, payload: payload,
} }
} }
@ -80,8 +56,8 @@ impl<H, C> Token<H, C>
Ok(Token { Ok(Token {
raw: Some(raw.into()), raw: Some(raw.into()),
header: Component::from_base64(pieces[0])?, header: Header::from_base64(pieces[0])?,
claims: Component::from_base64(pieces[1])?, payload: Payload::from_base64(pieces[1])?,
}) })
} }
@ -96,44 +72,42 @@ impl<H, C> Token<H, C>
let sig = pieces[0]; let sig = pieces[0];
let data = pieces[1]; let data = pieces[1];
Ok(crypt::verify(sig, data, key, &self.header.alg())?) Ok(crypt::verify(sig, data, key, &self.header.alg)?)
} }
/// Generate the signed token from a key with the specific algorithm as a url-safe, base64 /// Generate the signed token from a key with the specific algorithm as a url-safe, base64
/// string. /// string.
pub fn signed(&self, key: &[u8]) -> Result<String> { pub fn sign(&self, key: &[u8]) -> Result<String> {
let header = Component::to_base64(&self.header)?; let header = self.header.to_base64()?;
let claims = self.claims.to_base64()?; let payload = self.payload.to_base64()?;
let data = format!("{}.{}", header, claims); let data = format!("{}.{}", header, payload);
let sig = crypt::sign(&*data, key, &self.header.alg())?; let sig = crypt::sign(&*data, key, &self.header.alg)?;
Ok(format!("{}.{}", data, sig)) Ok(format!("{}.{}", data, sig))
} }
} }
impl<H, C> PartialEq for Token<H, C> impl<H, C> PartialEq for Token<H, C>
where H: Component + PartialEq, C: Component + PartialEq{ where H: Serialize + Deserialize + PartialEq,
C: Serialize + Deserialize + PartialEq
{
fn eq(&self, other: &Token<H, C>) -> bool { fn eq(&self, other: &Token<H, C>) -> bool {
self.header == other.header && self.header == other.header && self.payload == other.payload
self.claims == other.claims
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use Claims; use {DefaultToken, Header};
use Token;
use crypt::tests::load_pem; use crypt::tests::load_pem;
use header::Algorithm::{HS256,RS512}; use super::Algorithm::{HS256, RS512};
use header::DefaultHeader;
#[derive(Default, Debug, Serialize, Deserialize, PartialEq)]
struct EmptyClaim { }
#[test] #[test]
pub fn raw_data() { pub fn raw_data() {
let raw = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"; let raw = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.\
let token = Token::<DefaultHeader, Claims<EmptyClaim>>::parse(raw).unwrap(); eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.\
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ";
let token = DefaultToken::<()>::parse(raw).unwrap();
{ {
assert_eq!(token.header.alg, HS256); assert_eq!(token.header.alg, HS256);
@ -143,10 +117,10 @@ mod tests {
#[test] #[test]
pub fn roundtrip_hmac() { pub fn roundtrip_hmac() {
let token: Token<DefaultHeader, Claims<EmptyClaim>> = Default::default(); let token: DefaultToken<()> = Default::default();
let key = "secret".as_bytes(); let key = "secret".as_bytes();
let raw = token.signed(key).unwrap(); let raw = token.sign(key).unwrap();
let same = Token::parse(&*raw).unwrap(); let same = DefaultToken::parse(&*raw).unwrap();
assert_eq!(token, same); assert_eq!(token, same);
assert!(same.verify(key).unwrap()); assert!(same.verify(key).unwrap());
@ -154,16 +128,11 @@ mod tests {
#[test] #[test]
pub fn roundtrip_rsa() { pub fn roundtrip_rsa() {
let token: Token<DefaultHeader, Claims<EmptyClaim>> = Token { let header: Header<()> = Header { alg: RS512, ..Default::default() };
header: DefaultHeader { let token = DefaultToken { header: header, ..Default::default() };
alg: RS512,
..Default::default()
},
..Default::default()
};
let private_key = load_pem("./examples/privateKey.pem").unwrap(); let private_key = load_pem("./examples/privateKey.pem").unwrap();
let raw = token.signed(private_key.as_bytes()).unwrap(); let raw = token.sign(private_key.as_bytes()).unwrap();
let same = Token::parse(&*raw).unwrap(); let same = DefaultToken::parse(&*raw).unwrap();
assert_eq!(token, same); assert_eq!(token, same);
let public_key = load_pem("./examples/publicKey.pub").unwrap(); let public_key = load_pem("./examples/publicKey.pub").unwrap();

170
src/payload.rs Normal file
View file

@ -0,0 +1,170 @@
use base64::{decode_config, encode_config, URL_SAFE_NO_PAD};
use error::Error;
use serde::{Deserialize, Serialize};
use serde_json;
use serde_json::value::Value;
use super::Result;
/// A default claim set, including the standard, or registered, claims and the ability to specify
/// your own as custom claims.
#[derive(Debug, Serialize, Deserialize, Default, PartialEq)]
pub struct Payload<T: Serialize + Deserialize + PartialEq> {
#[serde(skip_serializing_if = "Option::is_none")]
pub iss: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sub: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub aud: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub exp: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub nbf: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub iat: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub jti: Option<String>,
#[serde(skip_serializing)]
pub claims: Option<T>,
}
/// A convenient type alias that assumes the standard claims are sufficient, the empty tuple type
/// satisfies Claims' generic parameter as simply and clearly as possible.
pub type DefaultPayload = Payload<()>;
impl<T: Serialize + Deserialize + PartialEq> Payload<T> {
/// This implementation simply parses the base64 data twice, first parsing out the standard
/// claims then any custom claims, assigning the latter into a copy of the former before
/// returning registered and custom claims.
pub fn from_base64(raw: &str) -> Result<Payload<T>> {
let data = decode_config(raw, URL_SAFE_NO_PAD)?;
let claims: Payload<T> = serde_json::from_slice(&data)?;
let custom: Option<T> = serde_json::from_slice(&data).ok();
Ok(Payload {
iss: claims.iss,
sub: claims.sub,
aud: claims.aud,
exp: claims.exp,
nbf: claims.nbf,
iat: claims.iat,
jti: claims.jti,
claims: custom,
})
}
/// Renders both the standard and custom claims into a single consolidated JSON representation
/// before encoding.
pub fn to_base64(&self) -> Result<String> {
if let Value::Object(mut claims_map) = serde_json::to_value(&self)? {
match self.claims {
Some(ref custom) => {
if let Value::Object(custom_map) = serde_json::to_value(&custom)? {
claims_map.extend(custom_map);
let s = serde_json::to_string(&claims_map)?;
let enc = encode_config((&*s).as_bytes(), URL_SAFE_NO_PAD);
Ok(enc)
} else {
Err(Error::Custom("Could not access custom claims.".to_owned()))
}
}
None => {
let s = serde_json::to_string(&claims_map)?;
let enc = encode_config((&*s).as_bytes(), URL_SAFE_NO_PAD);
return Ok(enc);
}
}
} else {
Err(Error::Custom("Could not access standard claims.".to_owned()))
}
}
}
#[cfg(test)]
mod tests {
use std::default::Default;
use super::{Payload, DefaultPayload};
#[derive(Default, Debug, Serialize, Deserialize, PartialEq)]
struct CustomClaims {
user_id: String,
is_admin: bool,
first_name: Option<String>,
last_name: Option<String>,
}
#[test]
fn from_base64() {
let enc = "eyJhdWQiOiJsb2dpbl9zZXJ2aWNlIiwiZXhwIjoxMzAyMzE5MTAwLCJpYXQiOjEzMDIzMTcxMDAsImlzcyI6ImV4YW1wbGUuY29tIiwibmJmIjoxMzAyMzE3MTAwLCJzdWIiOiJSYW5kb20gVXNlciJ9";
let payload: DefaultPayload = Payload::from_base64(enc).unwrap();
assert_eq!(payload, create_default());
}
#[test]
fn custom_from_base64() {
let enc = "eyJleHAiOjEzMDIzMTkxMDAsImZpcnN0X25hbWUiOiJSYW5kb20iLCJpYXQiOjEzMDIzMTcxMDAsImlzX2FkbWluIjpmYWxzZSwiaXNzIjoiZXhhbXBsZS5jb20iLCJsYXN0X25hbWUiOiJVc2VyIiwidXNlcl9pZCI6IjEyMzQ1NiJ9";
let payload: Payload<CustomClaims> = Payload::from_base64(enc).unwrap();
assert_eq!(payload, create_custom());
}
#[test]
fn to_base64() {
let enc = "eyJhdWQiOiJsb2dpbl9zZXJ2aWNlIiwiZXhwIjoxMzAyMzE5MTAwLCJpYXQiOjEzMDIzMTcxMDAsImlzcyI6ImV4YW1wbGUuY29tIiwibmJmIjoxMzAyMzE3MTAwLCJzdWIiOiJSYW5kb20gVXNlciJ9";
let payload = create_default();
assert_eq!(enc, payload.to_base64().unwrap());
}
#[test]
fn custom_to_base64() {
let enc = "eyJleHAiOjEzMDIzMTkxMDAsImZpcnN0X25hbWUiOiJSYW5kb20iLCJpYXQiOjEzMDIzMTcxMDAsImlzX2FkbWluIjpmYWxzZSwiaXNzIjoiZXhhbXBsZS5jb20iLCJsYXN0X25hbWUiOiJVc2VyIiwidXNlcl9pZCI6IjEyMzQ1NiJ9";
let payload = create_custom();
assert_eq!(enc, payload.to_base64().unwrap());
}
#[test]
fn roundtrip() {
let payload = create_default();
let enc = payload.to_base64().unwrap();
assert_eq!(payload, Payload::from_base64(&*enc).unwrap());
}
#[test]
fn roundtrip_custom() {
let payload = create_custom();
let enc = payload.to_base64().unwrap();
assert_eq!(payload, Payload::<CustomClaims>::from_base64(&*enc).unwrap());
}
fn create_default() -> DefaultPayload {
DefaultPayload {
aud: Some("login_service".into()),
iat: Some(1302317100),
iss: Some("example.com".into()),
exp: Some(1302319100),
nbf: Some(1302317100),
sub: Some("Random User".into()),
..Default::default()
}
}
fn create_custom() -> Payload<CustomClaims> {
Payload {
iss: Some("example.com".into()),
iat: Some(1302317100),
exp: Some(1302319100),
claims: Some(CustomClaims {
user_id: "123456".into(),
is_admin: false,
first_name: Some("Random".into()),
last_name: Some("User".into()),
}),
..Default::default()
}
}
}