diff --git a/identity-api-rs/src/auth.rs b/identity-api-rs/src/auth.rs index b62807e..b0d32ac 100644 --- a/identity-api-rs/src/auth.rs +++ b/identity-api-rs/src/auth.rs @@ -1,3 +1,5 @@ +use std::time::SystemTime; + use crate::env; use jsonwebtoken::{TokenData, Header, Validation}; use serde::{Serialize, Deserialize}; @@ -16,4 +18,11 @@ pub fn encode_jwt(claims: &JwtUser) -> jsonwebtoken::errors::Result { pub fn decode_jwt(jwt: &str) -> jsonwebtoken::errors::Result> { jsonwebtoken::decode::(jwt, &env::jwt_secret().1, &Validation::new(*env::jwt_alg())) +} + +pub fn expiration_time() -> u64 { + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("time went backwards") + .as_secs() + 180 * 24 * 3600 } \ No newline at end of file diff --git a/identity-api-rs/src/http/routes/auth.rs b/identity-api-rs/src/http/routes/auth.rs index cb75048..5f690dd 100644 --- a/identity-api-rs/src/http/routes/auth.rs +++ b/identity-api-rs/src/http/routes/auth.rs @@ -1,17 +1,18 @@ -use std::time::SystemTime; -use argon2::{Argon2, PasswordHasher, password_hash::{rand_core::OsRng, SaltString}}; +use argon2::{password_hash::{rand_core::OsRng, SaltString}, Argon2, PasswordHash, PasswordHasher, PasswordVerifier}; use axum::{extract::State, http::StatusCode, routing::{get, post}, Json, Router}; use chrono::{Utc, NaiveDateTime}; use diesel::{RunQueryDsl, ExpressionMethods}; use tracing::{error, info}; use serde::{Serialize, Deserialize}; use uuid::Uuid; -use crate::{database::actions, http::extractors::auth::ExtractUser, auth::JwtUser, AppState}; +use crate::{auth::{encode_jwt, expiration_time, JwtUser}, database::actions, http::extractors::auth::{ExtractJwtUser, ExtractUser}, AppState}; pub fn auth_router() -> Router { Router::new() .route("/auth/account", get(account)) .route("/auth/register", post(register)) + .route("/auth/login", post(login)) + .route("/auth/genkey", get(genkey)) } #[derive(Debug, Serialize)] @@ -33,6 +34,81 @@ async fn account(ExtractUser(user): ExtractUser) -> Result })) } +#[derive(Debug, Serialize)] +struct GenkeyResponse { + session_key: String, +} + +async fn genkey(State(state): State, ExtractJwtUser(user): ExtractJwtUser) -> Result, StatusCode> { + use crate::database::schema::session_keys::dsl::*; + + if let Ok(mut conn) = state.pool.get() { + let session_key = Uuid::new_v4().to_string(); + let result = diesel::insert_into(session_keys) + .values(( + user_id.eq(user.uid), + key.eq(&session_key), + )) + .execute(&mut conn); + + if result.is_ok() { + Ok(Json(GenkeyResponse { + session_key, + })) + } else { + error!("failed to insert into session_keys"); + Err(StatusCode::INTERNAL_SERVER_ERROR) + } + } else { + error!("failed to obtain pooled connection"); + Err(StatusCode::INTERNAL_SERVER_ERROR) + } + +} + +#[derive(Debug, Deserialize)] +struct LoginRequest { + email: String, + password: String, +} + +#[derive(Debug, Serialize)] +struct LoginResponse { + token: String, +} + +async fn login(State(state): State, Json(req): Json) -> Result, StatusCode> { + if let Ok(mut conn) = state.pool.get() { + if let Ok(Some(user)) = actions::user_by_email(&req.email, &mut conn) { + let parsed_hash = PasswordHash::new(&user.password).expect("invalid argon2 password hash"); + if Argon2::default().verify_password(req.password.as_bytes(), &parsed_hash).is_err() { + info!("failed login attempt, invalid password: {}", &req.email); + Err(StatusCode::UNAUTHORIZED) + } else { + info!("valid login attempt: {}", req.email); + match encode_jwt(&JwtUser { + uid: user.id, + email: user.email, + name: user.name, + exp: expiration_time(), + }) { + Ok(token) => Ok(Json(LoginResponse { token })), + Err(err) => { + error!("token couldn't be encoded: {:?}", err); + Err(StatusCode::INTERNAL_SERVER_ERROR) + } + } + } + } else { + info!("failed login attempt, email does not exist: {}", &req.email); + Err(StatusCode::UNAUTHORIZED) + } + } else { + error!("failed to obtain pooled connection"); + Err(StatusCode::INTERNAL_SERVER_ERROR) + } +} + #[derive(Debug, Deserialize)] struct RegisterRequest { email: String, @@ -105,10 +181,7 @@ async fn register(State(state): State, Json(req): Json Ok(Json(RegisterResponse { token })), Err(err) => {