Initial re-factor
This commit is contained in:
commit
29ad0d721d
14 changed files with 743 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
target
|
||||
Cargo.lock
|
17
Cargo.toml
Normal file
17
Cargo.toml
Normal file
|
@ -0,0 +1,17 @@
|
|||
[package]
|
||||
name = "medallion"
|
||||
version = "0.0.1"
|
||||
authors = ["Thomas Gideon <cmdln@thecommandline.net>"]
|
||||
description = "JWT library for rust using serde, serde_json and openssl"
|
||||
homepage = "http://github.com/commandline/meiallion"
|
||||
repository = "http://github.com/commandline/medallion"
|
||||
readme = "README.md"
|
||||
keywords = ["JWT", "token", "web"]
|
||||
license = "MIT"
|
||||
|
||||
[dependencies]
|
||||
base64 = "0.4"
|
||||
openssl = "0.9"
|
||||
serde = "0.9"
|
||||
serde_json = "0.9"
|
||||
serde_derive = "0.9"
|
22
LICENSE
Normal file
22
LICENSE
Normal file
|
@ -0,0 +1,22 @@
|
|||
(The MIT License)
|
||||
|
||||
Copyright (c) 2015 Michael Yang <mikkyangg@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
10
README.md
Normal file
10
README.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
# JWT
|
||||
|
||||
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`, `Encodable`,
|
||||
and `Decodable` traits. 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.
|
||||
|
||||
This library was originally forked from @mikkyang's rust-jwt.
|
50
examples/custom_claims.rs
Normal file
50
examples/custom_claims.rs
Normal file
|
@ -0,0 +1,50 @@
|
|||
extern crate crypto;
|
||||
extern crate jwt;
|
||||
extern crate rustc_serialize;
|
||||
|
||||
use std::default::Default;
|
||||
use jwt::{
|
||||
DefaultHeader,
|
||||
Token,
|
||||
};
|
||||
|
||||
#[derive(Default, RustcDecodable, RustcEncodable)]
|
||||
struct Custom {
|
||||
sub: String,
|
||||
rhino: bool,
|
||||
}
|
||||
|
||||
fn new_token(user_id: &str, password: &str) -> Option<String> {
|
||||
// Dummy auth
|
||||
if password != "password" {
|
||||
return None
|
||||
}
|
||||
|
||||
let header: DefaultHeader = Default::default();
|
||||
let claims = Custom {
|
||||
sub: user_id.into(),
|
||||
rhino: true,
|
||||
..Default::default()
|
||||
};
|
||||
let token = Token::new(header, claims);
|
||||
|
||||
token.signed(b"secret_key").ok()
|
||||
}
|
||||
|
||||
fn login(token: &str) -> Option<String> {
|
||||
let token = Token::<DefaultHeader, Custom>::parse(token).unwrap();
|
||||
|
||||
if token.verify(b"secret_key") {
|
||||
Some(token.claims.sub)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let token = new_token("Michael Yang", "password").unwrap();
|
||||
|
||||
let logged_in_user = login(&*token).unwrap();
|
||||
|
||||
assert_eq!(logged_in_user, "Michael Yang");
|
||||
}
|
44
examples/hs256.rs
Normal file
44
examples/hs256.rs
Normal file
|
@ -0,0 +1,44 @@
|
|||
extern crate crypto;
|
||||
extern crate jwt;
|
||||
|
||||
use std::default::Default;
|
||||
use jwt::{
|
||||
DefaultHeader,
|
||||
Registered,
|
||||
Token,
|
||||
};
|
||||
|
||||
fn new_token(user_id: &str, password: &str) -> Option<String> {
|
||||
// Dummy auth
|
||||
if password != "password" {
|
||||
return None
|
||||
}
|
||||
|
||||
let header: DefaultHeader = Default::default();
|
||||
let claims = Registered {
|
||||
iss: Some("mikkyang.com".into()),
|
||||
sub: Some(user_id.into()),
|
||||
..Default::default()
|
||||
};
|
||||
let token = Token::new(header, claims);
|
||||
|
||||
token.signed(b"secret_key").ok()
|
||||
}
|
||||
|
||||
fn login(token: &str) -> Option<String> {
|
||||
let token = Token::<DefaultHeader, Registered>::parse(token).unwrap();
|
||||
|
||||
if token.verify(b"secret_key") {
|
||||
token.claims.sub
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let token = new_token("Michael Yang", "password").unwrap();
|
||||
|
||||
let logged_in_user = login(&*token).unwrap();
|
||||
|
||||
assert_eq!(logged_in_user, "Michael Yang");
|
||||
}
|
27
examples/privateKey.pem
Normal file
27
examples/privateKey.pem
Normal file
|
@ -0,0 +1,27 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEAqfGTaNo/PBWs3frGcNpboXCh2/NV2edFwkXMnEjGVw9tyLEu
|
||||
62Tv/LD+hHGQ/EtRfy+iueamtqwRnpeQDnThdYgc7a1nfava7j7ecUCJNnjCJSBT
|
||||
8qWX5U7lsFrxm2NEhGIMgO40wQRxFylb4izxrloBvFKr+XtfgIY5CaUTaKdvZ2mo
|
||||
OXgzZ5kC8Qr7SASpEJiMRM432N9QDTTg3kv23htec+FzSucEdJHrkIxr2LjHAmtm
|
||||
XcT8vRikpN2GThbRUwjLEW3YsWJSTn9GGzF2XZ2rRhdDMviPPjjYPH7Sau2ROnk8
|
||||
sgGMtEzZNS7i/Lm5qsAoPw85mnnnTvJ6ArId2QIDAQABAoIBADPz4S+lwL0alz1J
|
||||
Q88OQgLpjuHR0wYJeL76XaHNcaz9z38SA5j8w40JgtV0bnFiiSiLpICWbZLcqYpF
|
||||
JUn2G1K16LoUT9YQap5448HVi9z2L8vvxRoh23zDkN5H/yKUx0Z9PvtPVxtGw1fk
|
||||
Ue2j9cJqS6uJzn83YyvEXL2BFJziS1mQ7W6raD9fWTBFOlcNwR2+djlECEvxoMcf
|
||||
SQqYa5oBUFWFmaJdCjOxqQxNRgWFxPEVlAz62PUoC1WUoP3uKAg5mvKhH/PjNSGl
|
||||
9K6JhpVgvlBfVUu7dWGAmaUZOLu8l6EUO9MWXDfk10qFKo1bZT9orrARHESPzL2H
|
||||
PPzd3J0CgYEA0TyJxaSVVfWqoSm8yYTbczaRuJ89S104pOzV3UjpLS8DWClFhbl0
|
||||
fYhXGgWIbu3QKksmJ2m9fWm7WbkcYzBjSJOuMFi3actA/glMzZ+DRP+v/154gDR0
|
||||
4/5Etwv+mI+xJqDFGThelC1I7qKtwiyiz85zslW/zDDrn2Yz9OE01S8CgYEAz+zm
|
||||
Qtp88FiSnllzZSM8Hnzey3j4CDk73R2izBrECTI4FE+EPHu6w7wmNiRRTSNjc1F2
|
||||
4qPz/95fUWqwH3ES0UgArFN8vBaoWaoQDmWZGG0ao9Pr6tWDl0DZKrz63jCtCKn7
|
||||
7/bhcxZXYKIPzWgRQhEsZndmcsvARqPVrecmC3cCgYEAofGdIJ/2BYYS/pHzUHXH
|
||||
9DB0MNTu9/m68cts68yWzSXqDL5E1O9pPg/ceoN1yYW+7D0l1rN8uiivnQ4s7ohx
|
||||
D6dd1oWT0ApEz1obW7ruOuU7LwfLdE8leaE/Rf2+nA37Ks6cPpzmdwFlxW2b1wH9
|
||||
MaG04n6D6GKku8a6x/nWjnkCgYEAwSnAMNNxxodCjsFjJs45B8nR4Q2cv2cMajsi
|
||||
BqPHAxQYbSYCH36C31xn01yh+xupRHSmEZ9nCom325dVz5/ob2yI048sDkCuXb5T
|
||||
9EwGkl6ppRE31o5NFbM1DTNLjCeEWMwyNZgRki1rN2bXb2gCwHHb4cWC85q+IeIK
|
||||
nOhku7kCgYAfj3vj/Wc/xTKkZREevgqLm4+B2Sgl5lLaV7OF8jtXEv2mmmmun3Xd
|
||||
r5V2dvvsBQ1D5DQmGw+ObICgdox9BViqG+2PYBWAUfAWyDZaaSEfo72L/1RDdsAR
|
||||
ldUh5fpbdCNl4cz9I2Tysl54pTKMCCH+zj10w+0g5TuNlEZCX/p7qA==
|
||||
-----END RSA PRIVATE KEY-----
|
9
examples/publicKey.pub
Normal file
9
examples/publicKey.pub
Normal file
|
@ -0,0 +1,9 @@
|
|||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqfGTaNo/PBWs3frGcNpb
|
||||
oXCh2/NV2edFwkXMnEjGVw9tyLEu62Tv/LD+hHGQ/EtRfy+iueamtqwRnpeQDnTh
|
||||
dYgc7a1nfava7j7ecUCJNnjCJSBT8qWX5U7lsFrxm2NEhGIMgO40wQRxFylb4izx
|
||||
rloBvFKr+XtfgIY5CaUTaKdvZ2moOXgzZ5kC8Qr7SASpEJiMRM432N9QDTTg3kv2
|
||||
3htec+FzSucEdJHrkIxr2LjHAmtmXcT8vRikpN2GThbRUwjLEW3YsWJSTn9GGzF2
|
||||
XZ2rRhdDMviPPjjYPH7Sau2ROnk8sgGMtEzZNS7i/Lm5qsAoPw85mnnnTvJ6ArId
|
||||
2QIDAQAB
|
||||
-----END PUBLIC KEY-----
|
57
examples/rs256.rs
Normal file
57
examples/rs256.rs
Normal file
|
@ -0,0 +1,57 @@
|
|||
extern crate crypto;
|
||||
extern crate jwt;
|
||||
|
||||
use std::default::Default;
|
||||
use std::fs::File;
|
||||
use std::io::{Error, Read};
|
||||
use jwt::{
|
||||
Algorithm,
|
||||
DefaultHeader,
|
||||
Registered,
|
||||
Token,
|
||||
};
|
||||
|
||||
fn load_key(keypath: &str) -> Result<String, Error> {
|
||||
let mut key_file = try!(File::open(keypath));
|
||||
let mut key = String::new();
|
||||
try!(key_file.read_to_string(&mut key));
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
fn new_token(user_id: &str, password: &str) -> Option<String> {
|
||||
// Dummy auth
|
||||
if password != "password" {
|
||||
return None
|
||||
}
|
||||
|
||||
let header: DefaultHeader = DefaultHeader {
|
||||
alg: Algorithm::RS256,
|
||||
..Default::default()
|
||||
};
|
||||
let claims = Registered {
|
||||
iss: Some("mikkyang.com".into()),
|
||||
sub: Some(user_id.into()),
|
||||
..Default::default()
|
||||
};
|
||||
let token = Token::new(header, claims);
|
||||
|
||||
token.signed(load_key("./privateKey.pem").unwrap().as_bytes()).ok()
|
||||
}
|
||||
|
||||
fn login(token: &str) -> Option<String> {
|
||||
let token = Token::<DefaultHeader, Registered>::parse(token).unwrap();
|
||||
|
||||
if token.verify(load_key("./publicKey.pub").unwrap().as_bytes()) {
|
||||
token.claims.sub
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let token = new_token("Michael Yang", "password").unwrap();
|
||||
|
||||
let logged_in_user = login(&*token).unwrap();
|
||||
|
||||
assert_eq!(logged_in_user, "Michael Yang");
|
||||
}
|
97
src/claims.rs
Normal file
97
src/claims.rs
Normal file
|
@ -0,0 +1,97 @@
|
|||
use base64::{decode, encode_config, URL_SAFE};
|
||||
use Component;
|
||||
use error::Error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json;
|
||||
use serde_json::value::{from_value, to_value, Map, Value};
|
||||
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub struct Claims {
|
||||
pub reg: Registered,
|
||||
pub private: Value
|
||||
}
|
||||
|
||||
#[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>,
|
||||
}
|
||||
|
||||
/// JWT Claims. Registered claims are directly accessible via the `Registered`
|
||||
/// struct embedded, while private fields are a map that contains `Json`
|
||||
/// values.
|
||||
impl Claims {
|
||||
pub fn new(reg: Registered, private: Value) -> Claims {
|
||||
Claims {
|
||||
reg: reg,
|
||||
private: private
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for Claims {
|
||||
fn from_base64(raw: &str) -> Result<Claims, Error> {
|
||||
let data = try!(decode(raw));
|
||||
let reg_claims: Registered = try!(serde_json::from_slice(&data));
|
||||
|
||||
let pri_claims: Value = try!(serde_json::from_slice(&data));
|
||||
|
||||
|
||||
Ok(Claims{
|
||||
reg: reg_claims,
|
||||
private: pri_claims
|
||||
})
|
||||
}
|
||||
|
||||
fn to_base64(&self) -> Result<String, Error> {
|
||||
let mut value = try!(serde_json::to_value(&self.reg));
|
||||
let mut obj_value = &value.as_object_mut().unwrap();
|
||||
// TODO iterate private claims and add to JSON Map
|
||||
//let mut pri_value = self.private.as_object_mut().unwrap();
|
||||
|
||||
//obj_value.extend(pri_value.into_iter());
|
||||
|
||||
let s = try!(serde_json::to_string(&obj_value));
|
||||
let enc = encode_config((&*s).as_bytes(), URL_SAFE);
|
||||
Ok(enc)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::default::Default;
|
||||
use claims::{Claims, Registered};
|
||||
use Component;
|
||||
|
||||
#[test]
|
||||
fn from_base64() {
|
||||
let enc = "ew0KICAiaXNzIjogIm1pa2t5YW5nLmNvbSIsDQogICJleHAiOiAxMzAyMzE5MTAwLA0KICAibmFtZSI6ICJNaWNoYWVsIFlhbmciLA0KICAiYWRtaW4iOiB0cnVlDQp9";
|
||||
let claims = Claims::from_base64(enc).unwrap();
|
||||
|
||||
assert_eq!(claims.reg.iss.unwrap(), "mikkyang.com");
|
||||
assert_eq!(claims.reg.exp.unwrap(), 1302319100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_types() {
|
||||
let enc = "ew0KICAiaXNzIjogIm1pa2t5YW5nLmNvbSIsDQogICJleHAiOiAxMzAyMzE5MTAwLA0KICAibmFtZSI6ICJNaWNoYWVsIFlhbmciLA0KICAiYWRtaW4iOiB0cnVlDQp9";
|
||||
let claims = Registered::from_base64(enc).unwrap();
|
||||
|
||||
assert_eq!(claims.iss.unwrap(), "mikkyang.com");
|
||||
assert_eq!(claims.exp.unwrap(), 1302319100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip() {
|
||||
let mut claims: Claims = Default::default();
|
||||
claims.reg.iss = Some("mikkyang.com".into());
|
||||
claims.reg.exp = Some(1302319100);
|
||||
let enc = claims.to_base64().unwrap();
|
||||
assert_eq!(claims, Claims::from_base64(&*enc).unwrap());
|
||||
}
|
||||
}
|
47
src/crypt.rs
Normal file
47
src/crypt.rs
Normal file
|
@ -0,0 +1,47 @@
|
|||
use base64::{decode, encode_config, URL_SAFE};
|
||||
use openssl::hash::MessageDigest;
|
||||
use openssl::memcmp;
|
||||
use openssl::pkey::PKey;
|
||||
use openssl::rsa::Rsa;
|
||||
use openssl::sign::{Signer, Verifier};
|
||||
|
||||
pub fn sign(data: &str, key: &[u8], digest: MessageDigest) -> String {
|
||||
let secret_key = PKey::hmac(key).unwrap();
|
||||
|
||||
let mut signer = Signer::new(digest, &secret_key).unwrap();
|
||||
signer.update(data.as_bytes()).unwrap();
|
||||
|
||||
let mac = signer.finish().unwrap();
|
||||
encode_config(&mac, URL_SAFE)
|
||||
}
|
||||
|
||||
pub fn sign_rsa(data: &str, key: &[u8], digest: MessageDigest) -> String {
|
||||
let private_key = Rsa::private_key_from_pem(key).unwrap();
|
||||
let pkey = PKey::from_rsa(private_key).unwrap();
|
||||
|
||||
let mut signer = Signer::new(digest, &pkey).unwrap();
|
||||
signer.update(data.as_bytes()).unwrap();
|
||||
let sig = signer.finish().unwrap();
|
||||
encode_config(&sig, URL_SAFE)
|
||||
}
|
||||
|
||||
pub fn verify(target: &str, data: &str, key: &[u8], digest: MessageDigest) -> bool {
|
||||
let target_bytes: Vec<u8> = decode(target).unwrap();
|
||||
let secret_key = PKey::hmac(key).unwrap();
|
||||
|
||||
let mut signer = Signer::new(digest, &secret_key).unwrap();
|
||||
signer.update(data.as_bytes()).unwrap();
|
||||
|
||||
let mac = signer.finish().unwrap();
|
||||
|
||||
memcmp::eq(&mac, &target_bytes)
|
||||
}
|
||||
|
||||
pub fn verify_rsa(signature: &str, data: &str, key: &[u8], digest: MessageDigest) -> bool {
|
||||
let signature_bytes: Vec<u8> = decode(signature).unwrap();
|
||||
let public_key = Rsa::public_key_from_pem(key).unwrap();
|
||||
let pkey = PKey::from_rsa(public_key).unwrap();
|
||||
let mut verifier = Verifier::new(digest, &pkey).unwrap();
|
||||
verifier.update(data.as_bytes()).unwrap();
|
||||
verifier.finish(&signature_bytes).unwrap()
|
||||
}
|
23
src/error.rs
Normal file
23
src/error.rs
Normal file
|
@ -0,0 +1,23 @@
|
|||
use base64::Base64Error;
|
||||
use serde_json;
|
||||
use std::string::FromUtf8Error;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Format,
|
||||
Utf8(FromUtf8Error),
|
||||
Base64(Base64Error),
|
||||
JSON(serde_json::Error),
|
||||
}
|
||||
|
||||
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!(Base64Error, Error::Base64);
|
||||
error_wrap!(serde_json::Error, Error::JSON);
|
74
src/header.rs
Normal file
74
src/header.rs
Normal file
|
@ -0,0 +1,74 @@
|
|||
use std::default::Default;
|
||||
use Header;
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct DefaultHeader {
|
||||
pub typ: Option<HeaderType>,
|
||||
pub kid: Option<String>,
|
||||
pub alg: Algorithm,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub enum HeaderType {
|
||||
JWT,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub enum Algorithm {
|
||||
HS256,
|
||||
HS384,
|
||||
HS512,
|
||||
RS256,
|
||||
RS384,
|
||||
RS512
|
||||
}
|
||||
|
||||
impl Default for DefaultHeader {
|
||||
fn default() -> DefaultHeader {
|
||||
DefaultHeader {
|
||||
typ: Some(HeaderType::JWT),
|
||||
kid: None,
|
||||
alg: Algorithm::HS256,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Header for DefaultHeader {
|
||||
fn alg(&self) -> &Algorithm {
|
||||
&(self.alg)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use Component;
|
||||
use header::{
|
||||
Algorithm,
|
||||
DefaultHeader,
|
||||
HeaderType,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn from_base64() {
|
||||
let enc = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9";
|
||||
let header = DefaultHeader::from_base64(enc).unwrap();
|
||||
|
||||
assert_eq!(header.typ.unwrap(), HeaderType::JWT);
|
||||
assert_eq!(header.alg, Algorithm::HS256);
|
||||
|
||||
|
||||
let enc = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjFLU0YzZyJ9";
|
||||
let header = DefaultHeader::from_base64(enc).unwrap();
|
||||
|
||||
assert_eq!(header.kid.unwrap(), "1KSF3g".to_string());
|
||||
assert_eq!(header.alg, Algorithm::RS256);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip() {
|
||||
let header: DefaultHeader = Default::default();
|
||||
let enc = Component::to_base64(&header).unwrap();
|
||||
assert_eq!(header, DefaultHeader::from_base64(&*enc).unwrap());
|
||||
}
|
||||
}
|
264
src/lib.rs
Normal file
264
src/lib.rs
Normal file
|
@ -0,0 +1,264 @@
|
|||
extern crate base64;
|
||||
extern crate openssl;
|
||||
extern crate serde;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
extern crate serde_json;
|
||||
|
||||
use base64::{decode, encode_config, URL_SAFE};
|
||||
use openssl::hash::MessageDigest;
|
||||
use serde::{Serialize, Deserialize};
|
||||
pub use error::Error;
|
||||
pub use header::DefaultHeader;
|
||||
pub use header::Algorithm;
|
||||
pub use claims::Claims;
|
||||
pub use claims::Registered;
|
||||
|
||||
pub mod error;
|
||||
pub mod header;
|
||||
pub mod claims;
|
||||
mod crypt;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Token<H, C>
|
||||
where H: Component, C: Component {
|
||||
raw: Option<String>,
|
||||
pub header: H,
|
||||
pub claims: C,
|
||||
}
|
||||
|
||||
pub trait Header {
|
||||
fn alg(&self) -> &header::Algorithm;
|
||||
}
|
||||
|
||||
pub trait Component: Sized {
|
||||
fn from_base64(raw: &str) -> Result<Self, Error>;
|
||||
fn to_base64(&self) -> Result<String, Error>;
|
||||
}
|
||||
|
||||
impl<T> Component for T
|
||||
where T: Serialize + Deserialize + Sized {
|
||||
|
||||
/// Parse from a string.
|
||||
fn from_base64(raw: &str) -> Result<T, Error> {
|
||||
let data = try!(decode(raw));
|
||||
let s = try!(String::from_utf8(data));
|
||||
Ok(try!(serde_json::from_str(&*s)))
|
||||
}
|
||||
|
||||
/// Encode to a string.
|
||||
fn to_base64(&self) -> Result<String, Error> {
|
||||
let s = try!(serde_json::to_string(&self));
|
||||
let enc = encode_config((&*s).as_bytes(), URL_SAFE);
|
||||
Ok(enc)
|
||||
}
|
||||
}
|
||||
|
||||
impl<H, C> Token<H, C>
|
||||
where H: Component + Header, C: Component {
|
||||
pub fn new(header: H, claims: C) -> Token<H, C> {
|
||||
Token {
|
||||
raw: None,
|
||||
header: header,
|
||||
claims: claims,
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a token from a string.
|
||||
pub fn parse(raw: &str) -> Result<Token<H, C>, Error> {
|
||||
let pieces: Vec<_> = raw.split('.').collect();
|
||||
|
||||
Ok(Token {
|
||||
raw: Some(raw.into()),
|
||||
header: try!(Component::from_base64(pieces[0])),
|
||||
claims: try!(Component::from_base64(pieces[1])),
|
||||
})
|
||||
}
|
||||
|
||||
/// Verify a from_base64 token with a key and the token's specific algorithm
|
||||
pub fn verify(&self, key: &[u8]) -> bool {
|
||||
match self.header.alg() {
|
||||
&Algorithm::HS256 => self.verify_hmac(key, MessageDigest::sha256()),
|
||||
&Algorithm::HS384 => self.verify_hmac(key, MessageDigest::sha384()),
|
||||
&Algorithm::HS512 => self.verify_hmac(key, MessageDigest::sha512()),
|
||||
&Algorithm::RS256 => self.verify_rsa(key, MessageDigest::sha256()),
|
||||
&Algorithm::RS384 => self.verify_rsa(key, MessageDigest::sha384()),
|
||||
&Algorithm::RS512 => self.verify_rsa(key, MessageDigest::sha512()),
|
||||
}
|
||||
}
|
||||
|
||||
fn verify_hmac(&self, key: &[u8], digest: MessageDigest) -> bool {
|
||||
let raw = match self.raw {
|
||||
Some(ref s) => s,
|
||||
None => return false,
|
||||
};
|
||||
|
||||
let pieces: Vec<_> = raw.rsplitn(2, '.').collect();
|
||||
let sig = pieces[0];
|
||||
let data = pieces[1];
|
||||
|
||||
crypt::verify(sig, data, key, digest)
|
||||
}
|
||||
|
||||
fn verify_rsa(&self, key: &[u8], digest: MessageDigest) -> bool {
|
||||
let raw = match self.raw {
|
||||
Some(ref s) => s,
|
||||
None => return false,
|
||||
};
|
||||
|
||||
let pieces: Vec<_> = raw.rsplitn(2, '.').collect();
|
||||
let sig = pieces[0];
|
||||
let data = pieces[1];
|
||||
|
||||
crypt::verify_rsa(sig, data, key, digest)
|
||||
}
|
||||
|
||||
/// Generate the signed token from a key and the specific algorithm
|
||||
pub fn signed(&self, key: &[u8]) -> Result<String, Error> {
|
||||
match self.header.alg() {
|
||||
&Algorithm::HS256 => self.signed_hmac(key, MessageDigest::sha256()),
|
||||
&Algorithm::HS384 => self.signed_hmac(key, MessageDigest::sha384()),
|
||||
&Algorithm::HS512 => self.signed_hmac(key, MessageDigest::sha512()),
|
||||
&Algorithm::RS256 => self.signed_rsa(key, MessageDigest::sha256()),
|
||||
&Algorithm::RS384 => self.signed_rsa(key, MessageDigest::sha384()),
|
||||
&Algorithm::RS512 => self.signed_rsa(key, MessageDigest::sha512()),
|
||||
}
|
||||
}
|
||||
|
||||
fn signed_hmac(&self, key: &[u8], digest: MessageDigest) -> Result<String, Error> {
|
||||
let header = try!(Component::to_base64(&self.header));
|
||||
let claims = try!(self.claims.to_base64());
|
||||
let data = format!("{}.{}", header, claims);
|
||||
|
||||
let sig = crypt::sign(&*data, key, digest);
|
||||
Ok(format!("{}.{}", data, sig))
|
||||
}
|
||||
|
||||
fn signed_rsa(&self, key: &[u8], digest: MessageDigest) -> Result<String, Error> {
|
||||
let header = try!(Component::to_base64(&self.header));
|
||||
let claims = try!(self.claims.to_base64());
|
||||
let data = format!("{}.{}", header, claims);
|
||||
|
||||
let sig = crypt::sign_rsa(&*data, key, digest);
|
||||
Ok(format!("{}.{}", data, sig))
|
||||
}
|
||||
}
|
||||
|
||||
impl<H, C> PartialEq for Token<H, C>
|
||||
where H: Component + PartialEq, C: Component + PartialEq{
|
||||
fn eq(&self, other: &Token<H, C>) -> bool {
|
||||
self.header == other.header &&
|
||||
self.claims == other.claims
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crypt::{
|
||||
sign,
|
||||
sign_rsa,
|
||||
verify,
|
||||
verify_rsa
|
||||
};
|
||||
use Claims;
|
||||
use Token;
|
||||
use header::Algorithm::{HS256,RS512};
|
||||
use header::DefaultHeader;
|
||||
use std::io::{Error, Read};
|
||||
use std::fs::File;
|
||||
use openssl::hash::MessageDigest;
|
||||
|
||||
#[test]
|
||||
pub fn sign_data() {
|
||||
let header = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9";
|
||||
let claims = "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9";
|
||||
let real_sig = "TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ";
|
||||
let data = format!("{}.{}", header, claims);
|
||||
|
||||
let sig = sign(&*data, "secret".as_bytes(), MessageDigest::sha256());
|
||||
|
||||
assert_eq!(sig, real_sig);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn sign_data_rsa() {
|
||||
let header = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9";
|
||||
let claims = "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9";
|
||||
let real_sig = "nXdpIkFQYZXZ0VlJjHmAc5/aewHCCJpT5jP1fpexUCF/9m3NxlC7uYNXAl6NKno520oh9wVT4VV/vmPeEin7BnnoIJNPcImWcUzkYpLTrDBntiF9HCuqFaniuEVzlf8dVlRJgo8QxhmUZEjyDFjPZXZxPlPV1LD6hrtItxMKZbh1qoNY3OL7Mwo+WuSRQ0mmKj+/y3weAmx/9EaTLY639uD8+o5iZxIIf85U4e55Wdp+C9FJ4RxyHpjgoG8p87IbChfleSdWcZL3NZuxjRCHVWgS1uYG0I+LqBWpWyXnJ1zk6+w4tfxOYpZFMOIyq4tY2mxJQ78Kvcu8bTO7UdI7iA";
|
||||
let data = format!("{}.{}", header, claims);
|
||||
|
||||
let key = load_key("./examples/privateKey.pem").unwrap();
|
||||
|
||||
let sig = sign_rsa(&*data, key.as_bytes(), MessageDigest::sha256());
|
||||
|
||||
assert_eq!(sig.trim(), real_sig);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn verify_data() {
|
||||
let header = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9";
|
||||
let claims = "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9";
|
||||
let target = "TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ";
|
||||
let data = format!("{}.{}", header, claims);
|
||||
|
||||
assert!(verify(target, &*data, "secret".as_bytes(), MessageDigest::sha256()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn verify_data_rsa() {
|
||||
let header = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9";
|
||||
let claims = "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9";
|
||||
let real_sig = "nXdpIkFQYZXZ0VlJjHmAc5/aewHCCJpT5jP1fpexUCF/9m3NxlC7uYNXAl6NKno520oh9wVT4VV/vmPeEin7BnnoIJNPcImWcUzkYpLTrDBntiF9HCuqFaniuEVzlf8dVlRJgo8QxhmUZEjyDFjPZXZxPlPV1LD6hrtItxMKZbh1qoNY3OL7Mwo+WuSRQ0mmKj+/y3weAmx/9EaTLY639uD8+o5iZxIIf85U4e55Wdp+C9FJ4RxyHpjgoG8p87IbChfleSdWcZL3NZuxjRCHVWgS1uYG0I+LqBWpWyXnJ1zk6+w4tfxOYpZFMOIyq4tY2mxJQ78Kvcu8bTO7UdI7iA";
|
||||
let data = format!("{}.{}", header, claims);
|
||||
|
||||
let key = load_key("./examples/publicKey.pub").unwrap();
|
||||
assert!(verify_rsa(&real_sig, &*data, key.as_bytes(), MessageDigest::sha256()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn raw_data() {
|
||||
let raw = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ";
|
||||
let token = Token::<DefaultHeader, Claims>::parse(raw).unwrap();
|
||||
|
||||
{
|
||||
assert_eq!(token.header.alg, HS256);
|
||||
}
|
||||
assert!(token.verify("secret".as_bytes()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn roundtrip() {
|
||||
let token: Token<DefaultHeader, Claims> = Default::default();
|
||||
let key = "secret".as_bytes();
|
||||
let raw = token.signed(key).unwrap();
|
||||
let same = Token::parse(&*raw).unwrap();
|
||||
|
||||
assert_eq!(token, same);
|
||||
assert!(same.verify(key));
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn roundtrip_rsa() {
|
||||
let token: Token<DefaultHeader, Claims> = Token {
|
||||
header: DefaultHeader {
|
||||
alg: RS512,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
let private_key = load_key("./examples/privateKey.pem").unwrap();
|
||||
let raw = token.signed(private_key.as_bytes()).unwrap();
|
||||
let same = Token::parse(&*raw).unwrap();
|
||||
|
||||
assert_eq!(token, same);
|
||||
let public_key = load_key("./examples/publicKey.pub").unwrap();
|
||||
assert!(same.verify(public_key.as_bytes()));
|
||||
}
|
||||
|
||||
fn load_key(keypath: &str) -> Result<String, Error> {
|
||||
let mut key_file = try!(File::open(keypath));
|
||||
let mut key = String::new();
|
||||
try!(key_file.read_to_string(&mut key));
|
||||
Ok(key)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue