add custom authentification middleware
This commit is contained in:
parent
fef33dc2d4
commit
18bd54d3f0
7 changed files with 323 additions and 11 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,3 +1,5 @@
|
||||||
|
.vscode/
|
||||||
|
|
||||||
# ---> Rust
|
# ---> Rust
|
||||||
# Generated by Cargo
|
# Generated by Cargo
|
||||||
# will have compiled files and executables
|
# will have compiled files and executables
|
||||||
|
|
1
nixin_farm_ssr/Cargo.lock
generated
1
nixin_farm_ssr/Cargo.lock
generated
|
@ -2446,6 +2446,7 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
|
"axum-extra",
|
||||||
"chrono",
|
"chrono",
|
||||||
"fluent-templates",
|
"fluent-templates",
|
||||||
"include_dir",
|
"include_dir",
|
||||||
|
|
|
@ -29,6 +29,7 @@ sea-orm = { version = "1.1.0", features = [
|
||||||
] }
|
] }
|
||||||
|
|
||||||
axum = "0.7.7"
|
axum = "0.7.7"
|
||||||
|
axum-extra = { version = "0.9", features = ["cookie"] }
|
||||||
include_dir = "0.7"
|
include_dir = "0.7"
|
||||||
uuid = { version = "1.11.0", features = ["v4"] }
|
uuid = { version = "1.11.0", features = ["v4"] }
|
||||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json"] }
|
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json"] }
|
||||||
|
|
|
@ -13,26 +13,27 @@ use loco_rs::{
|
||||||
use crate::{
|
use crate::{
|
||||||
models::users,
|
models::users,
|
||||||
views,
|
views,
|
||||||
|
controllers::middleware::auth_no_error,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[debug_handler]
|
#[debug_handler]
|
||||||
pub async fn home(
|
pub async fn home(
|
||||||
//auth: auth::JWT,
|
auth: auth_no_error::JWTWithUserOpt<users::Model>,
|
||||||
ViewEngine(v): ViewEngine<TeraView>,
|
ViewEngine(v): ViewEngine<TeraView>,
|
||||||
State(ctx): State<AppContext>,
|
State(ctx): State<AppContext>,
|
||||||
) -> Result<Response> {
|
) -> Result<Response> {
|
||||||
|
println!("{:?}",auth);
|
||||||
|
|
||||||
//let current_user = users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await;
|
//let current_user = users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await;
|
||||||
|
|
||||||
// match current_user {
|
match auth.user {
|
||||||
// Ok(user) => {
|
Some(user) => {
|
||||||
// views::home::index(&v,&user)
|
views::home::index(&v,&user)
|
||||||
// }
|
}
|
||||||
// Err(_) => {
|
None => {
|
||||||
// views::home::login(&v)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
views::home::login(&v)
|
views::home::login(&v)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn routes() -> Routes {
|
pub fn routes() -> Routes {
|
||||||
|
|
304
nixin_farm_ssr/src/controllers/middleware/auth_no_error.rs
Normal file
304
nixin_farm_ssr/src/controllers/middleware/auth_no_error.rs
Normal file
|
@ -0,0 +1,304 @@
|
||||||
|
//! Axum middleware for validating token header
|
||||||
|
//!
|
||||||
|
//! # Example:
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! use loco_rs::prelude::*;
|
||||||
|
//! use serde::Serialize;
|
||||||
|
//! use axum::extract::State;
|
||||||
|
//!
|
||||||
|
//! #[derive(Serialize)]
|
||||||
|
//! pub struct TestResponse {
|
||||||
|
//! pub pid: String,
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! async fn current(
|
||||||
|
//! auth: auth::JWT,
|
||||||
|
//! State(ctx): State<AppContext>,
|
||||||
|
//! ) -> Result<Response> {
|
||||||
|
//! format::json(TestResponse{ pid: auth.claims.pid})
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use axum::{
|
||||||
|
extract::{FromRef, FromRequestParts, Query},
|
||||||
|
http::{request::Parts, HeaderMap},
|
||||||
|
};
|
||||||
|
use axum_extra::extract::cookie;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use loco_rs::{
|
||||||
|
app::AppContext, auth, config::JWT as JWTConfig, errors::Error, model::Authenticable,
|
||||||
|
Result as LocoResult,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------
|
||||||
|
//
|
||||||
|
// JWT Auth extractor
|
||||||
|
//
|
||||||
|
// ---------------------------------------
|
||||||
|
|
||||||
|
// Define constants for token prefix and authorization header
|
||||||
|
const TOKEN_PREFIX: &str = "Bearer ";
|
||||||
|
const AUTH_HEADER: &str = "authorization";
|
||||||
|
|
||||||
|
// Define a struct to represent user authentication information serialized
|
||||||
|
// to/from JSON
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct JWTWithUserOpt<T: Authenticable> {
|
||||||
|
pub claims: Option<auth::jwt::UserClaims>,
|
||||||
|
pub user: Option<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement the FromRequestParts trait for the Auth struct
|
||||||
|
#[async_trait]
|
||||||
|
impl<S, T> FromRequestParts<S> for JWTWithUserOpt<T>
|
||||||
|
where
|
||||||
|
AppContext: FromRef<S>,
|
||||||
|
S: Send + Sync,
|
||||||
|
T: Authenticable,
|
||||||
|
{
|
||||||
|
type Rejection = Error;
|
||||||
|
|
||||||
|
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Error> {
|
||||||
|
let ctx: AppContext = AppContext::from_ref(state);
|
||||||
|
|
||||||
|
match get_jwt_from_config(&ctx) {
|
||||||
|
Ok(jwt_config) => {
|
||||||
|
match extract_token(jwt_config, parts) {
|
||||||
|
Ok(token) => {
|
||||||
|
let jwt_secret = ctx.config.get_jwt_config()?;
|
||||||
|
match auth::jwt::JWT::new(&jwt_secret.secret).validate(&token) {
|
||||||
|
Ok(claims) => {
|
||||||
|
let user = T::find_by_claims_key(&ctx.db, &claims.claims.pid)
|
||||||
|
.await;
|
||||||
|
match user {
|
||||||
|
Ok(user) => {
|
||||||
|
Ok(Self {
|
||||||
|
claims: Some(claims.claims),
|
||||||
|
user: Some(user),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// ToDo: log error ?
|
||||||
|
Ok(Self {
|
||||||
|
claims: Some(claims.claims),
|
||||||
|
user: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_err) => {
|
||||||
|
// ToDo: log error ?
|
||||||
|
Ok(Self {
|
||||||
|
claims: None,
|
||||||
|
user: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// ToDo: log error ?
|
||||||
|
Ok(Self {
|
||||||
|
claims: None,
|
||||||
|
user: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// ToDo: log error ?
|
||||||
|
Ok(Self {
|
||||||
|
claims: None,
|
||||||
|
user: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define a struct to represent user authentication information serialized
|
||||||
|
// to/from JSON
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct JWTOpt {
|
||||||
|
pub claims: Option<auth::jwt::UserClaims>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement the FromRequestParts trait for the Auth struct
|
||||||
|
#[async_trait]
|
||||||
|
impl<S> FromRequestParts<S> for JWTOpt
|
||||||
|
where
|
||||||
|
AppContext: FromRef<S>,
|
||||||
|
S: Send + Sync,
|
||||||
|
{
|
||||||
|
type Rejection = Error;
|
||||||
|
|
||||||
|
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Error> {
|
||||||
|
let ctx: AppContext = AppContext::from_ref(state); // change to ctx
|
||||||
|
|
||||||
|
match get_jwt_from_config(&ctx) {
|
||||||
|
Ok(jwt_config) => {
|
||||||
|
match extract_token(jwt_config, parts) {
|
||||||
|
Ok(token) => {
|
||||||
|
let jwt_secret = ctx.config.get_jwt_config()?;
|
||||||
|
match auth::jwt::JWT::new(&jwt_secret.secret).validate(&token) {
|
||||||
|
Ok(claims) => {
|
||||||
|
Ok(Self {
|
||||||
|
claims: Some(claims.claims),
|
||||||
|
}) }
|
||||||
|
Err(_err) => {
|
||||||
|
// ToDo: log error ?
|
||||||
|
Ok(Self {
|
||||||
|
claims: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// ToDo: log error ?
|
||||||
|
Ok(Self {
|
||||||
|
claims: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// ToDo: log error ?
|
||||||
|
Ok(Self {
|
||||||
|
claims: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// extract JWT token from context configuration
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Return an error when JWT token not configured
|
||||||
|
fn get_jwt_from_config(ctx: &AppContext) -> LocoResult<&JWTConfig> {
|
||||||
|
ctx.config
|
||||||
|
.auth
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| Error::string("auth not configured"))?
|
||||||
|
.jwt
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| Error::string("JWT token not configured"))
|
||||||
|
}
|
||||||
|
/// extract token from the configured jwt location settings
|
||||||
|
fn extract_token(jwt_config: &JWTConfig, parts: &Parts) -> LocoResult<String> {
|
||||||
|
#[allow(clippy::match_wildcard_for_single_variants)]
|
||||||
|
match jwt_config
|
||||||
|
.location
|
||||||
|
.as_ref()
|
||||||
|
.unwrap_or(&loco_rs::config::JWTLocation::Bearer)
|
||||||
|
{
|
||||||
|
loco_rs::config::JWTLocation::Query { name } => extract_token_from_query(name, parts),
|
||||||
|
loco_rs::config::JWTLocation::Cookie { name } => extract_token_from_cookie(name, parts),
|
||||||
|
loco_rs::config::JWTLocation::Bearer => extract_token_from_header(&parts.headers)
|
||||||
|
.map_err(|e| Error::Unauthorized(e.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Function to extract a token from the authorization header
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// When token is not valid or out found
|
||||||
|
pub fn extract_token_from_header(headers: &HeaderMap) -> LocoResult<String> {
|
||||||
|
Ok(headers
|
||||||
|
.get(AUTH_HEADER)
|
||||||
|
.ok_or_else(|| Error::Unauthorized(format!("header {AUTH_HEADER} token not found")))?
|
||||||
|
.to_str()
|
||||||
|
.map_err(|err| Error::Unauthorized(err.to_string()))?
|
||||||
|
.strip_prefix(TOKEN_PREFIX)
|
||||||
|
.ok_or_else(|| Error::Unauthorized(format!("error strip {AUTH_HEADER} value")))?
|
||||||
|
.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract a token value from cookie
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// when token value from cookie is not found
|
||||||
|
pub fn extract_token_from_cookie(name: &str, parts: &Parts) -> LocoResult<String> {
|
||||||
|
// LogoResult
|
||||||
|
let jar: cookie::CookieJar = cookie::CookieJar::from_headers(&parts.headers);
|
||||||
|
Ok(jar
|
||||||
|
.get(name)
|
||||||
|
.ok_or(Error::Unauthorized("token is not found".to_string()))?
|
||||||
|
.to_string()
|
||||||
|
.strip_prefix(&format!("{name}="))
|
||||||
|
.ok_or_else(|| Error::Unauthorized("error strip value".to_string()))?
|
||||||
|
.to_string())
|
||||||
|
}
|
||||||
|
/// Extract a token value from query
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// when token value from cookie is not found
|
||||||
|
pub fn extract_token_from_query(name: &str, parts: &Parts) -> LocoResult<String> {
|
||||||
|
// LogoResult
|
||||||
|
let parameters: Query<HashMap<String, String>> =
|
||||||
|
Query::try_from_uri(&parts.uri).map_err(|err| Error::Unauthorized(err.to_string()))?;
|
||||||
|
parameters
|
||||||
|
.get(name)
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| Error::Unauthorized(format!("`{name}` query parameter not found")))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------
|
||||||
|
//
|
||||||
|
// API Token Auth / Extractor
|
||||||
|
//
|
||||||
|
// ---------------------------------------
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
// Represents the data structure for the API token.
|
||||||
|
pub struct ApiTokenOpt<T: Authenticable> {
|
||||||
|
pub user: Option<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
// Implementing the `FromRequestParts` trait for `ApiToken` to enable extracting
|
||||||
|
// it from the request.
|
||||||
|
impl<S, T> FromRequestParts<S> for ApiTokenOpt<T>
|
||||||
|
where
|
||||||
|
AppContext: FromRef<S>,
|
||||||
|
S: Send + Sync,
|
||||||
|
T: Authenticable,
|
||||||
|
{
|
||||||
|
type Rejection = Error;
|
||||||
|
|
||||||
|
// Extracts `ApiToken` from the request parts.
|
||||||
|
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Error> {
|
||||||
|
// Extract API key from the request header.
|
||||||
|
match extract_token_from_header(&parts.headers) {
|
||||||
|
Ok(api_key) => {
|
||||||
|
// Convert the state reference to the application context.
|
||||||
|
let state: AppContext = AppContext::from_ref(state);
|
||||||
|
|
||||||
|
// Retrieve user information based on the API key from the database.
|
||||||
|
match T::find_by_api_key(&state.db, &api_key).await {
|
||||||
|
Ok(user) => {
|
||||||
|
Ok(Self { user: Some(user) })
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
//ToDo: Log error?
|
||||||
|
Ok(Self { user: None })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
//ToDo: Log error?
|
||||||
|
Ok(Self { user: None })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
//ToDo: Tests
|
||||||
|
}
|
1
nixin_farm_ssr/src/controllers/middleware/mod.rs
Normal file
1
nixin_farm_ssr/src/controllers/middleware/mod.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub mod auth_no_error;
|
|
@ -1,6 +1,8 @@
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
|
||||||
|
pub mod middleware;
|
||||||
|
|
||||||
pub mod server;
|
pub mod server;
|
||||||
pub mod bundle;
|
pub mod bundle;
|
||||||
pub mod service;
|
pub mod service;
|
||||||
|
|
Loading…
Reference in a new issue