initial auth stuff
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 10m55s
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 10m55s
This commit is contained in:
parent
eb436cf14c
commit
f78054fecd
@ -1,8 +1,8 @@
|
||||
name: test
|
||||
variables:
|
||||
- name: access_token
|
||||
value: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI5NWM0NWVlNC1iNmY4LTRjY2ItYTEzNi0wMDVlYzdmMmNjMTMiLCJpYXQiOjE3Nzc1NjY3MjYsImV4cCI6MTc3NzU2NzYyNiwianRpIjoiNDU1MTFkNzQtNmU0OS00NzA2LTljOGYtNWY2ODcxYjgzNTY5In0.0VGbyYuExdt8K-WuAILZJCbIzGxy3_MhexgcDUpyiCI
|
||||
- name: refresh_token
|
||||
value: refresh_token=3f9e5d21748cebd5113604c045ed485ec82ef6ea46f539bd86f0a7de1b03d025; HttpOnly; SameSite=Strict; Secure; Path=/; Max-Age=604800
|
||||
- name: base_url
|
||||
value: http://localhost:6969
|
||||
- name: access_token
|
||||
value: ""
|
||||
- name: refresh_token
|
||||
value: ""
|
||||
|
||||
37
http_client/login.yml
Normal file
37
http_client/login.yml
Normal file
@ -0,0 +1,37 @@
|
||||
info:
|
||||
name: login
|
||||
type: http
|
||||
seq: 3
|
||||
|
||||
http:
|
||||
method: POST
|
||||
url: "{{base_url}}/api/v1/auth/login"
|
||||
body:
|
||||
type: json
|
||||
data: |
|
||||
{
|
||||
"email": "a@a.it",
|
||||
"password": "Password1!6969_"
|
||||
}
|
||||
auth: inherit
|
||||
|
||||
runtime:
|
||||
scripts:
|
||||
- type: after-response
|
||||
code: |-
|
||||
const response = res.getBody();
|
||||
const token = response.access_token;
|
||||
bru.setEnvVar("access_token", token);
|
||||
console.log("login - access_token:", token);
|
||||
|
||||
const cookies = res.getHeaders()['set-cookie'];
|
||||
if (cookies) {
|
||||
bru.setEnvVar("refresh_token", cookies[0]);
|
||||
console.log("login - refresh_token:", cookies[0]);
|
||||
}
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
28
http_client/logout.yml
Normal file
28
http_client/logout.yml
Normal file
@ -0,0 +1,28 @@
|
||||
info:
|
||||
name: logout
|
||||
type: http
|
||||
seq: 6
|
||||
|
||||
http:
|
||||
method: POST
|
||||
url: "{{base_url}}/api/v1/protected/auth/logout"
|
||||
auth:
|
||||
type: bearer
|
||||
token: "{{access_token}}"
|
||||
|
||||
runtime:
|
||||
scripts:
|
||||
- type: after-response
|
||||
code: |-
|
||||
const status = res.getStatus();
|
||||
if (status === 200 || status === 204) {
|
||||
bru.setEnvVar("access_token", "");
|
||||
bru.setEnvVar("refresh_token", "");
|
||||
console.log("logout - tokens cleared");
|
||||
}
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
28
http_client/logout_all.yml
Normal file
28
http_client/logout_all.yml
Normal file
@ -0,0 +1,28 @@
|
||||
info:
|
||||
name: logout all
|
||||
type: http
|
||||
seq: 7
|
||||
|
||||
http:
|
||||
method: POST
|
||||
url: "{{base_url}}/api/v1/protected/auth/logout-all"
|
||||
auth:
|
||||
type: bearer
|
||||
token: "{{access_token}}"
|
||||
|
||||
runtime:
|
||||
scripts:
|
||||
- type: after-response
|
||||
code: |-
|
||||
const status = res.getStatus();
|
||||
if (status === 200 || status === 204) {
|
||||
bru.setEnvVar("access_token", "");
|
||||
bru.setEnvVar("refresh_token", "");
|
||||
console.log("logout_all - all sessions revoked, tokens cleared");
|
||||
}
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
30
http_client/refresh.yml
Normal file
30
http_client/refresh.yml
Normal file
@ -0,0 +1,30 @@
|
||||
info:
|
||||
name: refresh
|
||||
type: http
|
||||
seq: 5
|
||||
|
||||
http:
|
||||
method: POST
|
||||
url: "{{base_url}}/api/v1/auth/refresh"
|
||||
auth: inherit
|
||||
|
||||
runtime:
|
||||
scripts:
|
||||
- type: after-response
|
||||
code: |-
|
||||
const response = res.getBody();
|
||||
const token = response.access_token;
|
||||
bru.setEnvVar("access_token", token);
|
||||
console.log("refresh - access_token:", token);
|
||||
|
||||
const cookies = res.getHeaders()['set-cookie'];
|
||||
if (cookies) {
|
||||
bru.setEnvVar("refresh_token", cookies[0]);
|
||||
console.log("refresh - refresh_token:", cookies[0]);
|
||||
}
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
@ -10,8 +10,8 @@ http:
|
||||
type: json
|
||||
data: |
|
||||
{
|
||||
"email": "d3@v.it",
|
||||
"password": "password"
|
||||
"email": "a@a.it",
|
||||
"password": "Password1!6969_"
|
||||
}
|
||||
auth: inherit
|
||||
|
||||
@ -19,16 +19,16 @@ runtime:
|
||||
scripts:
|
||||
- type: after-response
|
||||
code: |-
|
||||
// Post-response script on your login endpoint
|
||||
const response = res.getBody();
|
||||
const token = response.access_token;
|
||||
// Save to collection variables
|
||||
bru.setEnvVar("access_token", token);
|
||||
console.log("access_token", token)
|
||||
console.log("register - access_token:", token);
|
||||
|
||||
const cookies = res.getHeaders()['set-cookie'];
|
||||
bru.setEnvVar("refresh_token", cookies[0]);
|
||||
console.log("refresh_token", cookies[0])
|
||||
if (cookies) {
|
||||
bru.setEnvVar("refresh_token", cookies[0]);
|
||||
console.log("register - refresh_token:", cookies[0]);
|
||||
}
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
|
||||
17
http_client/test protected routes.yml
Normal file
17
http_client/test protected routes.yml
Normal file
@ -0,0 +1,17 @@
|
||||
info:
|
||||
name: test protected routes
|
||||
type: http
|
||||
seq: 4
|
||||
|
||||
http:
|
||||
method: GET
|
||||
url: "{{base_url}}/api/v1/protected/ping"
|
||||
auth:
|
||||
type: bearer
|
||||
token: "{{access_token}}"
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
25
src/controller/middleware/auth_middleware.rs
Normal file
25
src/controller/middleware/auth_middleware.rs
Normal file
@ -0,0 +1,25 @@
|
||||
use crate::{errors::AppError, state::AppState, utils::jwt::verify_access_token};
|
||||
use axum::{
|
||||
extract::{Request, State},
|
||||
middleware::Next,
|
||||
response::Response,
|
||||
};
|
||||
pub async fn auth_middleware(
|
||||
State(state): State<AppState>,
|
||||
mut request: Request,
|
||||
next: Next,
|
||||
) -> Result<Response, AppError> {
|
||||
let auth_header = request
|
||||
.headers()
|
||||
.get(axum::http::header::AUTHORIZATION)
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.ok_or(AppError::Unauthorized)?;
|
||||
if !auth_header.starts_with("Bearer ") {
|
||||
return Err(AppError::Unauthorized);
|
||||
}
|
||||
let token = &auth_header[7..];
|
||||
let claims = verify_access_token(token, &state.jwt_secret)?;
|
||||
// Inject the user ID into extensions for downstream handlers
|
||||
request.extensions_mut().insert(claims.sub);
|
||||
Ok(next.run(request).await)
|
||||
}
|
||||
@ -1,2 +1,3 @@
|
||||
pub mod anti_enumeration_middleware;
|
||||
pub mod auth_middleware;
|
||||
pub mod rate_limiting_middleware;
|
||||
|
||||
@ -17,7 +17,6 @@ pub fn auth_router(state: AppState) -> Router<AppState> {
|
||||
.route("/login", post(login_handler))
|
||||
.route("/register", post(register_handler))
|
||||
.route("/refresh", post(refresh_handler))
|
||||
.route("/logout", post(logout_handler))
|
||||
.layer(from_fn(random_delay_middleware))
|
||||
.layer(from_fn_with_state(state.clone(), rate_limiting_middleware))
|
||||
.layer(CookieManagerLayer::new())
|
||||
@ -44,7 +43,3 @@ async fn refresh_handler(
|
||||
) -> Result<Json<AuthResponse>, AppError> {
|
||||
refresh(&s, cookies).await
|
||||
}
|
||||
|
||||
async fn logout_handler(State(s): State<AppState>, cookies: Cookies) -> Result<(), AppError> {
|
||||
crate::service::auth_service::logout(&s, cookies).await
|
||||
}
|
||||
|
||||
@ -1,10 +1,24 @@
|
||||
use axum::Router;
|
||||
use axum::{
|
||||
Router,
|
||||
middleware::{from_fn, from_fn_with_state},
|
||||
};
|
||||
|
||||
use crate::state::AppState;
|
||||
use crate::{
|
||||
controller::{middleware::auth_middleware::auth_middleware, v1::protected::protected_router},
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
pub mod auth_controller;
|
||||
mod auth_controller;
|
||||
mod protected;
|
||||
|
||||
pub fn router_v1(state: AppState) -> Router<AppState> {
|
||||
Router::new().nest("/auth", auth_controller::auth_router(state))
|
||||
}
|
||||
let public_routes = Router::new().nest("/auth", auth_controller::auth_router(state.clone()));
|
||||
|
||||
let protected_routes = Router::new().nest(
|
||||
"/protected",
|
||||
protected::protected_router(state.clone())
|
||||
.layer(from_fn_with_state(state, auth_middleware)),
|
||||
);
|
||||
|
||||
Router::new().merge(public_routes).merge(protected_routes)
|
||||
}
|
||||
|
||||
27
src/controller/v1/protected/auth_protected_controller.rs
Normal file
27
src/controller/v1/protected/auth_protected_controller.rs
Normal file
@ -0,0 +1,27 @@
|
||||
use axum::{Extension, Router, extract::State, routing::post};
|
||||
use tower_cookies::{CookieManagerLayer, Cookies};
|
||||
|
||||
use crate::{errors::AppError, service::auth_service, state::AppState};
|
||||
|
||||
pub fn router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/logout", post(logout_handler))
|
||||
.route("/logout-all", post(logout_all_handler))
|
||||
.layer(CookieManagerLayer::new())
|
||||
}
|
||||
|
||||
async fn logout_handler(
|
||||
State(s): State<AppState>,
|
||||
cookies: Cookies,
|
||||
Extension(user_id): Extension<uuid::Uuid>,
|
||||
) -> Result<(), AppError> {
|
||||
auth_service::logout(&s, cookies, user_id).await
|
||||
}
|
||||
|
||||
async fn logout_all_handler(
|
||||
State(s): State<AppState>,
|
||||
cookies: Cookies,
|
||||
Extension(user_id): Extension<uuid::Uuid>,
|
||||
) -> Result<(), AppError> {
|
||||
auth_service::logout_all(&s, cookies, user_id).await
|
||||
}
|
||||
11
src/controller/v1/protected/mod.rs
Normal file
11
src/controller/v1/protected/mod.rs
Normal file
@ -0,0 +1,11 @@
|
||||
use axum::Router;
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
mod auth_protected_controller;
|
||||
|
||||
pub fn protected_router(_state: AppState) -> Router<AppState> {
|
||||
Router::new()
|
||||
.nest("/auth", auth_protected_controller::router())
|
||||
.route("/ping", axum::routing::get("pong"))
|
||||
}
|
||||
@ -57,3 +57,20 @@ where
|
||||
.map_err(AppError::from)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn revoke_all_for_user<'e, E>(
|
||||
executor: E,
|
||||
user_id: Uuid,
|
||||
) -> Result<(), AppError>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres>,
|
||||
{
|
||||
sqlx::query!(
|
||||
"update refresh_tokens set revoked_at = now() where user_id = $1 and revoked_at is null",
|
||||
user_id
|
||||
)
|
||||
.execute(executor)
|
||||
.await
|
||||
.map_err(AppError::from)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -25,6 +25,8 @@ pub enum AppError {
|
||||
Validation(String),
|
||||
#[error("Internal server error")]
|
||||
Internal,
|
||||
#[error("Request not authorized")]
|
||||
Unauthorized,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
@ -50,6 +52,7 @@ impl IntoResponse for AppError {
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Internal server error".to_string(),
|
||||
),
|
||||
AppError::Unauthorized => (StatusCode::UNAUTHORIZED, self.to_string()),
|
||||
};
|
||||
(status, Json(serde_json::json!({ "error": message }))).into_response()
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ use crate::errors::AppError;
|
||||
use crate::state::AppState;
|
||||
use crate::utils::hash;
|
||||
use crate::utils::jwt::generate_access_token;
|
||||
use crate::utils::refresh_token::generate_refresh_token;
|
||||
use crate::utils::refresh_token::{generate_refresh_token, hash_refresh_token};
|
||||
|
||||
pub async fn login(
|
||||
state: &AppState,
|
||||
@ -86,8 +86,9 @@ pub async fn refresh(state: &AppState, cookies: Cookies) -> Result<Json<AuthResp
|
||||
let refresh_token = get_refresh_cookie(&cookies).ok_or(AppError::InvalidCredentials)?;
|
||||
|
||||
let mut tx = state.db.begin().await?;
|
||||
let hash = hash_refresh_token(&refresh_token);
|
||||
|
||||
let token_data = find_by_hash(&mut *tx, &refresh_token)
|
||||
let token_data = find_by_hash(&mut *tx, &hash)
|
||||
.await?
|
||||
.ok_or(AppError::InvalidCredentials)?;
|
||||
|
||||
@ -110,7 +111,9 @@ pub async fn refresh(state: &AppState, cookies: Cookies) -> Result<Json<AuthResp
|
||||
Ok(Json(AuthResponse { access_token }))
|
||||
}
|
||||
|
||||
pub async fn logout(state: &AppState, cookies: Cookies) -> Result<(), AppError> {
|
||||
use crate::db::repository::refresh_token_repository::revoke_all_for_user;
|
||||
|
||||
pub async fn logout(state: &AppState, cookies: Cookies, _user_id: uuid::Uuid) -> Result<(), AppError> {
|
||||
let refresh_token = match get_refresh_cookie(&cookies) {
|
||||
Some(t) => t,
|
||||
None => return Ok(()), // Already logged out
|
||||
@ -118,7 +121,9 @@ pub async fn logout(state: &AppState, cookies: Cookies) -> Result<(), AppError>
|
||||
|
||||
let mut tx = state.db.begin().await?;
|
||||
|
||||
if let Some(token_data) = find_by_hash(&mut *tx, &refresh_token).await? {
|
||||
let hash = hash_refresh_token(&refresh_token);
|
||||
|
||||
if let Some(token_data) = find_by_hash(&mut *tx, &hash).await? {
|
||||
revoke(&mut *tx, token_data.id).await?;
|
||||
}
|
||||
|
||||
@ -129,6 +134,18 @@ pub async fn logout(state: &AppState, cookies: Cookies) -> Result<(), AppError>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn logout_all(state: &AppState, cookies: Cookies, user_id: uuid::Uuid) -> Result<(), AppError> {
|
||||
let mut tx = state.db.begin().await?;
|
||||
|
||||
revoke_all_for_user(&mut *tx, user_id).await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
remove_refresh_cookie(&cookies);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const REFRESH_COOKIE_NAME: &str = "refresh_token";
|
||||
|
||||
fn remove_refresh_cookie(cookies: &Cookies) {
|
||||
|
||||
@ -31,3 +31,15 @@ pub fn generate_access_token(user_id: Uuid, secret: &str) -> Result<String, AppE
|
||||
)
|
||||
.map_err(|_| AppError::Internal)
|
||||
}
|
||||
|
||||
pub fn verify_access_token(token: &str, secret: &str) -> Result<Claims, AppError> {
|
||||
let mut validation = jsonwebtoken::Validation::default();
|
||||
validation.validate_exp = true; // Ensure expired tokens are rejected
|
||||
jsonwebtoken::decode::<Claims>(
|
||||
token,
|
||||
&jsonwebtoken::DecodingKey::from_secret(secret.as_bytes()),
|
||||
&validation,
|
||||
)
|
||||
.map(|data| data.claims)
|
||||
.map_err(|_| AppError::Unauthorized) // Map any JWT error to 401
|
||||
}
|
||||
|
||||
@ -7,12 +7,14 @@ pub fn generate_refresh_token() -> (String, String) {
|
||||
thread_rng.fill_bytes(&mut bytes);
|
||||
|
||||
let plain = hex::encode(bytes); // 64 hex chars for user
|
||||
let hash = {
|
||||
// SHA-256 for DB storage
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&plain);
|
||||
hex::encode(hasher.finalize())
|
||||
};
|
||||
let hash = hash_refresh_token(&plain);
|
||||
|
||||
(plain, hash)
|
||||
}
|
||||
|
||||
pub fn hash_refresh_token(plain: &str) -> String {
|
||||
// SHA-256 for DB storage
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&plain);
|
||||
hex::encode(hasher.finalize())
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user