From 3c9fd6b13b38e8b114f3bf1524f99d3d29943199 Mon Sep 17 00:00:00 2001 From: Thomas Gideon Date: Tue, 7 Mar 2017 14:03:24 -0500 Subject: [PATCH] Refactor header claims (#1) Simplify customization, bump to 2.0.0. --- Cargo.toml | 2 +- README.md | 109 +++++++++++++++++++++++- examples/custom_claims.rs | 33 +++---- examples/custom_headers.rs | 49 +++++++++++ examples/hs256.rs | 21 ++--- examples/rs256.rs | 27 +++--- src/claims.rs | 129 ---------------------------- src/crypt.rs | 3 - src/header.rs | 152 ++++++++++++++++++++++++--------- src/lib.rs | 119 ++++++++++---------------- src/payload.rs | 170 +++++++++++++++++++++++++++++++++++++ 11 files changed, 522 insertions(+), 292 deletions(-) create mode 100644 examples/custom_headers.rs delete mode 100644 src/claims.rs create mode 100644 src/payload.rs diff --git a/Cargo.toml b/Cargo.toml index 1494041..98e02b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "medallion" -version = "1.1.1" +version = "2.0.0" authors = ["Thomas Gideon "] description = "JWT library for rust using serde, serde_json and openssl" homepage = "http://github.com/commandline/medallion" diff --git a/README.md b/README.md index cb7446f..583ad7e 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,113 @@ A JWT library for rust using serde, serde_json and openssl. ## 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. diff --git a/examples/custom_claims.rs b/examples/custom_claims.rs index 8432110..c14cd9e 100644 --- a/examples/custom_claims.rs +++ b/examples/custom_claims.rs @@ -4,39 +4,42 @@ extern crate serde_derive; extern crate medallion; use std::default::Default; -use medallion::{ - DefaultHeader, - Token, -}; +use medallion::{Payload, Header, Token}; -#[derive(Default, Serialize, Deserialize)] +#[derive(Default, Serialize, Deserialize, PartialEq, Debug)] 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, rhino: bool, } fn new_token(user_id: &str, password: &str) -> Option { // Dummy auth if password != "password" { - return None + return None; } - let header: DefaultHeader = Default::default(); - let claims = Custom { - sub: user_id.into(), - rhino: true, + let header: Header<()> = Default::default(); + let payload = Payload { + claims: Some(Custom { + user_id: user_id.into(), + rhino: true, + ..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 { - let token = Token::::parse(token).unwrap(); + let token = Token::<(), Custom>::parse(token).unwrap(); if token.verify(b"secret_key").unwrap() { - Some(token.claims.sub) + Some(token.payload.claims.unwrap().user_id) } else { None } diff --git a/examples/custom_headers.rs b/examples/custom_headers.rs new file mode 100644 index 0000000..baebf54 --- /dev/null +++ b/examples/custom_headers.rs @@ -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, + typ: String, +} + +fn new_token(sub: &str, password: &str) -> Option { + // 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 { + let token = DefaultToken::::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"); +} diff --git a/examples/hs256.rs b/examples/hs256.rs index cd417f7..b84fc8a 100644 --- a/examples/hs256.rs +++ b/examples/hs256.rs @@ -1,34 +1,31 @@ extern crate medallion; use std::default::Default; -use medallion::{ - DefaultHeader, - Registered, - Token, -}; +use medallion::{Header, DefaultPayload, DefaultToken}; fn new_token(user_id: &str, password: &str) -> Option { // Dummy auth if password != "password" { - return None + return None; } - let header: DefaultHeader = Default::default(); - let claims = Registered { + // can satisfy Header's generic parameter with an empty type + let header: Header<()> = Default::default(); + let payload = DefaultPayload { iss: Some("example.com".into()), sub: Some(user_id.into()), ..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 { - let token = Token::::parse(token).unwrap(); + let token: DefaultToken<()> = DefaultToken::parse(token).unwrap(); if token.verify(b"secret_key").unwrap() { - token.claims.sub + token.payload.sub } else { None } diff --git a/examples/rs256.rs b/examples/rs256.rs index d685c34..6c70991 100644 --- a/examples/rs256.rs +++ b/examples/rs256.rs @@ -3,12 +3,7 @@ extern crate medallion; use std::default::Default; use std::fs::File; use std::io::{Error, Read}; -use medallion::{ - Algorithm, - DefaultHeader, - Registered, - Token, -}; +use medallion::{Algorithm, Header, DefaultPayload, DefaultToken}; fn load_pem(keypath: &str) -> Result { let mut key_file = File::open(keypath)?; @@ -20,28 +15,28 @@ fn load_pem(keypath: &str) -> Result { fn new_token(user_id: &str, password: &str) -> Option { // Dummy auth if password != "password" { - return None + return None; } - let header: DefaultHeader = DefaultHeader { - alg: Algorithm::RS256, - ..Default::default() - }; - let claims = Registered { + // can satisfy Header's generic parameter with an empty type + let header: Header<()> = Header { alg: Algorithm::RS256, ..Default::default() }; + let payload: DefaultPayload = DefaultPayload { iss: Some("example.com".into()), sub: Some(user_id.into()), ..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 { - let token = Token::::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() { - token.claims.sub + token.payload.sub } else { None } diff --git a/src/claims.rs b/src/claims.rs deleted file mode 100644 index 47ad08b..0000000 --- a/src/claims.rs +++ /dev/null @@ -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 { - pub reg: Registered, - pub private: T -} - -/// The registered claims from the spec. -#[derive(Debug, Default, PartialEq, Serialize, Deserialize)] -pub struct Registered { - pub iss: Option, - pub sub: Option, - pub aud: Option, - pub exp: Option, - pub nbf: Option, - pub iat: Option, - pub jti: Option, -} - -impl Claims{ - /// Convenience factory method - pub fn new(reg: Registered, private: T) -> Claims { - Claims { - reg: reg, - private: private - } - } -} - -impl Component for Claims { - /// This implementation simply parses the base64 data twice, each time applying it to the - /// registered and private claims. - fn from_base64(raw: &str) -> Result> { - 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 { - 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(®_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, - last_name: Option - } - - #[test] - fn from_base64() { - let enc = "eyJpc3MiOiJleGFtcGxlLmNvbSIsImV4cCI6MTMwMjMxOTEwMH0"; - let claims: Claims = 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 = 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 = 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::::from_base64(&*enc).unwrap()); - } -} diff --git a/src/crypt.rs b/src/crypt.rs index 73eda83..6bc1118 100644 --- a/src/crypt.rs +++ b/src/crypt.rs @@ -77,9 +77,6 @@ pub mod tests { use std::fs::File; use super::{sign, verify}; - #[derive(Default, Debug, Serialize, Deserialize, PartialEq)] - struct EmptyClaim { } - #[test] pub fn sign_data_hmac() { let header = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"; diff --git a/src/header.rs b/src/header.rs index 44fa47e..072e149 100644 --- a/src/header.rs +++ b/src/header.rs @@ -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 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)] -pub struct DefaultHeader { - pub typ: Option, - pub kid: Option, +pub struct Header { pub alg: Algorithm, -} - - -/// Default value for the header type field. -#[derive(Debug, PartialEq, Serialize, Deserialize)] -pub enum HeaderType { - JWT, + #[serde(skip_serializing)] + pub headers: Option, } /// Supported algorithms, each representing a valid signature and digest combination. @@ -24,56 +26,126 @@ pub enum Algorithm { HS512, RS256, RS384, - RS512 + RS512, } -impl Default for DefaultHeader { - fn default() -> DefaultHeader { - DefaultHeader { - typ: Some(HeaderType::JWT), - kid: None, - alg: Algorithm::HS256, +impl Header { + pub fn from_base64(raw: &str) -> Result> { + let data = decode_config(raw, URL_SAFE_NO_PAD)?; + let own: Header = serde_json::from_slice(&data)?; + + let headers: Option = serde_json::from_slice(&data).ok(); + + + Ok(Header { + alg: own.alg, + headers: headers, + }) + } + + /// Encode to a string. + pub fn to_base64(&self) -> Result { + 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 -/// specific type for the header. -impl Header for DefaultHeader { - fn alg(&self) -> &Algorithm { - &(self.alg) +impl Default for Header { + fn default() -> Header { + Header { + alg: Algorithm::HS256, + headers: None, + } } } #[cfg(test)] mod tests { - use Component; - use header::{ - Algorithm, - DefaultHeader, - HeaderType, - }; + use super::{Algorithm, Header}; + + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct CustomHeaders { + kid: String, + typ: String, + } #[test] fn from_base64() { - let enc = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"; - let header = DefaultHeader::from_base64(enc).unwrap(); + let enc = "eyJhbGciOiJIUzI1NiJ9"; + let header: Header<()> = Header::from_base64(enc).unwrap(); - assert_eq!(header.typ.unwrap(), HeaderType::JWT); assert_eq!(header.alg, Algorithm::HS256); + } + #[test] + fn custom_from_base64() { + let enc = "eyJhbGciOiJIUzI1NiIsImtpZCI6IjFLU0YzZyIsInR5cCI6IkpXVCJ9"; + let header: Header = Header::from_base64(enc).unwrap(); - let enc = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjFLU0YzZyJ9"; - let header = DefaultHeader::from_base64(enc).unwrap(); + let headers = header.headers.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()); - assert_eq!(header.alg, Algorithm::RS256); + #[test] + 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 = Header { + headers: Some(CustomHeaders { + kid: "1KSF3g".into(), + typ: "JWT".into(), + }), + ..Default::default() + }; + + assert_eq!(enc, header.to_base64().unwrap()); } #[test] fn roundtrip() { - let header: DefaultHeader = Default::default(); - let enc = Component::to_base64(&header).unwrap(); - assert_eq!(header, DefaultHeader::from_base64(&*enc).unwrap()); + let header: Header<()> = Default::default(); + let enc = header.to_base64().unwrap(); + assert_eq!(header, Header::from_base64(&*enc).unwrap()); + } + + #[test] + fn roundtrip_custom() { + let header: Header = 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()); } } diff --git a/src/lib.rs b/src/lib.rs index 40498e4..1ef0ae5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,69 +8,45 @@ extern crate serde; extern crate serde_derive; extern crate serde_json; -use base64::{decode_config, encode_config, URL_SAFE_NO_PAD}; use serde::{Serialize, Deserialize}; pub use error::Error; -pub use header::DefaultHeader; +pub use header::Header; pub use header::Algorithm; -pub use claims::Claims; -pub use claims::Registered; +pub use payload::{Payload, DefaultPayload}; pub mod error; -pub mod header; -pub mod claims; +mod header; +mod payload; mod crypt; pub type Result = std::result::Result; +/// 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 = Token; + /// Main struct representing a JSON Web Token, composed of a header and a set of claims. #[derive(Debug, Default)] pub struct Token - where H: Component, C: Component { + where H: Serialize + Deserialize + PartialEq, + C: Serialize + Deserialize + PartialEq +{ raw: Option, - pub header: H, - pub claims: 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; - fn to_base64(&self) -> Result; -} - -/// Provide a default implementation that should work in almost all cases. -impl Component for T - where T: Serialize + Deserialize + Sized { - - /// Parse from a string. - fn from_base64(raw: &str) -> Result { - 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 { - let s = serde_json::to_string(&self)?; - let enc = encode_config((&*s).as_bytes(), URL_SAFE_NO_PAD); - Ok(enc) - } + pub header: Header, + pub payload: Payload, } /// Provide the ability to parse a token, verify it and sign/serialize it. impl Token - where H: Component + Header, C: Component { - pub fn new(header: H, claims: C) -> Token { + where H: Serialize + Deserialize + PartialEq, + C: Serialize + Deserialize + PartialEq +{ + pub fn new(header: Header, payload: Payload) -> Token { Token { raw: None, header: header, - claims: claims, + payload: payload, } } @@ -80,8 +56,8 @@ impl Token Ok(Token { raw: Some(raw.into()), - header: Component::from_base64(pieces[0])?, - claims: Component::from_base64(pieces[1])?, + header: Header::from_base64(pieces[0])?, + payload: Payload::from_base64(pieces[1])?, }) } @@ -96,44 +72,42 @@ impl Token let sig = pieces[0]; 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 /// string. - pub fn signed(&self, key: &[u8]) -> Result { - let header = Component::to_base64(&self.header)?; - let claims = self.claims.to_base64()?; - let data = format!("{}.{}", header, claims); + pub fn sign(&self, key: &[u8]) -> Result { + let header = self.header.to_base64()?; + let payload = self.payload.to_base64()?; + 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)) } } impl PartialEq for Token - where H: Component + PartialEq, C: Component + PartialEq{ + where H: Serialize + Deserialize + PartialEq, + C: Serialize + Deserialize + PartialEq +{ fn eq(&self, other: &Token) -> bool { - self.header == other.header && - self.claims == other.claims + self.header == other.header && self.payload == other.payload } } #[cfg(test)] mod tests { - use Claims; - use Token; + use {DefaultToken, Header}; use crypt::tests::load_pem; - use header::Algorithm::{HS256,RS512}; - use header::DefaultHeader; - - #[derive(Default, Debug, Serialize, Deserialize, PartialEq)] - struct EmptyClaim { } + use super::Algorithm::{HS256, RS512}; #[test] pub fn raw_data() { - let raw = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"; - let token = Token::>::parse(raw).unwrap(); + let raw = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.\ + eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.\ + TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"; + let token = DefaultToken::<()>::parse(raw).unwrap(); { assert_eq!(token.header.alg, HS256); @@ -143,10 +117,10 @@ mod tests { #[test] pub fn roundtrip_hmac() { - let token: Token> = Default::default(); + let token: DefaultToken<()> = Default::default(); let key = "secret".as_bytes(); - let raw = token.signed(key).unwrap(); - let same = Token::parse(&*raw).unwrap(); + let raw = token.sign(key).unwrap(); + let same = DefaultToken::parse(&*raw).unwrap(); assert_eq!(token, same); assert!(same.verify(key).unwrap()); @@ -154,16 +128,11 @@ mod tests { #[test] pub fn roundtrip_rsa() { - let token: Token> = Token { - header: DefaultHeader { - alg: RS512, - ..Default::default() - }, - ..Default::default() - }; + let header: Header<()> = Header { alg: RS512, ..Default::default() }; + let token = DefaultToken { header: header, ..Default::default() }; let private_key = load_pem("./examples/privateKey.pem").unwrap(); - let raw = token.signed(private_key.as_bytes()).unwrap(); - let same = Token::parse(&*raw).unwrap(); + let raw = token.sign(private_key.as_bytes()).unwrap(); + let same = DefaultToken::parse(&*raw).unwrap(); assert_eq!(token, same); let public_key = load_pem("./examples/publicKey.pub").unwrap(); diff --git a/src/payload.rs b/src/payload.rs new file mode 100644 index 0000000..0b2b0e5 --- /dev/null +++ b/src/payload.rs @@ -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 { + #[serde(skip_serializing_if = "Option::is_none")] + pub iss: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub sub: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub aud: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub exp: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub nbf: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub iat: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub jti: Option, + #[serde(skip_serializing)] + pub claims: Option, +} + +/// 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 Payload { + /// 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> { + let data = decode_config(raw, URL_SAFE_NO_PAD)?; + + let claims: Payload = serde_json::from_slice(&data)?; + + let custom: Option = 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 { + 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, + last_name: Option, + } + + #[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 = 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::::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 { + 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() + } + } +}