extern crate hmac; extern crate jwt; extern crate sha2; 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 = 730; 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; fn signing_key() -> HmacSha256 { dotenv().ok(); let secret = env::var("JWT_SECRET").expect("JWT_SECRET must be set"); // HMAC-SHA256 accepts a key of any length, so this cannot fail. HmacSha256::new_from_slice(secret.as_bytes()).expect("HMAC accepts a key of any length") } impl JwtToken { pub fn encode(user_id: i32, token_version: i32) -> String { let key: HmacSha256 = signing_key(); 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") } pub fn decode(encoded_token: String) -> Result { let key: HmacSha256 = signing_key(); let token_str: &str = encoded_token.as_str(); let token: Result, jwt::Error> = VerifyWithKey::verify_with_key(token_str, &key); match token { Ok(token) => { let claims = token.claims(); if claims.exp < Utc::now().timestamp() { return Err("token has expired"); } Ok(JwtToken { user_id: claims.user_id, token_version: claims.tv, }) } Err(_err) => Err("could not decode token"), } } #[allow(dead_code)] pub fn decode_from_request(request: HttpRequest) -> Result { match request.headers().get("user-token") { Some(token) => match token.to_str() { Ok(token_str) => JwtToken::decode(String::from(token_str)), Err(_) => Err("token header is not valid text"), }, None => Err("There is no token"), } } } #[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::{Claims, JwtToken}; #[test] async fn encode_decode() { 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] async fn decode_incorrect_token() { let encoded_token: String = String::from("test"); match JwtToken::decode(encoded_token) { Err(message) => assert_eq!(message, "could not decode token"), _ => panic!("Incorrect token should not be able to decode."), } } #[test] async fn decode_expired_token() { let key: Hmac = 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, 0); let request = test::TestRequest::default() .insert_header(header::ContentType::json()) .insert_header(("user-token", encoded_token)) .to_http_request(); let out_come = JwtToken::decode_from_request(request); match out_come { Ok(token) => assert_eq!(32, token.user_id), _ => panic!("Token is not returned with it should be."), } } #[actix_web::test] async fn decode_from_request_with_no_token() { let request = test::TestRequest::default() .insert_header(("test", "test")) .to_http_request(); let out_come = JwtToken::decode_from_request(request); match out_come { Err(message) => assert_eq!("There is no token", message), _ => panic!("Token should not be returned when it is not present in the header."), } } }