jwtsec 0.2.0

JWT менеджер с поддержкой access и refresh токенов, проверкой IP и User-Agent
Documentation
/*!
JWT менеджер с поддержкой access и refresh токенов, проверкой IP и User-Agent.

Этот модуль позволяет:
- Генерировать access и refresh токены с разными TTL
- Проверять валидность токена, включая IP и User-Agent
- Автоматически проверять истечение токена
- Получать новый access-токен по валидному refresh-токену

# Пример использования

```rust
use std::time::Duration;
use jwtsec::jwt::{JwtManager, TokenType};

let manager = JwtManager::new("my_secret", Duration::from_secs(60), Duration::from_secs(3600));
let ip = "127.0.0.1";
let ua = "Mozilla/5.0";
let accept = "text/html";
let accept_encoding = "gzip";
let accept_language = "en-US";
let cache_control = "no-cache";

let access_token = manager.create_token("user1", ip, ua, TokenType::Access, accept, accept_encoding, accept_language, cache_control).unwrap();
let refresh_token = manager.create_token("user1", ip, ua, TokenType::Refresh, accept, accept_encoding, accept_language, cache_control).unwrap();

let claims = manager.verify_token(&access_token, ip, ua, accept, accept_encoding, accept_language, cache_control).unwrap();
let new_access = manager.refresh_access_token(&refresh_token, ip, ua, accept, accept_encoding, accept_language, cache_control).unwrap();
```
*/

use jsonwebtoken::{
    DecodingKey, EncodingKey, Header, TokenData, Validation, decode, encode, errors::ErrorKind,
    errors::Result,
};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::time::{Duration, SystemTime, UNIX_EPOCH};

/// Менеджер JWT токенов
#[derive(Debug)]
pub struct JwtManager {
    /// Базовый секрет в байтах
    base_secret: Vec<u8>,
    /// TTL для access токена
    access_ttl: Duration,
    /// TTL для refresh токена
    refresh_ttl: Duration,
}

/// Тип токена: access или refresh
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum TokenType {
    Access,
    Refresh,
}

/// Структура claims внутри токена
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
    /// Идентификатор пользователя
    pub sub: String,
    /// Время истечения токена (Unix timestamp)
    pub exp: usize,
    /// IP адрес клиента
    pub ip: String,
    /// User-Agent клиента
    pub user_agent: String,
    /// Тип токена (access или refresh)
    pub token_type: TokenType,
}

impl JwtManager {
    /// Создаёт новый менеджер JWT
    pub fn new(base_secret: &str, access_ttl: Duration, refresh_ttl: Duration) -> Self {
        JwtManager {
            base_secret: base_secret.as_bytes().to_vec(),
            access_ttl,
            refresh_ttl,
        }
    }

    /// Генерация ключа SHA256 на основе IP + User-Agent + базового секрета
    fn generate_key(
        &self,
        ip: &[u8],
        user_agent: &[u8],
        accept: &[u8],
        accept_encoding: &[u8],
        accept_language: &[u8],
        cache_control: &[u8],
    ) -> [u8; 32] {
        let mut hasher = Sha256::new();
        hasher.update(ip);
        hasher.update(user_agent);
        hasher.update(accept);
        hasher.update(accept_encoding);
        hasher.update(accept_language);
        hasher.update(cache_control);
        hasher.update(&self.base_secret);
        let result = hasher.finalize();
        let mut key = [0u8; 32];
        key.copy_from_slice(&result);
        key
    }

    /// Возвращает текущее время в секундах Unix
    fn now_ts() -> usize {
        SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap_or_else(|_| Duration::from_secs(0))
            .as_secs() as usize
    }

    /// Создание токена указанного типа
    pub fn create_token(
        &self,
        subject: &str,
        ip: &str,
        user_agent: &str,
        token_type: TokenType,
        accept: &str,
        accept_encoding: &str,
        accept_language: &str,
        cache_control: &str,
    ) -> Result<String> {
        let ttl = match token_type {
            TokenType::Access => self.access_ttl,
            TokenType::Refresh => self.refresh_ttl,
        };
        let exp = Self::now_ts() + ttl.as_secs() as usize;

        let claims = Claims {
            sub: subject.to_string(),
            exp,
            ip: ip.to_string(),
            user_agent: user_agent.to_string(),
            token_type,
        };

        let key = self.generate_key(
            ip.as_bytes(),
            user_agent.as_bytes(),
            accept.as_bytes(),
            accept_encoding.as_bytes(),
            accept_language.as_bytes(),
            cache_control.as_bytes(),
        );

        encode(&Header::default(), &claims, &EncodingKey::from_secret(&key))
    }

    /// Проверка токена на валидность и истечение
    pub fn verify_token(
        &self,
        token: &str,
        ip: &str,
        user_agent: &str,
        accept: &str,
        accept_encoding: &str,
        accept_language: &str,
        cache_control: &str,
    ) -> Result<TokenData<Claims>> {
        let key = self.generate_key(
            ip.as_bytes(),
            user_agent.as_bytes(),
            accept.as_bytes(),
            accept_encoding.as_bytes(),
            accept_language.as_bytes(),
            cache_control.as_bytes(),
        );

        let data = decode::<Claims>(
            token,
            &DecodingKey::from_secret(&key),
            &Validation::default(),
        )?;

        if data.claims.exp < Self::now_ts() {
            return Err(jsonwebtoken::errors::Error::from(
                ErrorKind::ExpiredSignature,
            ));
        }

        Ok(data)
    }

    /// Получение нового access токена по refresh токену
    pub fn refresh_access_token(
        &self,
        refresh_token: &str,
        ip: &str,
        user_agent: &str,
        accept: &str,
        accept_encoding: &str,
        accept_language: &str,
        cache_control: &str,
    ) -> Result<String> {
        let data = self.verify_token(
            refresh_token,
            ip,
            user_agent,
            accept,
            accept_encoding,
            accept_language,
            cache_control,
        )?;

        if data.claims.token_type != TokenType::Refresh {
            return Err(jsonwebtoken::errors::Error::from(ErrorKind::InvalidToken));
        }

        self.create_token(
            &data.claims.sub,
            ip,
            user_agent,
            TokenType::Access,
            accept,
            accept_encoding,
            accept_language,
            cache_control,
        )
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::time::Duration;

    #[test]
    fn test_access_and_refresh() {
        let manager = JwtManager::new("secret", Duration::from_secs(2), Duration::from_secs(10));
        let ip = "127.0.0.1";
        let ua = "Mozilla/5.0";
        let accept = "text/html";
        let accept_encoding = "gzip";
        let accept_language = "en-US";
        let cache_control = "no-cache";

        let access_token = manager
            .create_token(
                "user1",
                ip,
                ua,
                TokenType::Access,
                accept,
                accept_encoding,
                accept_language,
                cache_control,
            )
            .unwrap();

        let refresh_token = manager
            .create_token(
                "user1",
                ip,
                ua,
                TokenType::Refresh,
                accept,
                accept_encoding,
                accept_language,
                cache_control,
            )
            .unwrap();

        let claims = manager
            .verify_token(
                &access_token,
                ip,
                ua,
                accept,
                accept_encoding,
                accept_language,
                cache_control,
            )
            .unwrap();
        assert_eq!(claims.claims.token_type, TokenType::Access);

        let claims = manager
            .verify_token(
                &refresh_token,
                ip,
                ua,
                accept,
                accept_encoding,
                accept_language,
                cache_control,
            )
            .unwrap();
        assert_eq!(claims.claims.token_type, TokenType::Refresh);

        let new_access = manager
            .refresh_access_token(
                &refresh_token,
                ip,
                ua,
                accept,
                accept_encoding,
                accept_language,
                cache_control,
            )
            .unwrap();

        let new_claims = manager
            .verify_token(
                &new_access,
                ip,
                ua,
                accept,
                accept_encoding,
                accept_language,
                cache_control,
            )
            .unwrap();

        assert_eq!(new_claims.claims.token_type, TokenType::Access);
        assert_eq!(new_claims.claims.sub, "user1");
    }
}