Vapor Auth

認証ライブラリSwiftVaporサーバーサイドミドルウェアJWTセッション認証認可

認証ライブラリ

Vapor Auth

概要

Vapor Authは、SwiftのVaporフレームワークに内蔵された認証・認可システムです。Basic認証、Bearer認証、セッション認証、JWT認証など多様な認証方式をサポートし、ミドルウェアベースの設計により柔軟で拡張可能なセキュリティ機能を提供します。Vapor 4から統合されており、追加の依存関係なしで利用可能です。

詳細

Vapor Authは、Authenticatorプロトコルを中心とした設計で、リクエストから認証情報を抽出してユーザーを識別します。ミドルウェアとして動作し、認証の成功時にはreq.authにユーザー情報を格納します。Guard Middlewareと組み合わせることで、認証が必須のルートを保護できます。

主要コンポーネント

  • Authenticator: 認証方式を定義するミドルウェアプロトコル
  • BasicAuthenticator: Basic認証のヘルパー
  • BearerAuthenticator: Bearer認証のヘルパー
  • CredentialsAuthenticator: リクエストボディベース認証
  • Guard Middleware: 認証チェックの強制実行

認証方式

  • Basic Authentication: ユーザー名とパスワードによる認証
  • Bearer Authentication: トークンベース認証(JWT、APIトークン)
  • Session Authentication: セッションミドルウェアとの連携
  • Credentials Authentication: カスタムクレデンシャル認証
  • JWT Authentication: JSON Web Token認証

認可機能

  • ロールベースアクセス制御: ユーザーロールによる認可
  • カスタム認可ロジック: 独自の認可ルール実装
  • ルートレベル保護: 特定ルートの認証要求
  • グループレベル保護: ルートグループの一括認証

メリット・デメリット

メリット

  • 内蔵システム: Vapor 4に統合、追加依存なし
  • 型安全: Swiftの型システムを活用した安全な実装
  • ミドルウェア設計: 柔軟で構成可能なアーキテクチャ
  • 多様な認証方式: 豊富な認証オプション
  • 非同期対応: async/awaitでの最新Swift機能サポート
  • 拡張性: カスタム認証方式の容易な実装

デメリット

  • Swift/Vapor専用: Vaporフレームワーク限定
  • 学習コスト: Server-side Swiftの知識が必要
  • エコシステム: Server-side Swiftエコシステムが限定的
  • 設定複雑さ: 高度な認証設定には専門知識が必要

参考ページ

書き方の例

基本的なユーザーモデル

import Vapor

struct User: Authenticatable {
    var id: UUID?
    var name: String
    var email: String
    var passwordHash: String
}

// データベース対応の場合
import Fluent

final class User: Model, Content, Authenticatable {
    static let schema = "users"
    
    @ID(key: .id)
    var id: UUID?
    
    @Field(key: "name")
    var name: String
    
    @Field(key: "email")
    var email: String
    
    @Field(key: "password_hash")
    var passwordHash: String
    
    init() { }
    
    init(id: UUID? = nil, name: String, email: String, passwordHash: String) {
        self.id = id
        self.name = name
        self.email = email
        self.passwordHash = passwordHash
    }
}

Basic認証の実装

import Vapor

struct UserAuthenticator: BasicAuthenticator {
    typealias User = App.User

    func authenticate(
        basic: BasicAuthorization,
        for request: Request
    ) -> EventLoopFuture<Void> {
        // ユーザー名とパスワードの検証
        if basic.username == "admin" && basic.password == "secret" {
            let user = User(name: "Admin User", email: "[email protected]")
            request.auth.login(user)
        }
        return request.eventLoop.makeSucceededFuture(())
    }
}

// async/await版
struct AsyncUserAuthenticator: AsyncBasicAuthenticator {
    typealias User = App.User

    func authenticate(
        basic: BasicAuthorization,
        for request: Request
    ) async throws {
        if basic.username == "admin" && basic.password == "secret" {
            let user = User(name: "Admin User", email: "[email protected]")
            request.auth.login(user)
        }
    }
}

Bearer認証の実装

import Vapor

struct TokenAuthenticator: BearerAuthenticator {
    typealias User = App.User

    func authenticate(
        bearer: BearerAuthorization,
        for request: Request
    ) -> EventLoopFuture<Void> {
        // トークンの検証
        if bearer.token == "valid-api-token" {
            let user = User(name: "API User", email: "[email protected]")
            request.auth.login(user)
        }
        return request.eventLoop.makeSucceededFuture(())
    }
}

// async/await版
struct AsyncTokenAuthenticator: AsyncBearerAuthenticator {
    func authenticate(
        bearer: BearerAuthorization,
        for request: Request
    ) async throws {
        if bearer.token == "valid-api-token" {
            let user = User(name: "API User", email: "[email protected]")
            request.auth.login(user)
        }
    }
}

ルート保護の設定

import Vapor

func routes(_ app: Application) throws {
    // 認証不要のルート
    app.get("public") { req in
        return "This is public"
    }
    
    // Basic認証で保護されたルート
    let basicProtected = app.grouped(UserAuthenticator())
        .grouped(User.guardMiddleware())
    
    basicProtected.get("protected") { req -> String in
        let user = try req.auth.require(User.self)
        return "Hello, \(user.name)!"
    }
    
    // Bearer認証で保護されたAPI
    let bearerProtected = app.grouped(TokenAuthenticator())
        .grouped(User.guardMiddleware())
    
    bearerProtected.get("api", "data") { req -> String in
        let user = try req.auth.require(User.self)
        return "API data for \(user.name)"
    }
}

複数認証方式の組み合わせ

import Vapor

func routes(_ app: Application) throws {
    // 複数の認証方式を組み合わせ
    let multiAuth = app.grouped([
        UserAuthenticator(),      // Basic認証
        TokenAuthenticator(),     // Bearer認証
        User.guardMiddleware()    // どちらかの認証が必須
    ])
    
    multiAuth.get("secure") { req -> String in
        let user = try req.auth.require(User.self)
        return "Authenticated as \(user.name)"
    }
}

セッション認証の実装

import Vapor

// セッション認証の設定
func configure(_ app: Application) throws {
    // セッションミドルウェアの設定
    app.middleware.use(app.sessions.middleware)
    
    // セッション認証子の登録
    app.middleware.use(User.sessionAuthenticator())
}

// セッション認証用のユーザー拡張
extension User: SessionAuthenticatable {
    var sessionID: UUID {
        return self.id!
    }
}

// ログイン処理
func loginHandler(_ req: Request) throws -> EventLoopFuture<Response> {
    let credentials = try req.content.decode(UserCredentials.self)
    
    return User.authenticate(credentials: credentials, on: req.db)
        .map { user in
            if let user = user {
                req.auth.login(user)
                return req.redirect(to: "/dashboard")
            } else {
                return req.redirect(to: "/login?error=invalid")
            }
        }
}

// ログアウト処理
func logoutHandler(_ req: Request) -> Response {
    req.auth.logout(User.self)
    return req.redirect(to: "/")
}

JWT認証の実装

import Vapor
import JWT

// JWTペイロード
struct UserPayload: JWTPayload {
    let userID: UUID
    let exp: ExpirationClaim
    
    func verify(using signer: JWTSigner) throws {
        try self.exp.verifyNotExpired()
    }
}

// JWT認証器
struct JWTAuthenticator: JWTAuthenticator {
    typealias Payload = UserPayload
    
    func authenticate(jwt: UserPayload, for request: Request) -> EventLoopFuture<Void> {
        return User.find(jwt.userID, on: request.db).map { user in
            if let user = user {
                request.auth.login(user)
            }
        }
    }
}

// JWT保護ルートの設定
func routes(_ app: Application) throws {
    let jwtProtected = app.grouped(JWTAuthenticator())
        .grouped(User.guardMiddleware())
    
    jwtProtected.get("profile") { req -> EventLoopFuture<User> in
        let user = try req.auth.require(User.self)
        return req.eventLoop.makeSucceededFuture(user)
    }
}

カスタム認証ミドルウェア

import Vapor

struct AdminMiddleware: Middleware {
    func respond(to request: Request, chainingTo next: Responder) -> EventLoopFuture<Response> {
        guard let user = request.auth.get(User.self),
              user.isAdmin else {
            return request.eventLoop.makeFailedFuture(Abort(.unauthorized))
        }
        return next.respond(to: request)
    }
}

// async/await版
struct AsyncAdminMiddleware: AsyncMiddleware {
    func respond(to request: Request, chainingTo next: AsyncResponder) async throws -> Response {
        guard let user = request.auth.get(User.self),
              user.isAdmin else {
            throw Abort(.unauthorized)
        }
        return try await next.respond(to: request)
    }
}

// 使用例
let adminRoutes = app.grouped(User.sessionAuthenticator())
    .grouped(User.guardMiddleware())
    .grouped(AdminMiddleware())

adminRoutes.get("admin", "dashboard") { req in
    return "Admin Dashboard"
}

エラーハンドリング

import Vapor

// 認証エラーの処理
func protectedRoute(_ req: Request) throws -> String {
    do {
        let user = try req.auth.require(User.self)
        return "Hello, \(user.name)!"
    } catch {
        throw Abort(.unauthorized, reason: "Authentication required")
    }
}

// カスタムエラーレスポンス
struct AuthenticationError: AbortError {
    let status: HTTPResponseStatus = .unauthorized
    let reason = "Invalid authentication credentials"
}