cc_auth/
lib.rs

1//! Crate that implementing simple authorization system.
2//!
3//! CC Auth uses passwords' hashing with salts, SHA3-256 hash function and Redis-like tokens' storage.
4//!
5//! Usage:
6//!
7//! ```rust
8//! use bb8_redis::{RedisConnectionManager, bb8::Pool};
9//! use cc_auth::{ApiToken, check_token};
10//! use cc_utils::prelude::MResult;
11//!
12//! pub async fn authorized_action(
13//!   cacher: &Pool<RedisConnectionManager>,
14//!   token: ApiToken,
15//! ) -> MResult<()> {
16//!   let user_id = check_token(&token, cacher).await?;
17//!   Ok(())
18//! }
19//! ```
20
21#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
22use bb8_redis::redis::{AsyncCommands, LposOptions};
23
24#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
25use cc_utils::results::MResult;
26
27#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
28use chrono::Duration;
29
30#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
31use passwords::PasswordGenerator;
32
33use chrono::{DateTime, Utc, serde::ts_seconds};
34use serde::{Deserialize, Serialize};
35use sha3::{Digest, Sha3_256};
36
37/// Standard token length (64 UTF-8 symbols).
38#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
39const TOKEN_LENGTH: usize = 64;
40
41/// Prefix for tokens' location in Redis-like database.
42const TOKEN_PREFIX: &str = "user_tokens";
43
44/// Limit of tokens for one user (3 tokens). If the token limit is exceeded, old tokens will be overwritten.
45pub const MAX_TOKENS_PER_USER: isize = 3;
46
47/// Limit of token validation time (each token lives 28 days).
48pub const DAYS_VALID: i64 = 28;
49
50/// User identifier type.
51///
52/// You can use in your own code any ID type you want that convertible into u64.
53pub type UserId = u64;
54
55/// Holds user token.
56#[derive(Deserialize, Serialize)]
57pub struct UserToken {
58  pub user_id: UserId,
59  token_str: String,
60  #[serde(with = "ts_seconds")]
61  birth: DateTime<Utc>,
62}
63
64#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
65impl UserToken {
66  pub fn new(id: UserId) -> MResult<Self> { generate_token(id) }
67}
68
69/// Token as string (e.g. one that got from `Authorization` header).
70pub type ApiToken = String;
71
72/// Gets the salted password's SHA3-256 hash.
73pub fn hash_password(user_password: &[u8], user_salt: &[u8]) -> Vec<u8> {
74  let mut hasher = Sha3_256::new();
75  hasher.update([user_password, user_salt].concat());
76  hasher.finalize().to_vec()
77}
78
79/// Checks the password is correct.
80pub fn hashes_eq(user_password: &[u8], salt_from_db: &[u8], hash_from_db: &[u8]) -> bool {
81  hash_password(user_password, salt_from_db).eq(hash_from_db)
82}
83
84/// Returns the name of the list in Redis-like DB that stores the users' tokens.
85pub fn get_user_tokens_list_name(user_id: UserId) -> String {
86  format!("{}:id{}", TOKEN_PREFIX, user_id)
87}
88
89/// Authorizes the user by creating a new token for him if the data is correct.
90#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
91pub async fn log_in(
92  user_login: String,
93  salt_db: &[u8],
94  hash_db: &[u8],
95  possible_user_id: UserId,
96  cacher: &bb8_redis::bb8::Pool<bb8_redis::RedisConnectionManager>,
97) -> MResult<UserToken> {
98  if !hashes_eq(user_login.as_bytes(), salt_db, hash_db) { return Err("Hashes are not equal.".into()) } ;
99  let utl_name = get_user_tokens_list_name(possible_user_id);
100  let mut cacher_conn = cacher.get().await?;
101  let user_tokens_list_len: isize = cacher_conn.llen(&utl_name).await?;
102  let token = generate_token(possible_user_id)?;
103  if user_tokens_list_len >= MAX_TOKENS_PER_USER { cacher_conn.ltrim(&utl_name, 0, MAX_TOKENS_PER_USER - 1).await?; }
104  cacher_conn.lpush(&utl_name, &serde_json::to_string(&token)?).await?;
105  Ok(token)
106}
107
108/// Validates the user by token via Redis-like DB.
109#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
110pub async fn check_token(
111  token: &ApiToken,
112  cacher: &bb8_redis::bb8::Pool<bb8_redis::RedisConnectionManager>,
113) -> MResult<UserId> {
114  let token_data = serde_json::from_str::<UserToken>(&token)?;
115  let user_tokens_list = get_user_tokens_list_name(token_data.user_id);
116  let mut cacher_conn = cacher.get().await?;
117  let idx: Option<i32> = cacher_conn.lpos(&user_tokens_list, &token, LposOptions::default()).await?;
118  if idx.is_none() { return Err("There is no such tokens.".into()) }
119  let duration: Duration = Utc::now() - token_data.birth;
120  if duration.num_days() >= DAYS_VALID {
121    cacher_conn.lrem(user_tokens_list, 1, &token).await?;
122    return Err("The token is expired.".into())
123  }
124  Ok(token_data.user_id)
125}
126
127/// Removes the valid token from Redis-like DB.
128#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
129pub async fn check_and_remove_token(
130  token: &ApiToken,
131  cacher: &bb8_redis::bb8::Pool<bb8_redis::RedisConnectionManager>,
132) -> MResult<()> {
133  let token_data = serde_json::from_str::<UserToken>(&token)?;
134  let user_tokens_list = get_user_tokens_list_name(token_data.user_id);
135  let mut cacher_conn = cacher.get().await?;
136  let idx: Option<i32> = cacher_conn.lpos(&user_tokens_list, &token, LposOptions::default()).await?;
137  if idx.is_none() { return Err("There is no such tokens.".into()) }
138  cacher_conn.lrem(user_tokens_list, 1, &token).await?;
139  Ok(())
140}
141
142/// Creates fixed length password generator.
143#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
144fn get_password_generator(length: usize) -> PasswordGenerator {
145  PasswordGenerator {
146    length,
147    numbers: true,
148    lowercase_letters: true,
149    uppercase_letters: true,
150    symbols: true,
151    strict: true,
152    exclude_similar_characters: true,
153    spaces: false,
154  }
155}
156
157/// Generates new token for user.
158#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
159pub fn generate_token(user_id: UserId) -> MResult<UserToken> {
160  Ok(UserToken {
161    user_id,
162    token_str: get_password_generator(TOKEN_LENGTH).generate_one()?,
163    birth: Utc::now(),
164  })
165}
166
167/// Generates salt for new user.
168#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
169pub fn generate_salt() -> MResult<String> {
170  Ok(get_password_generator(16).generate_one()?)
171}