actix-identity

認証ライブラリRustActix Webセッション管理Web開発ミドルウェア

認証ライブラリ

actix-identity

概要

actix-identityは、Actix Webフレームワーク専用に設計されたRust製の認証・セッション管理ライブラリです。高性能なWebアプリケーションでのユーザー認証、セッション管理、アイデンティティ管理を効率的に行うためのミドルウェアとして機能します。Cookieベースの認証やセッション永続化をサポートし、Rustの安全性とActix Webの高性能を活かしたセキュアな認証システムを構築できます。

詳細

actix-identityの主な特徴:

  • Actix Web統合: Actix Webフレームワークとのネイティブ統合
  • Cookieベース認証: セキュアなCookie管理による認証機能
  • セッション管理: ユーザーセッションの作成、維持、削除
  • ミドルウェア設計: Actix Webのミドルウェアとして透過的に動作
  • 設定可能なポリシー: Cookie有効期限、セキュリティ設定のカスタマイズ
  • 非同期サポート: Rustの非同期処理に完全対応
  • 型安全性: Rustの型システムによる安全な認証処理
  • 高性能: ゼロコスト抽象化による最適化されたパフォーマンス

メリット・デメリット

メリット

  • Actix Webとの完璧な統合により開発効率が向上
  • Rustの安全性機能によりメモリ安全な認証システム
  • 高性能でスケーラブルなWebアプリケーション構築
  • シンプルで直感的なAPI設計
  • 設定可能なセキュリティポリシー
  • 非同期処理による高いスループット

デメリット

  • Actix Web専用のため他のフレームワークでは使用不可
  • Rustの学習コストが必要
  • エコシステムがまだ発展途上
  • 複雑な認証フローには追加実装が必要

参考ページ

書き方の例

基本セットアップ

use actix_web::{web, App, HttpServer, middleware, HttpResponse, Result};
use actix_identity::{Identity, CookieIdentityPolicy, IdentityService};

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .wrap(IdentityService::new(
                CookieIdentityPolicy::new(&[0; 32])    // 32-byte key
                    .name("auth-cookie")
                    .secure(false)  // HTTPSの場合はtrue
                    .http_only(true)
                    .max_age_time(chrono::Duration::days(1))
            ))
            .wrap(middleware::Logger::default())
            .service(web::resource("/login").route(web::post().to(login)))
            .service(web::resource("/logout").route(web::post().to(logout)))
            .service(web::resource("/profile").route(web::get().to(profile)))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

実装例

use actix_web::{HttpResponse, HttpRequest, Result};
use actix_identity::Identity;
use serde::Deserialize;

#[derive(Deserialize)]
struct LoginForm {
    username: String,
    password: String,
}

async fn login(
    id: Identity,
    form: web::Json<LoginForm>
) -> Result<HttpResponse> {
    // パスワード検証ロジック(実際の実装では適切なハッシュ化が必要)
    if authenticate_user(&form.username, &form.password).await {
        id.remember(form.username.clone());
        Ok(HttpResponse::Ok().json("Login successful"))
    } else {
        Ok(HttpResponse::Unauthorized().json("Invalid credentials"))
    }
}

async fn logout(id: Identity) -> Result<HttpResponse> {
    id.forget();
    Ok(HttpResponse::Ok().json("Logout successful"))
}

async fn profile(id: Identity) -> Result<HttpResponse> {
    if let Some(user_id) = id.identity() {
        Ok(HttpResponse::Ok().json(format!("User: {}", user_id)))
    } else {
        Ok(HttpResponse::Unauthorized().json("Not authenticated"))
    }
}

async fn authenticate_user(username: &str, password: &str) -> bool {
    // 実際の認証ロジックを実装
    // データベース照会、パスワードハッシュ検証など
    username == "admin" && password == "password"
}

高度な機能

use actix_identity::{CookieIdentityPolicy, IdentityService};
use actix_web::cookie::SameSite;

fn create_identity_service() -> IdentityService<CookieIdentityPolicy> {
    IdentityService::new(
        CookieIdentityPolicy::new(&generate_secret_key())
            .name("secure-auth")
            .secure(true)  // HTTPS必須
            .http_only(true)
            .same_site(SameSite::Strict)
            .max_age_time(chrono::Duration::hours(24))
            .domain("example.com")
            .path("/")
            .login_deadline(chrono::Duration::minutes(30))
    )
}

fn generate_secret_key() -> [u8; 32] {
    // 実際の実装では安全な鍵生成を使用
    use rand::RngCore;
    let mut key = [0u8; 32];
    rand::thread_rng().fill_bytes(&mut key);
    key
}

// ミドルウェアで認証状態を確認
async fn protected_endpoint(id: Identity) -> Result<HttpResponse> {
    match id.identity() {
        Some(user_id) => {
            // 認証済みユーザー処理
            Ok(HttpResponse::Ok().json(format!("Authorized user: {}", user_id)))
        },
        None => {
            Ok(HttpResponse::Unauthorized().json("Authentication required"))
        }
    }
}

セキュリティ

use actix_web::{HttpRequest, HttpResponse, Result};
use actix_identity::Identity;
use std::collections::HashMap;

// CSRF保護
async fn login_with_csrf(
    req: HttpRequest,
    id: Identity,
    form: web::Json<LoginForm>
) -> Result<HttpResponse> {
    // CSRFトークン検証
    if !verify_csrf_token(&req, &form.csrf_token) {
        return Ok(HttpResponse::Forbidden().json("Invalid CSRF token"));
    }

    if authenticate_user(&form.username, &form.password).await {
        id.remember(form.username.clone());
        Ok(HttpResponse::Ok().json("Login successful"))
    } else {
        Ok(HttpResponse::Unauthorized().json("Invalid credentials"))
    }
}

fn verify_csrf_token(req: &HttpRequest, token: &str) -> bool {
    // CSRF検証ロジック
    if let Some(session_token) = req.headers().get("X-CSRF-Token") {
        session_token.to_str().unwrap_or("") == token
    } else {
        false
    }
}

// セッション管理
async fn refresh_session(id: Identity) -> Result<HttpResponse> {
    if let Some(user_id) = id.identity() {
        // セッション更新
        id.remember(user_id.clone());
        Ok(HttpResponse::Ok().json("Session refreshed"))
    } else {
        Ok(HttpResponse::Unauthorized().json("No active session"))
    }
}

統合

use actix_web::{web, App, HttpServer, middleware};
use actix_identity::{Identity, IdentityService, CookieIdentityPolicy};
use sqlx::PgPool;

// データベース統合
#[derive(Clone)]
struct AppState {
    db: PgPool,
}

async fn login_with_db(
    data: web::Data<AppState>,
    id: Identity,
    form: web::Json<LoginForm>
) -> Result<HttpResponse> {
    // データベースからユーザー情報を取得
    let user = sqlx::query!(
        "SELECT id, password_hash FROM users WHERE username = $1",
        form.username
    )
    .fetch_optional(&data.db)
    .await
    .map_err(|_| actix_web::error::ErrorInternalServerError("Database error"))?;

    if let Some(user) = user {
        if verify_password(&form.password, &user.password_hash) {
            id.remember(user.id.to_string());
            Ok(HttpResponse::Ok().json("Login successful"))
        } else {
            Ok(HttpResponse::Unauthorized().json("Invalid credentials"))
        }
    } else {
        Ok(HttpResponse::Unauthorized().json("User not found"))
    }
}

fn verify_password(password: &str, hash: &str) -> bool {
    // パスワードハッシュ検証(bcrypt等を使用)
    use bcrypt::verify;
    verify(password, hash).unwrap_or(false)
}

// アプリケーション設定
async fn create_app() -> std::io::Result<()> {
    let database_url = std::env::var("DATABASE_URL")
        .expect("DATABASE_URL must be set");
    let pool = PgPool::connect(&database_url).await
        .expect("Failed to connect to database");

    HttpServer::new(move || {
        App::new()
            .app_data(web::Data::new(AppState { db: pool.clone() }))
            .wrap(create_identity_service())
            .wrap(middleware::Logger::default())
            .configure(configure_routes)
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

fn configure_routes(cfg: &mut web::ServiceConfig) {
    cfg.service(
        web::scope("/auth")
            .route("/login", web::post().to(login_with_db))
            .route("/logout", web::post().to(logout))
            .route("/profile", web::get().to(profile))
    );
}

エラーハンドリング

use actix_web::{HttpResponse, Result, error::ResponseError};
use actix_identity::Identity;
use thiserror::Error;

#[derive(Error, Debug)]
enum AuthError {
    #[error("Invalid credentials")]
    InvalidCredentials,
    #[error("Session expired")]
    SessionExpired,
    #[error("Access denied")]
    AccessDenied,
    #[error("Database error: {0}")]
    DatabaseError(String),
}

impl ResponseError for AuthError {
    fn error_response(&self) -> HttpResponse {
        match self {
            AuthError::InvalidCredentials => {
                HttpResponse::Unauthorized().json("Invalid credentials")
            },
            AuthError::SessionExpired => {
                HttpResponse::Unauthorized().json("Session expired")
            },
            AuthError::AccessDenied => {
                HttpResponse::Forbidden().json("Access denied")
            },
            AuthError::DatabaseError(_) => {
                HttpResponse::InternalServerError().json("Internal server error")
            },
        }
    }
}

async fn secure_endpoint(id: Identity) -> Result<HttpResponse, AuthError> {
    let user_id = id.identity()
        .ok_or(AuthError::SessionExpired)?;

    // ユーザー権限確認
    if !check_user_permissions(&user_id).await {
        return Err(AuthError::AccessDenied);
    }

    Ok(HttpResponse::Ok().json("Access granted"))
}

async fn check_user_permissions(user_id: &str) -> bool {
    // 権限確認ロジック
    true
}