parent
9df2ac741e
commit
3c9fd6b13b
11 changed files with 522 additions and 292 deletions
|
@ -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
109
README.md
|
@ -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.
|
||||||
|
|
|
@ -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 {
|
||||||
|
user_id: user_id.into(),
|
||||||
rhino: true,
|
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
|
||||||
}
|
}
|
||||||
|
|
49
examples/custom_headers.rs
Normal file
49
examples/custom_headers.rs
Normal 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");
|
||||||
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
129
src/claims.rs
129
src/claims.rs
|
@ -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(®_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());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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";
|
||||||
|
|
156
src/header.rs
156
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 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,
|
|
||||||
|
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()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Serialize + Deserialize> Default for Header<T> {
|
||||||
|
fn default() -> Header<T> {
|
||||||
|
Header {
|
||||||
alg: Algorithm::HS256,
|
alg: Algorithm::HS256,
|
||||||
|
headers: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
119
src/lib.rs
119
src/lib.rs
|
@ -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
170
src/payload.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue