feat(auth): add handlers, JWT middleware, RBAC, and module registration
- 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
This commit is contained in:
64
crates/erp-auth/src/middleware/jwt_auth.rs
Normal file
64
crates/erp-auth/src/middleware/jwt_auth.rs
Normal file
@@ -0,0 +1,64 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user