1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
extern crate serde;
extern crate serde_json;
extern crate yaml_rust;
extern crate chrono;
extern crate jsonwebtoken as jwt;
extern crate uuid;

use core::chrono::prelude::*;
use core::uuid::Uuid;
use std::collections::HashMap;
use std::error;
use std::fmt;
use std::result;

type Result<A> = result::Result<A, Error>;

/// Orizentic Errors
#[derive(Debug)]
pub enum Error {
    /// An underlying JWT decoding error. May be replaced with Orizentic semantic errors to better
    /// encapsulate the JWT library.
    JWTError(jwt::errors::Error),
    /// Token decoded and verified but was not present in the database.
    UnknownToken,
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Error::JWTError(err) => write!(f, "JWT failed to decode: {}", err),
            Error::UnknownToken => write!(f, "Token not recognized"),
        }
    }
}

impl error::Error for Error {
    fn description(&self) -> &str {
        match self {
            Error::JWTError(ref err) => err.description(),
            Error::UnknownToken => "Token not recognized",
        }
    }

    fn cause(&self) -> Option<&error::Error> {
        match self {
            Error::JWTError(ref err) => Some(err),
            Error::UnknownToken => None,
        }
    }
}

/// ResourceName is application-defined and names a resource to which access should be controlled
#[derive(Debug, PartialEq, Clone)]
pub struct ResourceName(pub String);

/// Permissions are application-defined descriptions of what can be done with the named resource
#[derive(Debug, PartialEq, Clone)]
pub struct Permissions(pub Vec<String>);

/// Issuers are typically informative, but should generally describe who or what created the token
#[derive(Debug, PartialEq, Clone)]
pub struct Issuer(pub String);

/// Time to live is the number of seconds until a token expires. This is used for creating tokens
/// but tokens store their actual expiration time.
#[derive(Debug, PartialEq, Clone)]
pub struct TTL(pub chrono::Duration);

/// Username, or Audience in JWT terms, should describe who or what is supposed to be using this
/// token
#[derive(Debug, PartialEq, Clone)]
pub struct Username(pub String);

#[derive(Debug, PartialEq, Clone)]
pub struct Secret(pub Vec<u8>);

/// A ClaimSet represents one set of permissions and claims. It is a standardized way of specifying
/// the owner, issuer, expiration time, relevant resources, and specific permissions on that
/// resource. By itself, this is only an informative data structure and so should never be trusted
/// when passed over the wire. See `VerifiedToken` and `UnverifiedToken`.
#[derive(Debug, PartialEq, Clone)]
pub struct ClaimSet {
    pub id: String,
    pub audience: Username,
    pub expiration: Option<DateTime<Utc>>,
    pub issuer: Issuer,
    pub issued_at: DateTime<Utc>,
    pub resource: ResourceName,
    pub permissions: Permissions,
}

impl ClaimSet {
    /// Create a new `ClaimSet`. This will return a claimset with the expiration time calculated
    /// from the TTL if the TTL is provided. No expiration will be set if no TTL is provided.
    pub fn new(
        issuer: Issuer,
        ttl: Option<TTL>,
        resource_name: ResourceName,
        user_name: Username,
        perms: Permissions,
    ) -> ClaimSet {
        let issued_at: DateTime<Utc> = Utc::now().with_nanosecond(0).unwrap();
        let expiration = match ttl {
            Some(TTL(ttl_)) => issued_at.checked_add_signed(ttl_),
            None => None,
        };
        ClaimSet {
            id: String::from(Uuid::new_v4().hyphenated().to_string()),
            audience: user_name,
            expiration,
            issuer,
            issued_at,
            resource: resource_name,
            permissions: perms,
        }
    }

    pub fn to_json(&self) -> result::Result<String, serde_json::Error> {
        serde_json::to_string(&(ClaimSetJS::from_claimset(self)))
    }

    pub fn from_json(text: &String) -> result::Result<ClaimSet, serde_json::Error> {
        serde_json::from_str(&text).map(|x| ClaimSetJS::to_claimset(&x))
    }
}

/// ClaimSetJS is an intermediary data structure between JWT serialization and a more usable
/// ClaimSet.
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub struct ClaimSetJS {
    jti: String,
    aud: String,
    exp: Option<i64>,
    iss: String,
    iat: i64,
    sub: String,
    perms: Vec<String>,
}

impl ClaimSetJS {
    pub fn from_claimset(claims: &ClaimSet) -> ClaimSetJS {
        ClaimSetJS {
            jti: claims.id.clone(),
            aud: claims.audience.0.clone(),
            exp: claims.expiration.map(|t| t.timestamp()),
            iss: claims.issuer.0.clone(),
            iat: claims.issued_at.timestamp(),
            sub: claims.resource.0.clone(),
            perms: claims.permissions.0.clone(),
        }
    }

    pub fn to_claimset(&self) -> ClaimSet {
        ClaimSet {
            id: self.jti.clone(),
            audience: Username(self.aud.clone()),
            expiration: self.exp.map(|t| Utc.timestamp(t, 0)),
            issuer: Issuer(self.iss.clone()),
            issued_at: Utc.timestamp(self.iat, 0),
            resource: ResourceName(self.sub.clone()),
            permissions: Permissions(self.perms.clone()),
        }
    }
}

/// The Orizentic Context encapsulates a set of claims and an associated secret. This provides the
/// overall convenience of easily creating and validating tokens. Generated claimsets are stored
/// here on the theory that, even with validation, only those claims actually stored in the
/// database should be considered valid.
pub struct OrizenticCtx(Secret, HashMap<String, ClaimSet>);

/// An UnverifiedToken is a combination of the JWT serialization and the decoded `ClaimSet`. As this
/// is unverified, this should only be used for information purposes, such as determining what a
/// user can do with a token even when the decoding key is absent.
#[derive(Debug)]
pub struct UnverifiedToken {
    pub text: String,
    pub claims: ClaimSet,
}

impl UnverifiedToken {
    /// Decode a JWT text string without verification
    pub fn decode_text(text: &String) -> Result<UnverifiedToken> {
        let res = jwt::dangerous_unsafe_decode::<ClaimSetJS>(text);
        match res {
            Ok(res_) => Ok(UnverifiedToken {
                text: text.clone(),
                claims: res_.claims.to_claimset(),
            }),
            Err(err) => Err(Error::JWTError(err)),
        }
    }
}

/// An VerifiedToken is a combination of the JWT serialization and the decoded `ClaimSet`. This will
/// only be created by the `validate_function`, and thus will represent a token which has been
/// validated via signature, expiration time, and presence in the database.
#[derive(Debug)]
pub struct VerifiedToken {
    pub text: String,
    pub claims: ClaimSet,
}

impl VerifiedToken {
    /// Given a `VerifiedToken`, pass the resource name and permissions to a user-defined function. The
    /// function should return true if the caller should be granted access to the resource and false,
    /// otherwise. That result will be passed back to the caller.
    pub fn check_authorizations<F: FnOnce(&ResourceName, &Permissions) -> bool>(
        &self,
        f: F,
    ) -> bool {
        f(&self.claims.resource, &self.claims.permissions)
    }
}


impl OrizenticCtx {
    /// Create a new Orizentic Context with an initial set of claims.
    pub fn new(secret: Secret, claims_lst: Vec<ClaimSet>) -> OrizenticCtx {
        let mut hm = HashMap::new();
        for claimset in claims_lst {
            hm.insert(claimset.id.clone(), claimset);
        }
        OrizenticCtx(secret, hm)
    }

    /// Validate a token by checking its signature, that it is not expired, and that it is still
    /// present in the database. Return an error if any check fails, but return a `VerifiedToken`
    /// if it all succeeds.
    pub fn validate_token(&self, token: &UnverifiedToken) -> Result<VerifiedToken> {
        let validator = match token.claims.expiration {
            Some(_) => jwt::Validation::default(),
            None => jwt::Validation {
                validate_exp: false,
                ..jwt::Validation::default()
            },
        };
        let res = jwt::decode::<ClaimSetJS>(&token.text, &(self.0).0, &validator);
        match res {
            Ok(res_) => {
                let claims = res_.claims;
                let in_db = self.1.get(&claims.jti);
                if in_db.is_some() {
                    Ok(VerifiedToken {
                        text: token.text.clone(),
                        claims: claims.to_claimset(),
                    })
                } else {
                    Err(Error::UnknownToken)
                }
            }
            Err(err) => Err(Error::JWTError(err)),
        }
    }

    /// Given a text string, as from a web application's `Authorization` header, decode the string
    /// and then validate the token.
    pub fn decode_and_validate_text(&self, text: &String) -> Result<VerifiedToken> {
        // it is necessary to first decode the token because we need the validator to know whether
        // to attempt to validate the expiration. Without that check, the validator will fail any
        // expiration set to None.
        match UnverifiedToken::decode_text(text) {
            Ok(unverified) => self.validate_token(&unverified),
            Err(err) => Err(err),
        }
    }

    /// Add a claimset to the database.
    pub fn add_claimset(&mut self, claimset: ClaimSet) {
        self.1.insert(claimset.id.clone(), claimset);
    }

    /// Remove a claims set from the database so that all additional validation checks fail.
    pub fn revoke_claimset(&mut self, claim: &ClaimSet) {
        self.1.remove(&claim.id);
    }

    /// Revoke a ClaimsSet given its ID, which is set in the `jti` claim of a JWT or the `id` field
    /// of a `ClaimSet`.
    pub fn revoke_by_uuid(&mut self, claim_id: &String) {
        self.1.remove(claim_id);
    }

    /// *NOT IMPLEMENTED*
    pub fn replace_claimsets(&mut self, _claims_lst: Vec<ClaimSet>) {
        unimplemented!()
    }

    /// List all of the `ClaimSet` IDs in the database.
    pub fn list_claimsets(&self) -> Vec<&ClaimSet> {
        self.1.values().map(|item| item).collect()
    }

    /// Find a `ClaimSet` by ID.
    pub fn find_claimset(&self, claims_id: &String) -> Option<&ClaimSet> {
        self.1.get(claims_id)
    }

    /// Encode and sign a claimset, returning the result as a `VerifiedToken`.
    pub fn encode_claimset(&self, claims: &ClaimSet) -> Result<VerifiedToken> {
        let in_db = self.1.get(&claims.id);
        if in_db.is_some() {
            let text = jwt::encode(
                &jwt::Header::default(),
                &ClaimSetJS::from_claimset(&claims),
                &(self.0).0,
            );
            match text {
                Ok(text_) => Ok(VerifiedToken {
                    text: text_,
                    claims: claims.clone(),
                }),
                Err(err) => Err(Error::JWTError(err)),
            }
        } else {
            Err(Error::UnknownToken)
        }
    }
}