- Auth handlers: login/refresh/logout + user CRUD with tenant isolation - JWT middleware: Bearer token validation → TenantContext injection - RBAC helpers: require_permission, require_any_permission, require_role - AuthModule: implements ErpModule with public/protected route split - AuthState: FromRef pattern avoids circular deps between erp-auth and erp-server - Server: public routes (health+login+refresh) + protected routes (JWT middleware) - ErpModule trait: added as_any() for downcast support - Workspace: added async-trait, sha2 dependencies
65 lines
2.0 KiB
Rust
65 lines
2.0 KiB
Rust
use axum::body::Body;
|
|
use axum::http::Request;
|
|
use axum::middleware::Next;
|
|
use axum::response::Response;
|
|
use erp_core::error::AppError;
|
|
use erp_core::types::TenantContext;
|
|
|
|
use crate::service::token_service::TokenService;
|
|
|
|
/// JWT authentication middleware function.
|
|
///
|
|
/// Extracts the `Bearer` token from the `Authorization` header, validates it
|
|
/// using `TokenService::decode_token`, and injects a `TenantContext` into the
|
|
/// request extensions so downstream handlers can access tenant/user identity.
|
|
///
|
|
/// The `jwt_secret` parameter is passed explicitly by the server crate at
|
|
/// middleware construction time, avoiding any circular dependency between
|
|
/// erp-auth and erp-server.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns `AppError::Unauthorized` if:
|
|
/// - The `Authorization` header is missing
|
|
/// - The header value does not start with `"Bearer "`
|
|
/// - The token cannot be decoded or has expired
|
|
/// - The token type is not "access"
|
|
pub async fn jwt_auth_middleware_fn(
|
|
jwt_secret: String,
|
|
req: Request<Body>,
|
|
next: Next,
|
|
) -> Result<Response, AppError> {
|
|
let auth_header = req
|
|
.headers()
|
|
.get("Authorization")
|
|
.and_then(|v| v.to_str().ok())
|
|
.ok_or(AppError::Unauthorized)?;
|
|
|
|
let token = auth_header
|
|
.strip_prefix("Bearer ")
|
|
.ok_or(AppError::Unauthorized)?;
|
|
|
|
let claims = TokenService::decode_token(token, &jwt_secret)
|
|
.map_err(|_| AppError::Unauthorized)?;
|
|
|
|
// Verify this is an access token, not a refresh token
|
|
if claims.token_type != "access" {
|
|
return Err(AppError::Unauthorized);
|
|
}
|
|
|
|
let ctx = TenantContext {
|
|
tenant_id: claims.tid,
|
|
user_id: claims.sub,
|
|
roles: claims.roles,
|
|
permissions: claims.permissions,
|
|
};
|
|
|
|
// Reconstruct the request with the TenantContext injected into extensions.
|
|
// We cannot borrow `req` mutably after reading headers, so we rebuild.
|
|
let (parts, body) = req.into_parts();
|
|
let mut req = Request::from_parts(parts, body);
|
|
req.extensions_mut().insert(ctx);
|
|
|
|
Ok(next.run(req).await)
|
|
}
|