feat: add password entry auth flow
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
use std::{collections::HashSet, error::Error, fmt};
|
||||
|
||||
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier, password_hash::SaltString};
|
||||
use jsonwebtoken::{
|
||||
Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode, errors::ErrorKind,
|
||||
};
|
||||
use rand_core::OsRng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use time::{Duration, OffsetDateTime};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub const ACCESS_TOKEN_ALGORITHM: Algorithm = Algorithm::HS256;
|
||||
pub const DEFAULT_ACCESS_TOKEN_TTL_SECONDS: u64 = 2 * 60 * 60;
|
||||
@@ -98,6 +101,12 @@ pub enum RefreshCookieError {
|
||||
InvalidConfig(&'static str),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum PasswordHashError {
|
||||
HashFailed(String),
|
||||
VerifyFailed(String),
|
||||
}
|
||||
|
||||
impl JwtConfig {
|
||||
pub fn new(
|
||||
issuer: String,
|
||||
@@ -370,6 +379,53 @@ pub fn read_refresh_session_token(
|
||||
None
|
||||
}
|
||||
|
||||
pub async fn hash_password(password: &str) -> Result<String, PasswordHashError> {
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
Argon2::default()
|
||||
.hash_password(password.as_bytes(), &salt)
|
||||
.map(|hash| hash.to_string())
|
||||
.map_err(|error| PasswordHashError::HashFailed(format!("密码哈希失败:{error}")))
|
||||
}
|
||||
|
||||
pub async fn verify_password(
|
||||
password_hash: &str,
|
||||
password: &str,
|
||||
) -> Result<bool, PasswordHashError> {
|
||||
let parsed_hash = PasswordHash::new(password_hash)
|
||||
.map_err(|error| PasswordHashError::VerifyFailed(format!("密码哈希格式非法:{error}")))?;
|
||||
|
||||
Ok(Argon2::default()
|
||||
.verify_password(password.as_bytes(), &parsed_hash)
|
||||
.is_ok())
|
||||
}
|
||||
|
||||
pub fn create_refresh_session_token() -> String {
|
||||
Uuid::new_v4().simple().to_string()
|
||||
}
|
||||
|
||||
pub fn build_refresh_session_set_cookie(token: &str, config: &RefreshCookieConfig) -> String {
|
||||
let mut parts = vec![
|
||||
format!(
|
||||
"{}={}",
|
||||
config.cookie_name(),
|
||||
urlencoding::encode(token).into_owned()
|
||||
),
|
||||
format!("Path={}", config.cookie_path()),
|
||||
"HttpOnly".to_string(),
|
||||
format!("SameSite={}", config.cookie_same_site().as_str()),
|
||||
format!(
|
||||
"Max-Age={}",
|
||||
u64::from(config.refresh_session_ttl_days()) * 24 * 60 * 60
|
||||
),
|
||||
];
|
||||
|
||||
if config.cookie_secure() {
|
||||
parts.push("Secure".to_string());
|
||||
}
|
||||
|
||||
parts.join("; ")
|
||||
}
|
||||
|
||||
impl fmt::Display for JwtError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
@@ -391,6 +447,16 @@ impl fmt::Display for RefreshCookieError {
|
||||
|
||||
impl Error for RefreshCookieError {}
|
||||
|
||||
impl fmt::Display for PasswordHashError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::HashFailed(message) | Self::VerifyFailed(message) => f.write_str(message),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for PasswordHashError {}
|
||||
|
||||
fn normalize_required_field(
|
||||
value: String,
|
||||
error_message: &'static str,
|
||||
@@ -560,4 +626,29 @@ mod tests {
|
||||
|
||||
assert!(token.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hash_and_verify_password_round_trip() {
|
||||
let password_hash = hash_password("secret123")
|
||||
.await
|
||||
.expect("password hash should build");
|
||||
|
||||
let is_valid = verify_password(&password_hash, "secret123")
|
||||
.await
|
||||
.expect("password hash should verify");
|
||||
|
||||
assert!(is_valid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_refresh_session_cookie_respects_config() {
|
||||
let cookie =
|
||||
build_refresh_session_set_cookie("refresh/token=01", &build_refresh_cookie_config());
|
||||
|
||||
assert!(cookie.contains("genarrative_refresh_session=refresh%2Ftoken%3D01"));
|
||||
assert!(cookie.contains("Path=/api/auth"));
|
||||
assert!(cookie.contains("HttpOnly"));
|
||||
assert!(cookie.contains("SameSite=Lax"));
|
||||
assert!(cookie.contains("Max-Age=2592000"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user