Improve security
This commit is contained in:
+53
-11
@@ -2,17 +2,32 @@ extern crate hmac;
|
||||
extern crate jwt;
|
||||
extern crate sha2;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::env;
|
||||
|
||||
use actix_web::HttpRequest;
|
||||
use chrono::{Duration, Utc};
|
||||
use dotenv::dotenv;
|
||||
use hmac::{Hmac, Mac};
|
||||
use jwt::{Header, SignWithKey, Token, VerifyWithKey};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::Sha256;
|
||||
|
||||
/// How long a freshly issued token remains valid for.
|
||||
const TOKEN_LIFETIME_HOURS: i64 = 24;
|
||||
|
||||
pub struct JwtToken {
|
||||
pub user_id: i32,
|
||||
pub token_version: i32,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Claims {
|
||||
user_id: i32,
|
||||
/// Must match `users.token_version` for the token to be accepted; bumping
|
||||
/// the column (e.g. on logout) revokes every token issued before that point.
|
||||
tv: i32,
|
||||
/// Unix timestamp after which the token is rejected, independent of signature validity.
|
||||
exp: i64,
|
||||
}
|
||||
|
||||
type HmacSha256 = Hmac<Sha256>;
|
||||
@@ -25,11 +40,14 @@ fn signing_key() -> HmacSha256 {
|
||||
}
|
||||
|
||||
impl JwtToken {
|
||||
pub fn encode(user_id: i32) -> String {
|
||||
pub fn encode(user_id: i32, token_version: i32) -> String {
|
||||
let key: HmacSha256 = signing_key();
|
||||
let mut claims = BTreeMap::new();
|
||||
claims.insert("user_id", user_id);
|
||||
// Signing a simple map of claims with a valid HMAC key cannot fail.
|
||||
let claims = Claims {
|
||||
user_id,
|
||||
tv: token_version,
|
||||
exp: (Utc::now() + Duration::hours(TOKEN_LIFETIME_HOURS)).timestamp(),
|
||||
};
|
||||
// Signing claims with a valid HMAC key cannot fail.
|
||||
claims
|
||||
.sign_with_key(&key)
|
||||
.expect("signing claims with a valid HMAC key cannot fail")
|
||||
@@ -38,15 +56,18 @@ impl JwtToken {
|
||||
pub fn decode(encoded_token: String) -> Result<JwtToken, &'static str> {
|
||||
let key: HmacSha256 = signing_key();
|
||||
let token_str: &str = encoded_token.as_str();
|
||||
let token: Result<Token<Header, BTreeMap<String, i32>, jwt::Verified>, jwt::Error> =
|
||||
let token: Result<Token<Header, Claims, jwt::Verified>, jwt::Error> =
|
||||
VerifyWithKey::verify_with_key(token_str, &key);
|
||||
|
||||
match token {
|
||||
Ok(token) => {
|
||||
let _header = token.header();
|
||||
let claims = token.claims();
|
||||
if claims.exp < Utc::now().timestamp() {
|
||||
return Err("token has expired");
|
||||
}
|
||||
Ok(JwtToken {
|
||||
user_id: claims["user_id"],
|
||||
user_id: claims.user_id,
|
||||
token_version: claims.tv,
|
||||
})
|
||||
}
|
||||
Err(_err) => Err("could not decode token"),
|
||||
@@ -68,14 +89,19 @@ impl JwtToken {
|
||||
#[cfg(test)]
|
||||
mod jwt_test {
|
||||
use actix_web::{http::header, test};
|
||||
use chrono::{Duration, Utc};
|
||||
use hmac::Hmac;
|
||||
use jwt::SignWithKey;
|
||||
use sha2::Sha256;
|
||||
|
||||
use super::JwtToken;
|
||||
use super::{Claims, JwtToken};
|
||||
|
||||
#[test]
|
||||
async fn encode_decode() {
|
||||
let encoded_token: String = JwtToken::encode(32);
|
||||
let encoded_token: String = JwtToken::encode(32, 0);
|
||||
let decoded_token: JwtToken = JwtToken::decode(encoded_token).unwrap();
|
||||
assert_eq!(32, decoded_token.user_id);
|
||||
assert_eq!(0, decoded_token.token_version);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -88,9 +114,25 @@ mod jwt_test {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn decode_expired_token() {
|
||||
let key: Hmac<Sha256> = super::signing_key();
|
||||
let claims = Claims {
|
||||
user_id: 32,
|
||||
tv: 0,
|
||||
exp: (Utc::now() - Duration::hours(1)).timestamp(),
|
||||
};
|
||||
let expired_token: String = claims.sign_with_key(&key).unwrap();
|
||||
|
||||
match JwtToken::decode(expired_token) {
|
||||
Err(message) => assert_eq!(message, "token has expired"),
|
||||
_ => panic!("Expired token should not be accepted."),
|
||||
}
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn decode_from_request_with_correct_token() {
|
||||
let encoded_token: String = JwtToken::encode(32);
|
||||
let encoded_token: String = JwtToken::encode(32, 0);
|
||||
let request = test::TestRequest::default()
|
||||
.insert_header(header::ContentType::json())
|
||||
.insert_header(("user-token", encoded_token))
|
||||
|
||||
Reference in New Issue
Block a user