jwtsec 0.1.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 access_token = manager.create_token("user1", ip, ua, TokenType::Access).unwrap();
let refresh_token = manager.create_token("user1", ip, ua, TokenType::Refresh).unwrap();

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

use jsonwebtoken::{
    decode, encode, errors::ErrorKind, errors::Result, DecodingKey, EncodingKey, Header, TokenData,
    Validation,
};
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]) -> [u8; 32] {
        let mut hasher = Sha256::new();
        hasher.update(ip);
        hasher.update(user_agent);
        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,
    ) -> 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());
        encode(&Header::default(), &claims, &EncodingKey::from_secret(&key))
    }

    /// Проверка токена на валидность и истечение
    pub fn verify_token(
        &self,
        token: &str,
        ip: &str,
        user_agent: &str,
    ) -> Result<TokenData<Claims>> {
        let key = self.generate_key(ip.as_bytes(), user_agent.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,
    ) -> Result<String> {
        let data = self.verify_token(refresh_token, ip, user_agent)?;
        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)
    }
}

#[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 access_token = manager
            .create_token("user1", ip, ua, TokenType::Access)
            .unwrap();
        let refresh_token = manager
            .create_token("user1", ip, ua, TokenType::Refresh)
            .unwrap();

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

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

        let new_access = manager
            .refresh_access_token(&refresh_token, ip, ua)
            .unwrap();
        let new_claims = manager.verify_token(&new_access, ip, ua).unwrap();
        assert_eq!(new_claims.claims.token_type, TokenType::Access);
        assert_eq!(new_claims.claims.sub, "user1");
    }
}