From 7585a7f0f9d090cdfa633f6a569a1ba43799be14 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Sat, 31 Oct 2015 15:37:15 +0000 Subject: [PATCH] Initial commit --- .editorconfig | 12 ++++++ .gitignore | 2 + Cargo.toml | 16 ++++++++ README.md | 6 +++ src/claims.rs | 101 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/errors.rs | 23 ++++++++++++ src/header.rs | 58 +++++++++++++++++++++++++++++ src/lib.rs | 46 +++++++++++++++++++++++ 8 files changed, 264 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 src/claims.rs create mode 100644 src/errors.rs create mode 100644 src/header.rs create mode 100644 src/lib.rs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d1f040a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 4 + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a9d37c5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..91b098f --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "jwt" +version = "0.1.0" +authors = ["Vincent Prouillet "] +license = "MIT" +readme = "README.md" +description = "Create and parse JWT." +keywords = ["jwt", "web", "api", "token"] + +[dependencies] +rustc-serialize = "0.3" +clippy = {version = "0.0.22", optional = true} + +[features] +default = [] +dev = ["clippy"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..3e8b9f3 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +# JWT + +``` +JWT::encode(payload, secret, algo) -> Result +JWT::decode(token, secret, algo) -> Result +``` diff --git a/src/claims.rs b/src/claims.rs new file mode 100644 index 0000000..2310172 --- /dev/null +++ b/src/claims.rs @@ -0,0 +1,101 @@ +use std::collections::BTreeMap; +use rustc_serialize::json::{self, Json, ToJson}; +use rustc_serialize::base64::{self, ToBase64, FromBase64}; + +use errors::Error; + +macro_rules! add_registered_claims { + ($map: expr, $key: expr, $reg: expr) => { + if let Some(val) = $reg { + $map.insert($key, val.to_json()); + } + } +} + +#[derive(Debug, Default, RustcEncodable, RustcDecodable)] +struct RegisteredClaims { + iss: Option, + sub: Option, + aud: Option, + exp: Option, + nbf: Option, + iat: Option, + jti: Option, +} + +#[derive(Debug)] +pub struct Claims { + registered: RegisteredClaims, + private: BTreeMap, +} + +impl Claims { + pub fn new() -> Claims { + Claims { + registered: Default::default(), + private: BTreeMap::new(), + } + } + + fn to_base64(self) -> Result { + let mut map: BTreeMap = BTreeMap::new(); + // just encoding the struct would give null values in the resulting json + // like {"iss": null}, which we don't want + add_registered_claims!(map, "iss".to_owned(), self.registered.iss); + add_registered_claims!(map, "sub".to_owned(), self.registered.sub); + add_registered_claims!(map, "aud".to_owned(), self.registered.aud); + add_registered_claims!(map, "exp".to_owned(), self.registered.exp); + add_registered_claims!(map, "nbf".to_owned(), self.registered.nbf); + add_registered_claims!(map, "iat".to_owned(), self.registered.iat); + add_registered_claims!(map, "jti".to_owned(), self.registered.jti); + + map.extend(self.private); + let encoded = try!(json::encode(&map)); + Ok(encoded.as_bytes().to_base64(base64::STANDARD)) + } + + fn from_base64(encoded String) -> Result { + + } + + pub fn add(&mut self, key: String, value: T) { + self.private.insert(key, value.to_json()); + } +} + +#[cfg(test)] +mod tests { + use claims::Claims; + use rustc_serialize::json::{ToJson}; + + #[test] + fn to_base64_no_null_values() { + let mut claims = Claims::new(); + claims.registered.iss = Some("JWT".to_owned()); + let result = claims.to_base64().unwrap(); + let expected = "eyJpc3MiOiJKV1QifQ=="; + + assert_eq!(result, expected); + } + + #[test] + fn to_base64_custom_claims() { + let mut claims = Claims::new(); + claims.add::("group".to_owned(), "zombie".to_owned()); + let result = claims.to_base64().unwrap(); + let expected = "eyJncm91cCI6InpvbWJpZSJ9"; + + assert_eq!(result, expected); + } + + #[test] + fn to_base64_registered_and_customs() { + let mut claims = Claims::new(); + claims.registered.iss = Some("JWT".to_owned()); + claims.add::("group".to_owned(), "zombie".to_owned()); + let result = claims.to_base64().unwrap(); + let expected = "eyJncm91cCI6InpvbWJpZSIsImlzcyI6IkpXVCJ9"; + + assert_eq!(result, expected); + } +} diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..940a0db --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,23 @@ +use std::string; +use rustc_serialize::{json, base64}; + +#[derive(Debug)] +pub enum Error { + EncodeJSON(json::EncoderError), + DecodeBase64(base64::FromBase64Error), + DecodeJSON(json::DecoderError), + Utf8(string::FromUtf8Error), +} + +macro_rules! impl_from_error { + ($f: ty, $e: expr) => { + impl From<$f> for Error { + fn from(f: $f) -> Error { $e(f) } + } + } +} + +impl_from_error!(json::EncoderError, Error::EncodeJSON); +impl_from_error!(base64::FromBase64Error, Error::DecodeBase64); +impl_from_error!(json::DecoderError, Error::DecodeJSON); +impl_from_error!(string::FromUtf8Error, Error::Utf8); diff --git a/src/header.rs b/src/header.rs new file mode 100644 index 0000000..5e6b346 --- /dev/null +++ b/src/header.rs @@ -0,0 +1,58 @@ +use rustc_serialize::json; +use rustc_serialize::base64::{self, ToBase64, FromBase64}; + +use errors::Error; + + +#[derive(Debug, PartialEq, RustcEncodable, RustcDecodable)] +pub struct Header { + typ: Option, + alg: String, +} + +impl Header { + pub fn new(algorithm: String) -> Header { + Header { + typ: Some("JWT".to_owned()), + alg: algorithm, + } + } + pub fn to_base64(&self) -> Result { + let encoded = try!(json::encode(&self)); + Ok(encoded.as_bytes().to_base64(base64::STANDARD)) + } + + pub fn from_base64(encoded: String) -> Result { + let decoded = try!(encoded.as_bytes().from_base64()); + let s = try!(String::from_utf8(decoded)); + Ok(try!(json::decode(&s))) + } +} + +#[cfg(test)] +mod tests { + use header::Header; + + #[test] + fn to_base64() { + let expected = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9".to_owned(); + let result = Header::new("HS256".to_owned()).to_base64(); + + assert_eq!(expected, result.unwrap()); + } + + #[test] + fn from_base64() { + let encoded = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9".to_owned(); + let header = Header::from_base64(encoded).unwrap(); + + assert_eq!(header.typ.unwrap(), "JWT"); + assert_eq!(header.alg, "HS256"); + } + + #[test] + fn round_trip() { + let header = Header::new("HS256".to_owned()); + assert_eq!(Header::from_base64(header.to_base64().unwrap()).unwrap(), header); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..c5cd5d1 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,46 @@ +//! Create and parses JWT (JSON Web Tokens) +//! + +// #![deny( +// missing_docs, +// missing_debug_implementations, missing_copy_implementations, +// trivial_casts, trivial_numeric_casts, +// unsafe_code, +// unstable_features, +// unused_import_braces, unused_qualifications +// )] + +#![cfg_attr(feature = "dev", allow(unstable_features))] +#![cfg_attr(feature = "dev", feature(plugin))] +#![cfg_attr(feature = "dev", plugin(clippy))] + +extern crate rustc_serialize; + +pub mod errors; +pub mod header; +pub mod claims; + +#[derive(Debug)] +pub enum Algorithm { + HS256 +} + +impl ToString for Algorithm { + fn to_string(&self) -> String { + match *self { + Algorithm::HS256 => "HS256".to_owned(), + } + } +} + +// pub fn encode(secret: String, algorithm: Algorithm) -> String { + +// } + +// pub fn decode(token: String, secret: String, algorithm: Algorithm) -> Result { + +// } + +#[cfg(test)] +mod tests { +}