use crate::errors;
use crate::errors::ErrorKind;
use crate::Result;
use chrono::NaiveDateTime;
use nkeys::KeyPair;
use serde::de::DeserializeOwned;
use serde::Serialize;
use serde_json::{from_str, to_string};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
const HEADER_TYPE: &str = "jwt";
const HEADER_ALGORITHM: &str = "Ed25519";
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct Token<T> {
pub jwt: String,
pub claims: Claims<T>,
}
#[derive(Debug, Serialize, Deserialize)]
struct ClaimsHeader {
#[serde(rename = "typ")]
header_type: String,
#[serde(rename = "alg")]
algorithm: String,
}
fn default_as_false() -> bool {
false
}
pub trait WascapEntity: Clone {
fn name(&self) -> String;
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Default)]
pub struct Actor {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(rename = "hash")]
pub module_hash: String,
#[serde(rename = "tags", skip_serializing_if = "Option::is_none")]
pub tags: Option<Vec<String>>,
#[serde(rename = "caps", skip_serializing_if = "Option::is_none")]
pub caps: Option<Vec<String>>,
#[serde(rename = "rev", skip_serializing_if = "Option::is_none")]
pub rev: Option<i32>,
#[serde(rename = "ver", skip_serializing_if = "Option::is_none")]
pub ver: Option<String>,
#[serde(rename = "prov", default = "default_as_false")]
pub provider: bool,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Default)]
pub struct Account {
pub name: String,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Default)]
pub struct Operator {
pub name: String,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Default)]
pub struct Claims<T> {
#[serde(rename = "exp", skip_serializing_if = "Option::is_none")]
pub expires: Option<u64>,
#[serde(rename = "jti")]
pub id: String,
#[serde(rename = "iat")]
pub issued_at: u64,
#[serde(rename = "iss")]
pub issuer: String,
#[serde(rename = "sub")]
pub subject: String,
#[serde(rename = "nbf", skip_serializing_if = "Option::is_none")]
pub not_before: Option<u64>,
#[serde(rename = "wascap", skip_serializing_if = "Option::is_none")]
pub metadata: Option<T>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
pub struct TokenValidation {
pub expired: bool,
pub cannot_use_yet: bool,
pub expires_human: String,
pub not_before_human: String,
pub signature_valid: bool,
}
impl<T> Claims<T>
where
T: Serialize + DeserializeOwned + WascapEntity,
{
pub fn encode(&self, kp: &KeyPair) -> Result<String> {
let header = ClaimsHeader {
header_type: HEADER_TYPE.to_string(),
algorithm: HEADER_ALGORITHM.to_string(),
};
let jheader = to_jwt_segment(&header)?;
let jclaims = to_jwt_segment(self)?;
let head_and_claims = format!("{}.{}", jheader, jclaims);
let sig = kp.sign(head_and_claims.as_bytes())?;
let sig64 = base64::encode_config(&sig, base64::URL_SAFE_NO_PAD);
Ok(format!("{}.{}", head_and_claims, sig64))
}
pub fn decode(input: &str) -> Result<Claims<T>> {
let segments: Vec<&str> = input.split('.').collect();
let claims: Claims<T> = from_jwt_segment(segments[1])?;
Ok(claims)
}
pub fn name(&self) -> String {
self.metadata
.as_ref()
.map_or("Anonymous".to_string(), |md| md.name())
}
}
impl WascapEntity for Actor {
fn name(&self) -> String {
self.name
.as_ref()
.unwrap_or(&"Anonymous".to_string())
.to_string()
}
}
impl WascapEntity for Account {
fn name(&self) -> String {
self.name.clone()
}
}
impl WascapEntity for Operator {
fn name(&self) -> String {
self.name.clone()
}
}
impl Claims<Account> {
pub fn new(name: String, issuer: String, subject: String) -> Claims<Account> {
Self::with_dates(name, issuer, subject, None, None)
}
pub fn with_dates(
name: String,
issuer: String,
subject: String,
not_before: Option<u64>,
expires: Option<u64>,
) -> Claims<Account> {
Claims {
metadata: Some(Account { name }),
expires,
id: nuid::next(),
issued_at: since_the_epoch().as_secs(),
issuer,
subject,
not_before,
}
}
}
impl Claims<Operator> {
pub fn new(name: String, issuer: String, subject: String) -> Claims<Operator> {
Self::with_dates(name, issuer, subject, None, None)
}
pub fn with_dates(
name: String,
issuer: String,
subject: String,
not_before: Option<u64>,
expires: Option<u64>,
) -> Claims<Operator> {
Claims {
metadata: Some(Operator { name }),
expires,
id: nuid::next(),
issued_at: since_the_epoch().as_secs(),
issuer,
subject,
not_before,
}
}
}
impl Claims<Actor> {
pub fn new(
name: String,
issuer: String,
subject: String,
caps: Option<Vec<String>>,
tags: Option<Vec<String>>,
provider: bool,
rev: Option<i32>,
ver: Option<String>,
) -> Claims<Actor> {
Self::with_dates(
name, issuer, subject, caps, tags, None, None, provider, rev, ver,
)
}
pub fn with_dates(
name: String,
issuer: String,
subject: String,
caps: Option<Vec<String>>,
tags: Option<Vec<String>>,
not_before: Option<u64>,
expires: Option<u64>,
provider: bool,
rev: Option<i32>,
ver: Option<String>,
) -> Claims<Actor> {
Claims {
metadata: Some(Actor::new(name, caps, tags, provider, rev, ver)),
expires,
id: nuid::next(),
issued_at: since_the_epoch().as_secs(),
issuer,
subject,
not_before,
}
}
}
#[derive(Default)]
pub struct ClaimsBuilder<T> {
claims: Claims<T>,
}
impl<T> ClaimsBuilder<T>
where
T: Default + WascapEntity,
{
pub fn new() -> Self {
ClaimsBuilder::default()
}
pub fn issuer(&mut self, issuer: &str) -> &mut Self {
self.claims.issuer = issuer.to_string();
self
}
pub fn subject(&mut self, module: &str) -> &mut Self {
self.claims.subject = module.to_string();
self
}
pub fn expires_in(&mut self, d: Duration) -> &mut Self {
self.claims.expires = Some(d.as_secs() + since_the_epoch().as_secs());
self
}
pub fn valid_in(&mut self, d: Duration) -> &mut Self {
self.claims.not_before = Some(d.as_secs() + since_the_epoch().as_secs());
self
}
pub fn with_metadata(&mut self, metadata: T) -> &mut Self {
self.claims.metadata = Some(metadata);
self
}
pub fn build(&self) -> Claims<T> {
Claims {
id: nuid::next(),
issued_at: since_the_epoch().as_secs(),
..self.claims.clone()
}
}
}
pub fn validate_token<T>(input: &str) -> Result<TokenValidation>
where
T: Serialize + DeserializeOwned + WascapEntity,
{
let segments: Vec<&str> = input.split('.').collect();
let header_and_claims = format!("{}.{}", segments[0], segments[1]);
let sig = base64::decode_config(segments[2], base64::URL_SAFE_NO_PAD)?;
let header: ClaimsHeader = from_jwt_segment(segments[0])?;
validate_header(&header)?;
let claims = Claims::<T>::decode(input)?;
let kp = KeyPair::from_public_key(&claims.issuer)?;
let sigverify = kp.verify(header_and_claims.as_bytes(), &sig);
let validation = TokenValidation {
signature_valid: sigverify.is_ok(),
expired: validate_expiration(claims.expires).is_err(),
expires_human: stamp_to_human(claims.expires).unwrap_or_else(|| "never".to_string()),
not_before_human: stamp_to_human(claims.not_before)
.unwrap_or_else(|| "immediately".to_string()),
cannot_use_yet: validate_notbefore(claims.not_before).is_err(),
};
Ok(validation)
}
fn validate_notbefore(nb: Option<u64>) -> Result<()> {
if let Some(nbf) = nb {
let nbf_secs = Duration::from_secs(nbf);
if since_the_epoch() < nbf_secs {
Err(errors::new(ErrorKind::TokenTooEarly))
} else {
Ok(())
}
} else {
Ok(())
}
}
fn validate_expiration(exp: Option<u64>) -> Result<()> {
if let Some(exp) = exp {
let exp_secs = Duration::from_secs(exp);
if exp_secs < since_the_epoch() {
Err(errors::new(ErrorKind::ExpiredToken))
} else {
Ok(())
}
} else {
Ok(())
}
}
fn since_the_epoch() -> Duration {
let start = SystemTime::now();
start
.duration_since(UNIX_EPOCH)
.expect("A timey wimey problem has occurred!")
}
fn validate_header(h: &ClaimsHeader) -> Result<()> {
if h.algorithm != HEADER_ALGORITHM {
Err(errors::new(ErrorKind::InvalidAlgorithm))
} else if h.header_type != HEADER_TYPE {
Err(errors::new(ErrorKind::Token("Invalid header".to_string())))
} else {
Ok(())
}
}
fn to_jwt_segment<T: Serialize>(input: &T) -> Result<String> {
let encoded = to_string(input)?;
Ok(base64::encode_config(
encoded.as_bytes(),
base64::URL_SAFE_NO_PAD,
))
}
fn from_jwt_segment<B: AsRef<str>, T: DeserializeOwned>(encoded: B) -> Result<T> {
let decoded = base64::decode_config(encoded.as_ref(), base64::URL_SAFE_NO_PAD)?;
let s = String::from_utf8(decoded)?;
Ok(from_str(&s)?)
}
fn stamp_to_human(stamp: Option<u64>) -> Option<String> {
stamp.map(|s| {
let now = NaiveDateTime::from_timestamp(since_the_epoch().as_secs() as i64, 0);
let then = NaiveDateTime::from_timestamp(s as i64, 0);
let diff = then - now;
let ht = chrono_humanize::HumanTime::from(diff);
format!("{}", ht)
})
}
impl Actor {
pub fn new(
name: String,
caps: Option<Vec<String>>,
tags: Option<Vec<String>>,
provider: bool,
rev: Option<i32>,
ver: Option<String>,
) -> Actor {
Actor {
name: Some(name),
module_hash: "".to_string(),
tags,
caps,
provider,
rev,
ver,
}
}
}
impl Account {
pub fn new(
name: String,
) -> Account {
Account {
name
}
}
}
impl Operator {
pub fn new(
name: String,
) -> Operator {
Operator {
name
}
}
}
#[cfg(test)]
mod test {
use super::{Actor, Claims, KeyPair, Account, Operator};
use crate::caps::{KEY_VALUE, MESSAGING};
use crate::jwt::since_the_epoch;
use crate::jwt::validate_token;
#[test]
fn full_validation_nbf() {
let kp = KeyPair::new_account();
let claims = Claims {
metadata: Some(Actor::new(
"test".to_string(),
Some(vec![MESSAGING.to_string(), KEY_VALUE.to_string()]),
Some(vec![]),
false,
Some(0),
Some("".to_string()),
)),
expires: None,
id: nuid::next(),
issued_at: 0,
issuer: kp.public_key(),
subject: "test.wasm".to_string(),
not_before: Some(since_the_epoch().as_secs() + 1000),
};
let encoded = claims.encode(&kp).unwrap();
let vres = validate_token::<Actor>(&encoded);
assert!(vres.is_ok());
if let Ok(v) = vres {
assert_eq!(v.expired, false);
assert_eq!(v.cannot_use_yet, true);
assert_eq!(v.not_before_human, "in 16 minutes");
}
}
#[test]
fn full_validation_expires() {
let kp = KeyPair::new_account();
let claims = Claims {
metadata: Some(Actor::new(
"test".to_string(),
Some(vec![MESSAGING.to_string(), KEY_VALUE.to_string()]),
Some(vec![]),
false,
Some(1),
Some("".to_string()),
)),
expires: Some(since_the_epoch().as_secs() - 30000),
id: nuid::next(),
issued_at: 0,
issuer: kp.public_key(),
subject: "test.wasm".to_string(),
not_before: None,
};
let encoded = claims.encode(&kp).unwrap();
let vres = validate_token::<Actor>(&encoded);
assert!(vres.is_ok());
if let Ok(v) = vres {
assert!(v.expired);
assert_eq!(v.cannot_use_yet, false);
assert_eq!(v.expires_human, "8 hours ago");
}
}
#[test]
fn validate_account() {
let issuer = KeyPair::new_operator();
let claims = Claims {
metadata: Some(Account::new(
"test account".to_string()
)),
expires: Some(since_the_epoch().as_secs() - 30000),
id: nuid::next(),
issued_at: 0,
issuer: issuer.public_key(),
subject: "foo".to_string(),
not_before: None,
};
let encoded = claims.encode(&issuer).unwrap();
let vres = validate_token::<Account>(&encoded);
assert!(vres.is_ok());
if let Ok(v) = vres {
assert!(v.expired);
assert_eq!(v.cannot_use_yet, false);
assert_eq!(v.expires_human, "8 hours ago");
}
}
#[test]
fn full_validation() {
let kp = KeyPair::new_account();
let claims = Claims {
metadata: Some(Actor::new(
"test".to_string(),
Some(vec![MESSAGING.to_string(), KEY_VALUE.to_string()]),
Some(vec![]),
false,
Some(1),
Some("".to_string()),
)),
expires: None,
id: nuid::next(),
issued_at: 0,
issuer: kp.public_key(),
subject: "test.wasm".to_string(),
not_before: None,
};
let encoded = claims.encode(&kp).unwrap();
let vres = validate_token::<Actor>(&encoded);
assert!(vres.is_ok());
}
#[test]
fn encode_decode_mismatch() {
let issuer = KeyPair::new_operator();
let claims = Claims {
metadata: Some(Account::new("test account".to_string())),
expires: None,
id: nuid::next(),
issued_at: 0,
issuer: "foo".to_string(),
subject: "test".to_string(),
not_before: None,
};
let encoded = claims.encode(&issuer).unwrap();
let decoded = Claims::<Actor>::decode(&encoded);
assert!(decoded.is_err());
}
#[test]
fn decode_actor_as_operator() {
let kp = KeyPair::new_account();
let claims = Claims {
metadata: Some(Actor::new(
"test".to_string(),
Some(vec![MESSAGING.to_string(), KEY_VALUE.to_string()]),
Some(vec![]),
false,
Some(1),
Some("".to_string()),
)),
expires: None,
id: nuid::next(),
issued_at: 0,
issuer: kp.public_key(),
subject: "test.wasm".to_string(),
not_before: None,
};
let encoded = claims.encode(&kp).unwrap();
let decoded = Claims::<Operator>::decode(&encoded);
assert!(decoded.is_ok());
assert_eq!(decoded.unwrap().metadata.unwrap().name, "test");
}
#[test]
fn encode_decode_roundtrip() {
let kp = KeyPair::new_account();
let claims = Claims {
metadata: Some(Actor::new(
"test".to_string(),
Some(vec![MESSAGING.to_string(), KEY_VALUE.to_string()]),
Some(vec![]),
false,
Some(1),
Some("".to_string()),
)),
expires: None,
id: nuid::next(),
issued_at: 0,
issuer: kp.public_key(),
subject: "test.wasm".to_string(),
not_before: None,
};
let encoded = claims.encode(&kp).unwrap();
let decoded = Claims::decode(&encoded).unwrap();
assert!(validate_token::<Actor>(&encoded).is_ok());
assert_eq!(claims, decoded);
}
}