axum-casbin-auth

This library implements the Service, Layer traits of Tower for Authorisation Service using Casbin. Please read Casbin for all supported models and features. The basic idea is to have a set of Casbin Policies and the service expects a struct with Subject: String in the request. The service then matches with the policies defined for casbin and tries to authorise the subject. If failed, the layer rejects with UNAUTHORISED response. You can find many supported Authorisation models here. Below is a basic usage of the library for RESTful model.

Installation

Install my-project with npm

Cargo.toml

```toml [dependencies]

Other deps

axum-casbin-auth = "1.0.0" ```

Code usage

jwt.rs

```rust use std::str::FromStr;

use crate::constants::JWTSECRET; use asynctrait::asynctrait; use axum::extract::{FromRequest, RequestParts, TypedHeader}; use axum::headers::{authorization::Bearer, Authorization}; use axum::response::IntoResponse; use axum::{http::StatusCode, Json}; use axumcasbinauth::CasbinAuthClaims; use chrono::{Duration, Utc}; use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation}; use serde::{Deserialize, Serialize}; use serdejson::json;

type Result = std::result::Result;

[derive(Debug, Deserialize, Serialize, Clone)]

pub struct Claims { pub subject: String, pub exp: i64, pub iat: i64, pub user_id: i32, }

impl Claims { pub fn new(email: String, userid: i32, role: String) -> Self { let iat = Utc::now(); let exp = iat + Duration::hours(24); Self { subject: email, iat: iat.timestamp(), exp: exp.timestamp(), userid, role: ROLES::from_str(&role).unwrap(), } } }

pub fn sign(email: String, userid: i32, role: String) -> ErrorResult { Ok(jsonwebtoken::encode( &Header::default(), &Claims::new(email, userid, role), &EncodingKey::fromsecret(JWTSECRET.as_bytes()), )?) }

pub fn verify(token: &str) -> ErrorResult { Ok(jsonwebtoken::decode( token, &DecodingKey::fromsecret(JWTSECRET.as_bytes()), &Validation::default(), ) .map(|data| data.claims)?) }

[derive(Debug)]

pub enum AuthError { WrongCredentials, MissingCredentials, TokenCreation, InvalidToken, }

[async_trait]

impl FromRequest for Claims where B: Send, { type Rejection = AuthError;

async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
    let TypedHeader(Authorization(bearer)) =
        TypedHeader::<Authorization<Bearer>>::from_request(req)
            .await
            .map_err(|_| AuthError::InvalidToken)?;
    // Decode the user data
    let token_data = verify(bearer.token()).map_err(|_| AuthError::InvalidToken)?;
    req.extensions_mut().insert(token_data.clone());
    // THIS IS WHERE WE SATISFY THE CONTRACT FOR CASBIN_AUTH_MIDDLEWARE
    req.extensions_mut()
        .insert(CasbinAuthClaims::new(token_data.clone().subject));
    Ok(token_data)
}

}

impl IntoResponse for AuthError { fn intoresponse(self) -> axum::response::Response { let (status, errormessage) = match self { AuthError::WrongCredentials => (StatusCode::UNAUTHORIZED, "Wrong credentials"), AuthError::MissingCredentials => (StatusCode::BADREQUEST, "Missing credentials"), AuthError::TokenCreation => (StatusCode::INTERNALSERVERERROR, "Token creation error"), AuthError::InvalidToken => (StatusCode::BADREQUEST, "Invalid token"), }; let body = Json(json!({ "error": errormessage, })); (status, body).intoresponse() } } ```

Create a folder called casbin (or any name) inside your project root and create two files.

  1. model.conf

```conf [request_definition] r = sub, obj, act

[policy_definition] p = sub, obj, act

[policy_effect] e = some(where (p.eft == allow))

[matchers] m = r.sub == p.sub && keyMatch(r.obj, p.obj) && regexMatch(r.act, p.act) ```

  1. policy.csv

csv p, admin@test.com, *, POST

Please read Casbin documentation for detailed explanations

main.rs

```rust use axumcasbinauth::{ casbin::{CoreApi, Enforcer}, CasbinAuthLayer, };

use lib::utils::jwt::Claims; // ... server code

let e = Enforcer::new("casbin/model.conf", "casbin/policy.csv").await?; let casbinauthenforcer = Arc::new(RwLock::new(e));

let app = Router::new() .route("/inventory", post(addproduct::handle)) // Order is important .layer(CasbinAuthLayer::new(casbinauthenforcer)) .layer(fromextractor::());

// ... ```

After this any request will be authenticated by the Claims extractor and will inject the CasbinAuthClaims with subject in the request post which will be passed through the CasbinAuthLayer and the request will be authorised based on the policies. The policies can be dynamically stored and maintained in databases as well. You can find more Adapters here