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
|
name: test
|
||||||
variables:
|
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
|
- name: base_url
|
||||||
value: http://localhost:6969
|
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
|
type: json
|
||||||
data: |
|
data: |
|
||||||
{
|
{
|
||||||
"email": "d3@v.it",
|
"email": "a@a.it",
|
||||||
"password": "password"
|
"password": "Password1!6969_"
|
||||||
}
|
}
|
||||||
auth: inherit
|
auth: inherit
|
||||||
|
|
||||||
@ -19,16 +19,16 @@ runtime:
|
|||||||
scripts:
|
scripts:
|
||||||
- type: after-response
|
- type: after-response
|
||||||
code: |-
|
code: |-
|
||||||
// Post-response script on your login endpoint
|
|
||||||
const response = res.getBody();
|
const response = res.getBody();
|
||||||
const token = response.access_token;
|
const token = response.access_token;
|
||||||
// Save to collection variables
|
|
||||||
bru.setEnvVar("access_token", token);
|
bru.setEnvVar("access_token", token);
|
||||||
console.log("access_token", token)
|
console.log("register - access_token:", token);
|
||||||
|
|
||||||
const cookies = res.getHeaders()['set-cookie'];
|
const cookies = res.getHeaders()['set-cookie'];
|
||||||
bru.setEnvVar("refresh_token", cookies[0]);
|
if (cookies) {
|
||||||
console.log("refresh_token", cookies[0])
|
bru.setEnvVar("refresh_token", cookies[0]);
|
||||||
|
console.log("register - refresh_token:", cookies[0]);
|
||||||
|
}
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
encodeUrl: true
|
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 anti_enumeration_middleware;
|
||||||
|
pub mod auth_middleware;
|
||||||
pub mod rate_limiting_middleware;
|
pub mod rate_limiting_middleware;
|
||||||
|
|||||||
@ -17,7 +17,6 @@ pub fn auth_router(state: AppState) -> Router<AppState> {
|
|||||||
.route("/login", post(login_handler))
|
.route("/login", post(login_handler))
|
||||||
.route("/register", post(register_handler))
|
.route("/register", post(register_handler))
|
||||||
.route("/refresh", post(refresh_handler))
|
.route("/refresh", post(refresh_handler))
|
||||||
.route("/logout", post(logout_handler))
|
|
||||||
.layer(from_fn(random_delay_middleware))
|
.layer(from_fn(random_delay_middleware))
|
||||||
.layer(from_fn_with_state(state.clone(), rate_limiting_middleware))
|
.layer(from_fn_with_state(state.clone(), rate_limiting_middleware))
|
||||||
.layer(CookieManagerLayer::new())
|
.layer(CookieManagerLayer::new())
|
||||||
@ -44,7 +43,3 @@ async fn refresh_handler(
|
|||||||
) -> Result<Json<AuthResponse>, AppError> {
|
) -> Result<Json<AuthResponse>, AppError> {
|
||||||
refresh(&s, cookies).await
|
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> {
|
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)?;
|
.map_err(AppError::from)?;
|
||||||
Ok(())
|
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),
|
Validation(String),
|
||||||
#[error("Internal server error")]
|
#[error("Internal server error")]
|
||||||
Internal,
|
Internal,
|
||||||
|
#[error("Request not authorized")]
|
||||||
|
Unauthorized,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
@ -50,6 +52,7 @@ impl IntoResponse for AppError {
|
|||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
"Internal server error".to_string(),
|
"Internal server error".to_string(),
|
||||||
),
|
),
|
||||||
|
AppError::Unauthorized => (StatusCode::UNAUTHORIZED, self.to_string()),
|
||||||
};
|
};
|
||||||
(status, Json(serde_json::json!({ "error": message }))).into_response()
|
(status, Json(serde_json::json!({ "error": message }))).into_response()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ use crate::errors::AppError;
|
|||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
use crate::utils::hash;
|
use crate::utils::hash;
|
||||||
use crate::utils::jwt::generate_access_token;
|
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(
|
pub async fn login(
|
||||||
state: &AppState,
|
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 refresh_token = get_refresh_cookie(&cookies).ok_or(AppError::InvalidCredentials)?;
|
||||||
|
|
||||||
let mut tx = state.db.begin().await?;
|
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?
|
.await?
|
||||||
.ok_or(AppError::InvalidCredentials)?;
|
.ok_or(AppError::InvalidCredentials)?;
|
||||||
|
|
||||||
@ -110,7 +111,9 @@ pub async fn refresh(state: &AppState, cookies: Cookies) -> Result<Json<AuthResp
|
|||||||
Ok(Json(AuthResponse { access_token }))
|
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) {
|
let refresh_token = match get_refresh_cookie(&cookies) {
|
||||||
Some(t) => t,
|
Some(t) => t,
|
||||||
None => return Ok(()), // Already logged out
|
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?;
|
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?;
|
revoke(&mut *tx, token_data.id).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,6 +134,18 @@ pub async fn logout(state: &AppState, cookies: Cookies) -> Result<(), AppError>
|
|||||||
Ok(())
|
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";
|
const REFRESH_COOKIE_NAME: &str = "refresh_token";
|
||||||
|
|
||||||
fn remove_refresh_cookie(cookies: &Cookies) {
|
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)
|
.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);
|
thread_rng.fill_bytes(&mut bytes);
|
||||||
|
|
||||||
let plain = hex::encode(bytes); // 64 hex chars for user
|
let plain = hex::encode(bytes); // 64 hex chars for user
|
||||||
let hash = {
|
let hash = hash_refresh_token(&plain);
|
||||||
// SHA-256 for DB storage
|
|
||||||
let mut hasher = Sha256::new();
|
|
||||||
hasher.update(&plain);
|
|
||||||
hex::encode(hasher.finalize())
|
|
||||||
};
|
|
||||||
|
|
||||||
(plain, hash)
|
(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