anzar-shared 0.9.15

Anzar is a lightweight authentication and authorization framework that runs as a separate microservice
Documentation
use crate::error::{AuthError, CoreError, Result, TokenErrorType};

use crate::domain::model::{Claims, IssuedTokens, RefreshToken, TokenType};
use crate::intern::session::SessionService;

impl SessionService {
    #[tracing::instrument(name = "auth.consume_refresh_token", skip(self, refresh_token))]
    pub async fn consume_refresh_token(&self, refresh_token: &str) -> Result<String> {
        let claims: Claims = self.crypto.jwt()?.decode(refresh_token)?;

        if claims.token_type != TokenType::RefreshToken {
            return Err(CoreError::Unauthenticated(AuthError::TokenInvalid {
                token_type: TokenErrorType::RefreshToken,
            }));
        }

        match self.jwt_repository.find_and_consume(&claims).await {
            Ok(_) => Ok(claims.sub),
            Err(CoreError::Unauthenticated { .. }) => {
                // Potential breach — revoke everything for this user
                // TODO: Send an email indicating a breach
                self.jwt_repository.revoke(&claims.sub).await?;
                Err(CoreError::Unauthenticated(AuthError::TokenReplay {
                    token_type: TokenErrorType::RefreshToken,
                }))
            }
            Err(e) => Err(e), // NotFound, Expired bubble up as-is
        }
    }

    #[tracing::instrument(
        name = "auth.issue_jwt", skip(self), fields(user.id = user_id)
    )]
    pub async fn issue_jwt(
        &self,
        user_id: &str,
        full_permissions: Vec<String>,
    ) -> Result<IssuedTokens> {
        let jwt_config = self.configuration.auth.jwt()?;
        let rbac_config = &self.configuration.auth.rbac;
        let jwt = self.crypto.jwt()?;

        let (access_claims, refresh_claims) = Claims::new(user_id, &rbac_config.default_role)
            .with_issuer(&jwt_config.issuer)
            .with_audience(&jwt_config.audience)
            .with_permissions(full_permissions)
            .into_token_pair(jwt_config);
        let jti = refresh_claims.jti;

        let refresh_token = RefreshToken::new(&jti.to_string())
            .with_user_id(user_id)
            .with_expire_at(jwt_config.refresh_token_expires_in);
        self.jwt_repository.insert(refresh_token).await?;

        let access = jwt.encode(access_claims)?;
        let refresh = jwt.encode(refresh_claims)?;
        Ok(IssuedTokens::default()
            .with_access_token(&access)
            .with_refresh_token(&refresh)
            .with_jti(jti))
    }

    #[tracing::instrument(name = "auth.invalidate_jwt", skip(self, refresh_token))]
    pub async fn invalidate_jwt(&self, refresh_token: &str) -> Result<()> {
        let claims: Claims = self.crypto.jwt()?.decode(refresh_token)?;

        if claims.token_type != TokenType::RefreshToken {
            return Err(CoreError::from(AuthError::TokenInvalid {
                token_type: TokenErrorType::RefreshToken,
            }));
        }

        self.jwt_repository.invalidate(claims.jti).await?;
        Ok(())
    }

    // async fn logout(&self, payload: AuthPayload) -> Result<()> {
    //     self.jwt_service.invalidate(&payload.jti).await?;
    //     self.session_service.revoke(&payload.user_id).await?;
    //     Ok(())
    // }
    #[tracing::instrument(name = "auth.logout_all", skip(self), fields(user.id = user_id))]
    pub async fn logout_all(&self, user_id: &str) -> Result<()> {
        self.jwt_repository.revoke(user_id).await?;
        self.session_repository.revoke(user_id).await?;
        Ok(())
    }

    #[tracing::instrument(name = "auth.find_jwt_by_jti", skip(self, jti))]
    pub async fn find_jwt_by_jti(&self, jti: &str) -> Result<RefreshToken> {
        self.jwt_repository.find_by_jti(jti).await
    }
}