2023-08-01 16:29:03 +02:00
|
|
|
use std::ops::Add;
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
|
|
|
use actix_jwt_authc::*;
|
2023-08-02 08:56:53 +02:00
|
|
|
use actix_web::web::{Data, Json, ServiceConfig};
|
2023-08-01 22:38:56 +02:00
|
|
|
use actix_web::{get, post, HttpResponse};
|
2023-08-01 16:29:03 +02:00
|
|
|
use dashmap::DashSet;
|
2023-08-01 22:06:04 +02:00
|
|
|
use futures::channel::{mpsc, mpsc::Sender};
|
2023-08-01 16:29:03 +02:00
|
|
|
use futures::stream::Stream;
|
2023-08-01 22:06:04 +02:00
|
|
|
use futures::SinkExt;
|
2023-08-01 16:29:03 +02:00
|
|
|
use jsonwebtoken::*;
|
|
|
|
use ring::rand::SystemRandom;
|
|
|
|
use ring::signature::{Ed25519KeyPair, KeyPair};
|
2023-08-01 22:38:56 +02:00
|
|
|
use sea_orm::{DatabaseConnection, EntityTrait};
|
2023-08-01 16:29:03 +02:00
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use time::ext::*;
|
|
|
|
use time::OffsetDateTime;
|
|
|
|
use tokio::sync::Mutex;
|
2023-08-01 22:06:04 +02:00
|
|
|
use uuid::Uuid;
|
2023-08-01 16:29:03 +02:00
|
|
|
|
2023-08-01 22:06:04 +02:00
|
|
|
pub type UserSession = Authenticated<Claims>;
|
|
|
|
|
|
|
|
#[derive(Clone, Copy)]
|
|
|
|
pub struct JWTTtl(time::Duration);
|
|
|
|
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
|
|
|
|
pub struct Claims {
|
|
|
|
exp: usize,
|
|
|
|
iat: usize,
|
|
|
|
sub: String,
|
2023-08-01 16:29:03 +02:00
|
|
|
}
|
|
|
|
|
2023-08-01 22:06:04 +02:00
|
|
|
#[derive(Serialize, Deserialize)]
|
|
|
|
pub struct EmptyResponse {}
|
|
|
|
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
|
|
pub struct LoginResponse {
|
|
|
|
bearer_token: String,
|
|
|
|
claims: Claims,
|
2023-08-01 16:29:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
const JWT_SIGNING_ALGO: Algorithm = Algorithm::EdDSA;
|
|
|
|
|
2023-08-01 22:06:04 +02:00
|
|
|
#[derive(Clone)]
|
|
|
|
pub struct SessionConfigurator {
|
|
|
|
jwt_ttl: Data<JWTTtl>,
|
|
|
|
invalidated_jwts_store: Data<InvalidatedJWTStore>,
|
|
|
|
encoding_key: Data<EncodingKey>,
|
|
|
|
factory: AuthenticateMiddlewareFactory<Claims>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl SessionConfigurator {
|
|
|
|
pub fn app_data(self, config: &mut ServiceConfig) {
|
|
|
|
config
|
|
|
|
.app_data(self.invalidated_jwts_store)
|
|
|
|
.app_data(self.encoding_key)
|
|
|
|
.app_data(self.jwt_ttl)
|
|
|
|
.service(login)
|
|
|
|
.service(logout)
|
|
|
|
.service(session_info);
|
|
|
|
}
|
|
|
|
|
2023-08-02 08:56:53 +02:00
|
|
|
pub fn factory(&self) -> AuthenticateMiddlewareFactory<Claims> {
|
|
|
|
self.factory.clone()
|
|
|
|
}
|
2023-08-01 22:06:04 +02:00
|
|
|
|
|
|
|
pub fn new() -> Self {
|
|
|
|
let jwt_ttl = JWTTtl(31.days());
|
|
|
|
let jwt_signing_keys = JwtSigningKeys::generate().unwrap();
|
|
|
|
let validator = Validation::new(JWT_SIGNING_ALGO);
|
|
|
|
let auth_middleware_settings = AuthenticateMiddlewareSettings {
|
|
|
|
jwt_decoding_key: jwt_signing_keys.decoding_key,
|
|
|
|
jwt_authorization_header_prefixes: Some(vec!["Bearer".to_string()]),
|
|
|
|
jwt_validator: validator,
|
|
|
|
jwt_session_key: None,
|
|
|
|
};
|
|
|
|
let (invalidated_jwts_store, stream) = InvalidatedJWTStore::new_with_stream();
|
|
|
|
let auth_middleware_factory =
|
|
|
|
AuthenticateMiddlewareFactory::<Claims>::new(stream, auth_middleware_settings.clone());
|
|
|
|
|
|
|
|
Self {
|
|
|
|
invalidated_jwts_store: Data::new(invalidated_jwts_store.clone()),
|
|
|
|
encoding_key: Data::new(jwt_signing_keys.encoding_key.clone()),
|
|
|
|
jwt_ttl: Data::new(jwt_ttl.clone()),
|
|
|
|
factory: auth_middleware_factory,
|
|
|
|
}
|
|
|
|
}
|
2023-08-01 16:29:03 +02:00
|
|
|
}
|
|
|
|
|
2023-08-01 22:38:56 +02:00
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
|
|
|
|
pub struct SignInPayload {
|
|
|
|
login: String,
|
|
|
|
password: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[post("/login")]
|
2023-08-01 16:29:03 +02:00
|
|
|
async fn login(
|
|
|
|
jwt_encoding_key: Data<EncodingKey>,
|
2023-08-01 22:06:04 +02:00
|
|
|
jwt_ttl: Data<JWTTtl>,
|
2023-08-01 22:38:56 +02:00
|
|
|
db: Data<DatabaseConnection>,
|
|
|
|
payload: Json<SignInPayload>,
|
2023-08-01 16:29:03 +02:00
|
|
|
) -> Result<HttpResponse, Error> {
|
2023-08-01 22:38:56 +02:00
|
|
|
let db = db.into_inner();
|
|
|
|
let payload = payload.into_inner();
|
|
|
|
|
2023-08-01 16:29:03 +02:00
|
|
|
let sub = format!("{}", Uuid::new_v4().as_u128());
|
|
|
|
let iat = OffsetDateTime::now_utc().unix_timestamp() as usize;
|
|
|
|
let expires_at = OffsetDateTime::now_utc().add(jwt_ttl.0);
|
|
|
|
let exp = expires_at.unix_timestamp() as usize;
|
|
|
|
|
2023-08-01 22:38:56 +02:00
|
|
|
use sea_orm::*;
|
2023-08-02 08:56:53 +02:00
|
|
|
let account = match oswilno_contract::accounts::Entity::find()
|
|
|
|
.filter(oswilno_contract::accounts::Column::Login.eq(payload.login.as_str()))
|
|
|
|
.one(&*db)
|
|
|
|
.await
|
|
|
|
{
|
2023-08-01 22:38:56 +02:00
|
|
|
Ok(a) => a,
|
|
|
|
Err(e) => {
|
|
|
|
tracing::warn!("Failed to find account: {e}");
|
2023-08-02 08:56:53 +02:00
|
|
|
return Err(Error::InternalError);
|
2023-08-01 22:38:56 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2023-08-01 16:29:03 +02:00
|
|
|
let jwt_claims = Claims { iat, exp, sub };
|
|
|
|
let jwt_token = encode(
|
|
|
|
&Header::new(JWT_SIGNING_ALGO),
|
|
|
|
&jwt_claims,
|
|
|
|
&jwt_encoding_key,
|
|
|
|
)
|
|
|
|
.map_err(|_| Error::InternalError)?;
|
|
|
|
let login_response = LoginResponse {
|
|
|
|
bearer_token: jwt_token,
|
|
|
|
claims: jwt_claims,
|
|
|
|
};
|
|
|
|
|
|
|
|
Ok(HttpResponse::Ok().json(login_response))
|
|
|
|
}
|
|
|
|
|
|
|
|
#[get("/session")]
|
2023-08-01 22:06:04 +02:00
|
|
|
async fn session_info(authenticated: UserSession) -> Result<HttpResponse, Error> {
|
2023-08-01 16:29:03 +02:00
|
|
|
Ok(HttpResponse::Ok().json(authenticated))
|
|
|
|
}
|
|
|
|
|
|
|
|
#[get("/logout")]
|
|
|
|
async fn logout(
|
|
|
|
invalidated_jwts: Data<InvalidatedJWTStore>,
|
2023-08-01 22:06:04 +02:00
|
|
|
authenticated: Authenticated<Claims>,
|
2023-08-01 16:29:03 +02:00
|
|
|
) -> Result<HttpResponse, Error> {
|
|
|
|
invalidated_jwts.add_to_invalidated(authenticated).await;
|
|
|
|
Ok(HttpResponse::Ok().json(EmptyResponse {}))
|
|
|
|
}
|
2023-08-01 22:06:04 +02:00
|
|
|
|
2023-08-02 08:56:53 +02:00
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
|
|
|
|
struct AccountInfo {
|
|
|
|
login: String,
|
|
|
|
password: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[post("/register")]
|
|
|
|
async fn register(
|
|
|
|
invalidated_jwts: Data<InvalidatedJWTStore>,
|
|
|
|
authenticated: Authenticated<Claims>,
|
|
|
|
db: Data<DatabaseConnection>,
|
|
|
|
payload: Json<AccountInfo>,
|
|
|
|
) -> Result<HttpResponse, Error> {
|
|
|
|
use sea_orm::*;
|
|
|
|
use oswilno_contract::accounts::*;
|
|
|
|
|
|
|
|
Ok(HttpResponse::Ok().json(EmptyResponse {}))
|
|
|
|
}
|
|
|
|
|
2023-08-01 22:06:04 +02:00
|
|
|
#[derive(Clone)]
|
|
|
|
struct InvalidatedJWTStore {
|
|
|
|
store: Arc<DashSet<JWT>>,
|
|
|
|
tx: Arc<Mutex<Sender<InvalidatedTokensEvent>>>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl InvalidatedJWTStore {
|
|
|
|
/// Returns a [InvalidatedJWTStore] with a Stream of [InvalidatedTokensEvent]s
|
|
|
|
fn new_with_stream() -> (
|
|
|
|
InvalidatedJWTStore,
|
|
|
|
impl Stream<Item = InvalidatedTokensEvent>,
|
|
|
|
) {
|
|
|
|
let invalidated = Arc::new(DashSet::new());
|
|
|
|
let (tx, rx) = mpsc::channel(100);
|
|
|
|
let tx_to_hold = Arc::new(Mutex::new(tx));
|
|
|
|
(
|
|
|
|
InvalidatedJWTStore {
|
|
|
|
store: invalidated,
|
|
|
|
tx: tx_to_hold,
|
|
|
|
},
|
|
|
|
rx,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn add_to_invalidated(&self, authenticated: Authenticated<Claims>) {
|
|
|
|
self.store.insert(authenticated.jwt.clone());
|
|
|
|
let mut tx = self.tx.lock().await;
|
|
|
|
if let Err(_e) = tx
|
|
|
|
.send(InvalidatedTokensEvent::Add(authenticated.jwt))
|
|
|
|
.await
|
|
|
|
{
|
|
|
|
#[cfg(feature = "tracing")]
|
|
|
|
error!(error = ?_e, "Failed to send update on adding to invalidated")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct JwtSigningKeys {
|
|
|
|
encoding_key: EncodingKey,
|
|
|
|
decoding_key: DecodingKey,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl JwtSigningKeys {
|
|
|
|
fn generate() -> Result<Self, Box<dyn std::error::Error>> {
|
|
|
|
let doc = Ed25519KeyPair::generate_pkcs8(&SystemRandom::new())?;
|
|
|
|
let keypair = Ed25519KeyPair::from_pkcs8(doc.as_ref())?;
|
|
|
|
let encoding_key = EncodingKey::from_ed_der(doc.as_ref());
|
|
|
|
let decoding_key = DecodingKey::from_ed_der(keypair.public_key().as_ref());
|
|
|
|
Ok(JwtSigningKeys {
|
|
|
|
encoding_key,
|
|
|
|
decoding_key,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2023-08-01 22:38:56 +02:00
|
|
|
|
|
|
|
mod hasing {
|
2023-08-02 08:56:53 +02:00
|
|
|
use argon2::{
|
|
|
|
password_hash::{
|
|
|
|
rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString,
|
|
|
|
},
|
|
|
|
Argon2
|
|
|
|
};
|
|
|
|
|
|
|
|
pub fn encrypt(password: &str) -> argon2::password_hash::Result<String> {
|
|
|
|
let salt = SaltString::generate(&mut OsRng);
|
|
|
|
let argon2 = Argon2::default();
|
|
|
|
argon2.hash_password(password.as_bytes(), &salt).map(|hash| hash.to_string())
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn verify(password_hash: &str, password: &str) -> argon2::password_hash::Result<()> {
|
|
|
|
let parsed_hash = PasswordHash::new(&password_hash)?;
|
|
|
|
Argon2::default()
|
|
|
|
.verify_password(password.as_bytes(), &parsed_hash)
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn check_always_random_salt() {
|
|
|
|
let pass = "ahs9dya8tsd7fa8tsa86tT&^R%^DS^%ARS&A";
|
|
|
|
let hash = encrypt(pass).unwrap();
|
|
|
|
assert!(verify( hash.as_str(), pass).is_ok());
|
|
|
|
}
|
2023-08-01 22:38:56 +02:00
|
|
|
}
|
|
|
|
}
|