コンテンツにスキップ

認証

認証は、ユーザーの身元を確認する行為です。これは、ユーザー名とパスワードまたは一意のトークンのような認証情報の検証を通じて行われます。認証(auth/cとも呼ばれる)は、以前に認証されたユーザーが特定のタスクを実行する権限を確認する行為である認可(auth/z)とは異なります。

はじめに

VaporのAuthentication APIは、BasicおよびBearerを使用して、Authorizationヘッダーを介したユーザー認証をサポートします。また、Content APIからデコードされたデータを介したユーザー認証もサポートしています。

認証は、検証ロジックを含むAuthenticatorを作成することで実装されます。オーセンティケータは、個々のルートグループまたはアプリ全体を保護するために使用できます。Vaporには以下のオーセンティケータヘルパーが付属しています:

プロトコル 説明
RequestAuthenticator/AsyncRequestAuthenticator ミドルウェアを作成できる基本オーセンティケータ。
BasicAuthenticator/AsyncBasicAuthenticator Basic認証ヘッダーを認証します。
BearerAuthenticator/AsyncBearerAuthenticator Bearer認証ヘッダーを認証します。
CredentialsAuthenticator/AsyncCredentialsAuthenticator リクエストボディから認証情報のペイロードを認証します。

認証が成功した場合、オーセンティケータは検証されたユーザーをreq.authに追加します。このユーザーは、オーセンティケータによって保護されているルートでreq.auth.get(_:)を使用してアクセスできます。認証が失敗した場合、ユーザーはreq.authに追加されず、アクセスしようとしても失敗します。

Authenticatable

Authentication APIを使用するには、まずAuthenticatableに準拠するユーザータイプが必要です。これはstructclass、またはFluentのModelでも構いません。以下の例では、nameという1つのプロパティを持つシンプルなUser構造体を想定しています。

import Vapor

struct User: Authenticatable {
    var name: String
}

以下の各例では、作成したオーセンティケータのインスタンスを使用します。これらの例では、UserAuthenticatorと呼んでいます。

ルート

オーセンティケータはミドルウェアであり、ルートの保護に使用できます。

let protected = app.grouped(UserAuthenticator())
protected.get("me") { req -> String in
    try req.auth.require(User.self).name
}

req.auth.requireは認証されたUserを取得するために使用されます。認証が失敗した場合、このメソッドはエラーをスローし、ルートを保護します。

ガードミドルウェア

ルートグループでGuardMiddlewareを使用して、ルートハンドラに到達する前にユーザーが認証されていることを確認することもできます。

let protected = app.grouped(UserAuthenticator())
    .grouped(User.guardMiddleware())

認証の要求は、オーセンティケータの構成を可能にするため、オーセンティケータミドルウェアによって行われません。構成の詳細については以下をお読みください。

Basic

Basic認証は、Authorizationヘッダーでユーザー名とパスワードを送信します。ユーザー名とパスワードはコロン(例:test:secret)で連結され、base-64エンコードされ、"Basic "でプレフィックスされます。次の例のリクエストは、ユーザー名testとパスワードsecretをエンコードしています。

GET /me HTTP/1.1
Authorization: Basic dGVzdDpzZWNyZXQ=

Basic認証は通常、ユーザーのログインとトークンの生成に一度だけ使用されます。これにより、ユーザーの機密パスワードを送信する頻度を最小限に抑えます。プレーンテキストまたは未検証のTLS接続でBasic認証を送信しないでください。

アプリでBasic認証を実装するには、BasicAuthenticatorに準拠する新しいオーセンティケータを作成します。以下は、上記のリクエストを検証するためにハードコードされた例のオーセンティケータです。

import Vapor

struct UserAuthenticator: BasicAuthenticator {
    typealias User = App.User

    func authenticate(
        basic: BasicAuthorization,
        for request: Request
    ) -> EventLoopFuture<Void> {
        if basic.username == "test" && basic.password == "secret" {
            request.auth.login(User(name: "Vapor"))
        }
        return request.eventLoop.makeSucceededFuture(())
   }
}

async/awaitを使用している場合は、代わりにAsyncBasicAuthenticatorを使用できます:

import Vapor

struct UserAuthenticator: AsyncBasicAuthenticator {
    typealias User = App.User

    func authenticate(
        basic: BasicAuthorization,
        for request: Request
    ) async throws {
        if basic.username == "test" && basic.password == "secret" {
            request.auth.login(User(name: "Vapor"))
        }
   }
}

このプロトコルでは、着信リクエストにAuthorization: Basic ...ヘッダーが含まれているときに呼び出されるauthenticate(basic:for:)を実装する必要があります。ユーザー名とパスワードを含むBasicAuthorization構造体がメソッドに渡されます。

このテストオーセンティケータでは、ユーザー名とパスワードはハードコードされた値と照合されます。実際のオーセンティケータでは、データベースや外部APIと照合する可能性があります。これがauthenticateメソッドがフューチャーを返すことができる理由です。

Tip

パスワードはプレーンテキストでデータベースに保存しないでください。比較には常にパスワードハッシュを使用してください。

認証パラメータが正しい場合(この場合はハードコードされた値と一致)、Vaporという名前のUserがログインされます。認証パラメータが一致しない場合、ユーザーはログインされず、認証が失敗したことを示します。

このオーセンティケータをアプリに追加し、上記で定義したルートをテストすると、ログインが成功した場合に名前"Vapor"が返されるはずです。認証情報が正しくない場合は、401 Unauthorizedエラーが表示されるはずです。

Bearer

Bearer認証は、Authorizationヘッダーでトークンを送信します。トークンには"Bearer "がプレフィックスされます。次の例のリクエストはトークンfooを送信しています。

GET /me HTTP/1.1
Authorization: Bearer foo

Bearer認証は、一般的にAPIエンドポイントの認証に使用されます。ユーザーは通常、ユーザー名とパスワードなどの認証情報をログインエンドポイントに送信してBearerトークンをリクエストします。このトークンは、アプリケーションのニーズに応じて数分から数日間有効です。

トークンが有効である限り、ユーザーは認証情報の代わりにそれを使用してAPIに対して認証できます。トークンが無効になった場合、ログインエンドポイントを使用して新しいトークンを生成できます。

アプリでBearer認証を実装するには、BearerAuthenticatorに準拠する新しいオーセンティケータを作成します。以下は、上記のリクエストを検証するためにハードコードされた例のオーセンティケータです。

import Vapor

struct UserAuthenticator: BearerAuthenticator {
    typealias User = App.User

    func authenticate(
        bearer: BearerAuthorization,
        for request: Request
    ) -> EventLoopFuture<Void> {
       if bearer.token == "foo" {
           request.auth.login(User(name: "Vapor"))
       }
       return request.eventLoop.makeSucceededFuture(())
   }
}

async/awaitを使用している場合は、代わりにAsyncBearerAuthenticatorを使用できます:

import Vapor

struct UserAuthenticator: AsyncBearerAuthenticator {
    typealias User = App.User

    func authenticate(
        bearer: BearerAuthorization,
        for request: Request
    ) async throws {
       if bearer.token == "foo" {
           request.auth.login(User(name: "Vapor"))
       }
   }
}

このプロトコルでは、着信リクエストにAuthorization: Bearer ...ヘッダーが含まれているときに呼び出されるauthenticate(bearer:for:)を実装する必要があります。トークンを含むBearerAuthorization構造体がメソッドに渡されます。

このテストオーセンティケータでは、トークンはハードコードされた値と照合されます。実際のオーセンティケータでは、データベースと照合したり、JWTで行われるような暗号化手段を使用してトークンを検証する可能性があります。これがauthenticateメソッドがフューチャーを返すことができる理由です。

Tip

トークン検証を実装する際は、水平方向のスケーラビリティを考慮することが重要です。アプリケーションが多くのユーザーを同時に処理する必要がある場合、認証が潜在的なボトルネックになる可能性があります。同時に実行される複数のアプリケーションインスタンスにわたって設計がどのようにスケールするかを検討してください。

認証パラメータが正しい場合(この場合はハードコードされた値と一致)、Vaporという名前のUserがログインされます。認証パラメータが一致しない場合、ユーザーはログインされず、認証が失敗したことを示します。

このオーセンティケータをアプリに追加し、上記で定義したルートをテストすると、ログインが成功した場合に名前"Vapor"が返されるはずです。認証情報が正しくない場合は、401 Unauthorizedエラーが表示されるはずです。

構成

複数のオーセンティケータを構成(組み合わせ)して、より複雑なエンドポイント認証を作成できます。オーセンティケータミドルウェアは認証が失敗してもリクエストを拒否しないため、これらのミドルウェアの複数を連鎖させることができます。オーセンティケータは2つの主要な方法で構成できます。

メソッドの構成

認証構成の最初の方法は、同じユーザータイプに対して複数のオーセンティケータを連鎖させることです。次の例を見てください:

app.grouped(UserPasswordAuthenticator())
    .grouped(UserTokenAuthenticator())
    .grouped(User.guardMiddleware())
    .post("login") 
{ req in
    let user = try req.auth.require(User.self)
    // ユーザーで何かを行う。
}

この例では、両方ともUserを認証する2つのオーセンティケータUserPasswordAuthenticatorUserTokenAuthenticatorを想定しています。これらのオーセンティケータの両方がルートグループに追加されます。最後に、Userが正常に認証されたことを要求するため、オーセンティケータの後にGuardMiddlewareが追加されます。

このオーセンティケータの構成により、パスワードまたはトークンのいずれかでアクセスできるルートが作成されます。このようなルートは、ユーザーがログインしてトークンを生成し、その後そのトークンを使用して新しいトークンを生成し続けることができます。

ユーザーの構成

認証構成の2番目の方法は、異なるユーザータイプのオーセンティケータを連鎖させることです。次の例を見てください:

app.grouped(AdminAuthenticator())
    .grouped(UserAuthenticator())
    .get("secure") 
{ req in
    guard req.auth.has(Admin.self) || req.auth.has(User.self) else {
        throw Abort(.unauthorized)
    }
    // 何かを行う。
}

この例では、それぞれAdminUserを認証する2つのオーセンティケータAdminAuthenticatorUserAuthenticatorを想定しています。これらのオーセンティケータの両方がルートグループに追加されます。GuardMiddlewareを使用する代わりに、ルートハンドラにAdminまたはUserのいずれかが認証されたかどうかを確認するチェックが追加されます。そうでない場合は、エラーがスローされます。

このオーセンティケータの構成により、潜在的に異なる認証方法を持つ2つの異なるタイプのユーザーがアクセスできるルートが作成されます。このようなルートは、通常のユーザー認証を許可しながら、スーパーユーザーへのアクセスも提供できます。

手動

req.authを使用して認証を手動で処理することもできます。これは特にテストに便利です。

ユーザーを手動でログインするには、req.auth.login(_:)を使用します。任意のAuthenticatableユーザーをこのメソッドに渡すことができます。

req.auth.login(User(name: "Vapor"))

認証されたユーザーを取得するには、req.auth.require(_:)を使用します

let user: User = try req.auth.require(User.self)
print(user.name) // String

認証が失敗したときに自動的にエラーをスローしたくない場合は、req.auth.get(_:)を使用することもできます。

let user = req.auth.get(User.self)
print(user?.name) // String?

ユーザーの認証を解除するには、ユーザータイプをreq.auth.logout(_:)に渡します。

req.auth.logout(User.self)

Fluent

Fluentは、既存のモデルに追加できるModelAuthenticatableModelTokenAuthenticatableの2つのプロトコルを定義しています。モデルをこれらのプロトコルに準拠させることで、エンドポイントを保護するためのオーセンティケータを作成できます。

ModelTokenAuthenticatableはBearerトークンで認証します。これは、ほとんどのエンドポイントを保護するために使用するものです。ModelAuthenticatableはユーザー名とパスワードで認証し、トークンを生成するための単一のエンドポイントで使用されます。

このガイドは、Fluentに精通しており、データベースを使用するようにアプリを正常に設定していることを前提としています。Fluentを初めて使用する場合は、概要から始めてください。

ユーザー

開始するには、認証されるユーザーを表すモデルが必要です。このガイドでは、次のモデルを使用しますが、既存のモデルを自由に使用できます。

import Fluent
import Vapor

final class User: Model, Content {
    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
    }
}

モデルは、この場合はメールアドレスであるユーザー名とパスワードハッシュを保存できる必要があります。また、重複ユーザーを避けるために、emailを一意のフィールドに設定します。この例のモデルに対応するマイグレーションは次のとおりです:

import Fluent
import Vapor

extension User {
    struct Migration: AsyncMigration {
        var name: String { "CreateUser" }

        func prepare(on database: Database) async throws {
            try await database.schema("users")
                .id()
                .field("name", .string, .required)
                .field("email", .string, .required)
                .field("password_hash", .string, .required)
                .unique(on: "email")
                .create()
        }

        func revert(on database: Database) async throws {
            try await database.schema("users").delete()
        }
    }
}

app.migrationsにマイグレーションを追加することを忘れないでください。

app.migrations.add(User.Migration())

Tip

メールアドレスは大文字小文字を区別しないため、データベースに保存する前にメールアドレスを小文字に強制するMiddlewareを追加することをお勧めします。ただし、ModelAuthenticatableは大文字小文字を区別する比較を使用するため、これを行う場合は、クライアントでの大文字小文字の強制、またはカスタムオーセンティケータを使用して、ユーザーの入力がすべて小文字であることを確認する必要があります。

最初に必要なのは、新しいユーザーを作成するためのエンドポイントです。POST /usersを使用しましょう。このエンドポイントが期待するデータを表すContent構造体を作成します。

import Vapor

extension User {
    struct Create: Content {
        var name: String
        var email: String
        var password: String
        var confirmPassword: String
    }
}

必要に応じて、この構造体をValidatableに準拠させて、検証要件を追加できます。

import Vapor

extension User.Create: Validatable {
    static func validations(_ validations: inout Validations) {
        validations.add("name", as: String.self, is: !.empty)
        validations.add("email", as: String.self, is: .email)
        validations.add("password", as: String.self, is: .count(8...))
    }
}

これでPOST /usersエンドポイントを作成できます。

app.post("users") { req async throws -> User in
    try User.Create.validate(content: req)
    let create = try req.content.decode(User.Create.self)
    guard create.password == create.confirmPassword else {
        throw Abort(.badRequest, reason: "Passwords did not match")
    }
    let user = try User(
        name: create.name,
        email: create.email,
        passwordHash: Bcrypt.hash(create.password)
    )
    try await user.save(on: req.db)
    return user
}

このエンドポイントは、着信リクエストを検証し、User.Create構造体をデコードし、パスワードが一致することを確認します。次に、デコードされたデータを使用して新しいUserを作成し、データベースに保存します。プレーンテキストのパスワードは、データベースに保存する前にBcryptを使用してハッシュされます。

プロジェクトをビルドして実行し、最初にデータベースをマイグレートしてから、次のリクエストを使用して新しいユーザーを作成します。

POST /users HTTP/1.1
Content-Length: 97
Content-Type: application/json

{
    "name": "Vapor",
    "email": "test@vapor.codes",
    "password": "secret42",
    "confirmPassword": "secret42"
}

Model Authenticatable

これで、ユーザーモデルと新しいユーザーを作成するためのエンドポイントができたので、モデルをModelAuthenticatableに準拠させましょう。これにより、モデルをユーザー名とパスワードを使用して認証できるようになります。

import Fluent
import Vapor

extension User: ModelAuthenticatable {
    static let usernameKey = \User.$email
    static let passwordHashKey = \User.$passwordHash

    func verify(password: String) throws -> Bool {
        try Bcrypt.verify(password, created: self.passwordHash)
    }
}

この拡張はUserModelAuthenticatable準拠を追加します。最初の2つのプロパティは、ユーザー名とパスワードハッシュをそれぞれ保存するために使用するフィールドを指定します。\記法は、Fluentがそれらにアクセスするために使用できるフィールドへのキーパスを作成します。

最後の要件は、Basic認証ヘッダーで送信されたプレーンテキストパスワードを検証するメソッドです。サインアップ中にパスワードをハッシュするためにBcryptを使用しているため、Bcryptを使用して、提供されたパスワードが保存されたパスワードハッシュと一致することを検証します。

UserModelAuthenticatableに準拠したので、ログインルートを保護するためのオーセンティケータを作成できます。

let passwordProtected = app.grouped(User.authenticator())
passwordProtected.post("login") { req -> User in
    try req.auth.require(User.self)
}

ModelAuthenticatableは、オーセンティケータを作成するための静的メソッドauthenticatorを追加します。

次のリクエストを送信して、このルートが機能することをテストします。

POST /login HTTP/1.1
Authorization: Basic dGVzdEB2YXBvci5jb2RlczpzZWNyZXQ0Mg==

このリクエストは、Basic認証ヘッダーを介してユーザー名test@vapor.codesとパスワードsecret42を渡します。以前に作成したユーザーが返されるはずです。

理論的にはBasic認証を使用してすべてのエンドポイントを保護できますが、代わりに別のトークンを使用することをお勧めします。これにより、ユーザーの機密パスワードをインターネット経由で送信する頻度が最小限に抑えられます。また、パスワードハッシュはログイン中にのみ実行する必要があるため、認証がはるかに高速になります。

ユーザートークン

ユーザートークンを表す新しいモデルを作成します。

import Fluent
import Vapor

final class UserToken: Model, Content {
    static let schema = "user_tokens"

    @ID(key: .id)
    var id: UUID?

    @Field(key: "value")
    var value: String

    @Parent(key: "user_id")
    var user: User

    init() { }

    init(id: UUID? = nil, value: String, userID: User.IDValue) {
        self.id = id
        self.value = value
        self.$user.id = userID
    }
}

このモデルには、トークンの一意の文字列を保存するためのvalueフィールドが必要です。また、ユーザーモデルへの親関係も必要です。有効期限などの追加のプロパティを必要に応じてこのトークンに追加できます。

次に、このモデルのマイグレーションを作成します。

import Fluent

extension UserToken {
    struct Migration: AsyncMigration {
        var name: String { "CreateUserToken" }

        func prepare(on database: Database) async throws {
            try await database.schema("user_tokens")
                .id()
                .field("value", .string, .required)
                .field("user_id", .uuid, .required, .references("users", "id"))
                .unique(on: "value")
                .create()
        }

        func revert(on database: Database) async throws {
            try await database.schema("user_tokens").delete()
        }
    }
}

このマイグレーションはvalueフィールドを一意にすることに注意してください。また、user_idフィールドとusersテーブル間の外部キー参照も作成します。

app.migrationsにマイグレーションを追加することを忘れないでください。

app.migrations.add(UserToken.Migration())

最後に、新しいトークンを生成するメソッドをUserに追加します。このメソッドはログイン中に使用されます。

extension User {
    func generateToken() throws -> UserToken {
        try .init(
            value: [UInt8].random(count: 16).base64, 
            userID: self.requireID()
        )
    }
}

ここでは、[UInt8].random(count:)を使用してランダムなトークン値を生成しています。この例では、16バイト(128ビット)のランダムデータを使用しています。必要に応じてこの数値を調整できます。ランダムデータは、HTTPヘッダーで簡単に送信できるようにbase-64エンコードされます。

ユーザートークンを生成できるようになったので、POST /loginルートを更新してトークンを作成して返すようにします。

let passwordProtected = app.grouped(User.authenticator())
passwordProtected.post("login") { req async throws -> UserToken in
    let user = try req.auth.require(User.self)
    let token = try user.generateToken()
    try await token.save(on: req.db)
    return token
}

上記と同じログインリクエストを使用して、このルートが機能することをテストします。ログイン時に次のようなトークンを取得するはずです:

8gtg300Jwdhc/Ffw784EXA==

後で使用するので、取得したトークンを保持してください。

Model Token Authenticatable

UserTokenModelTokenAuthenticatableに準拠させます。これにより、トークンがUserモデルを認証できるようになります。

import Vapor
import Fluent

extension UserToken: ModelTokenAuthenticatable {
    static var valueKey: KeyPath<UserToken, Field<String>> { \.$value }
    static var userKey: KeyPath<UserToken, Parent<User>> { \.$user }

    var isValid: Bool {
        true
    }
}

最初のプロトコル要件は、トークンの一意の値を保存するフィールドを指定します。これは、Bearer認証ヘッダーで送信される値です。2番目の要件は、Userモデルへの親関係を指定します。これは、Fluentが認証されたユーザーを検索する方法です。

最後の要件はisValidブール値です。これがfalseの場合、トークンはデータベースから削除され、ユーザーは認証されません。簡単にするために、これをtrueにハードコードしてトークンを永続的にします。

トークンがModelTokenAuthenticatableに準拠したので、ルートを保護するためのオーセンティケータを作成できます。

現在認証されているユーザーを取得するための新しいエンドポイントGET /meを作成します。

let tokenProtected = app.grouped(UserToken.authenticator())
tokenProtected.get("me") { req -> User in
    try req.auth.require(User.self)
}

Userと同様に、UserTokenにはオーセンティケータを生成できる静的なauthenticator()メソッドがあります。オーセンティケータは、Bearer認証ヘッダーで提供された値を使用して一致するUserTokenを見つけようとします。一致するものが見つかった場合、関連するUserを取得して認証します。

POST /loginリクエストから保存した値をトークンとして使用して、次のHTTPリクエストを送信してこのルートが機能することをテストします。

GET /me HTTP/1.1
Authorization: Bearer <token>

認証されたUserが返されるはずです。

セッション

VaporのSession APIを使用して、リクエスト間でユーザー認証を自動的に永続化できます。これは、ログインに成功した後、ユーザーの一意の識別子をリクエストのセッションデータに保存することで機能します。後続のリクエストでは、ユーザーの識別子がセッションから取得され、ルートハンドラを呼び出す前にユーザーを認証するために使用されます。

セッションは、Webブラウザに直接HTMLを提供するVaporで構築されたフロントエンドWebアプリケーションに最適です。APIの場合、リクエスト間でユーザーデータを永続化するために、ステートレスなトークンベースの認証を使用することをお勧めします。

Session Authenticatable

セッションベースの認証を使用するには、SessionAuthenticatableに準拠するタイプが必要です。この例では、シンプルな構造体を使用します。

import Vapor

struct User {
    var email: String
}

SessionAuthenticatableに準拠するには、sessionIDを指定する必要があります。これは、セッションデータに保存される値であり、ユーザーを一意に識別する必要があります。

extension User: SessionAuthenticatable {
    var sessionID: String {
        self.email
    }
}

シンプルなUserタイプの場合、セッションの一意の識別子としてメールアドレスを使用します。

セッションオーセンティケータ

次に、永続化されたセッション識別子からUserのインスタンスを解決する処理を行うSessionAuthenticatorが必要です。

struct UserSessionAuthenticator: SessionAuthenticator {
    typealias User = App.User
    func authenticate(sessionID: String, for request: Request) -> EventLoopFuture<Void> {
        let user = User(email: sessionID)
        request.auth.login(user)
        return request.eventLoop.makeSucceededFuture(())
    }
}

async/awaitを使用している場合は、AsyncSessionAuthenticatorを使用できます:

struct UserSessionAuthenticator: AsyncSessionAuthenticator {
    typealias User = App.User
    func authenticate(sessionID: String, for request: Request) async throws {
        let user = User(email: sessionID)
        request.auth.login(user)
    }
}

例のUserを初期化するために必要なすべての情報がセッション識別子に含まれているため、ユーザーを同期的に作成してログインできます。実際のアプリケーションでは、セッション識別子を使用してデータベース検索やAPIリクエストを実行し、認証する前に残りのユーザーデータを取得する可能性があります。

次に、初期認証を実行するためのシンプルなベアラーオーセンティケータを作成しましょう。

struct UserBearerAuthenticator: AsyncBearerAuthenticator {
    func authenticate(bearer: BearerAuthorization, for request: Request) async throws {
        if bearer.token == "test" {
            let user = User(email: "hello@vapor.codes")
            request.auth.login(user)
        }
    }
}

このオーセンティケータは、ベアラートークンtestが送信されたときに、メールhello@vapor.codesを持つユーザーを認証します。

最後に、これらのすべての部分をアプリケーションで組み合わせましょう。

// ユーザー認証を必要とする保護されたルートグループを作成します。
let protected = app.routes.grouped([
    app.sessions.middleware,
    UserSessionAuthenticator(),
    UserBearerAuthenticator(),
    User.guardMiddleware(),
])

// ユーザーのメールを読み取るためのGET /meルートを追加します。
protected.get("me") { req -> String in
    try req.auth.require(User.self).email
}

SessionsMiddlewareが最初に追加され、アプリケーションでセッションサポートが有効になります。セッションの設定に関する詳細は、Session APIセクションにあります。

次に、SessionAuthenticatorが追加されます。これは、セッションがアクティブな場合にユーザーを認証する処理を行います。

認証がまだセッションに永続化されていない場合、リクエストは次のオーセンティケータに転送されます。UserBearerAuthenticatorはベアラートークンをチェックし、それが"test"と等しい場合にユーザーを認証します。

最後に、User.guardMiddleware()は、Userが前のミドルウェアのいずれかによって認証されたことを確認します。ユーザーが認証されていない場合、エラーがスローされます。

このルートをテストするには、まず次のリクエストを送信します:

GET /me HTTP/1.1
authorization: Bearer test

これにより、UserBearerAuthenticatorがユーザーを認証します。認証されると、UserSessionAuthenticatorはユーザーの識別子をセッションストレージに永続化し、クッキーを生成します。レスポンスからのクッキーを使用して、ルートへの2番目のリクエストを行います。

GET /me HTTP/1.1
cookie: vapor_session=123

今回は、UserSessionAuthenticatorがユーザーを認証し、再びユーザーのメールが返されるはずです。

Model Session Authenticatable

Fluentモデルは、ModelSessionAuthenticatableに準拠することでSessionAuthenticatorを生成できます。これは、モデルの一意の識別子をセッション識別子として使用し、セッションからモデルを復元するためのデータベース検索を自動的に実行します。

import Fluent

final class User: Model { ... }

// このモデルをセッションに永続化できるようにします。
extension User: ModelSessionAuthenticatable { }

ModelSessionAuthenticatableを既存のモデルに空の準拠として追加できます。追加されると、そのモデルのSessionAuthenticatorを作成するための新しい静的メソッドが利用可能になります。

User.sessionAuthenticator()

これは、ユーザーを解決するためにアプリケーションのデフォルトデータベースを使用します。データベースを指定するには、識別子を渡します。

User.sessionAuthenticator(.sqlite)

ウェブサイト認証

ウェブサイトは、ブラウザの使用により認証情報をブラウザに添付する方法が制限されるため、認証の特殊なケースです。これにより、2つの異なる認証シナリオが発生します:

  • フォームを介した初回ログイン
  • セッションクッキーで認証される後続の呼び出し

VaporとFluentは、これをシームレスにするためのいくつかのヘルパーを提供します。

セッション認証

セッション認証は上記で説明したとおりに機能します。ユーザーがアクセスするすべてのルートにセッションミドルウェアとセッションオーセンティケータを適用する必要があります。これには、保護されたルート、ログインしている場合にユーザーにアクセスしたいパブリックルート(アカウントボタンを表示するためなど)、およびログインルートが含まれます。

configure.swiftでアプリにグローバルに有効にできます:

app.middleware.use(app.sessions.middleware)
app.middleware.use(User.sessionAuthenticator())

これらのミドルウェアは次のことを行います:

  • セッションミドルウェアは、リクエストで提供されたセッションクッキーを取得し、セッションに変換します
  • セッションオーセンティケータはセッションを取得し、そのセッションに認証されたユーザーがいるかどうかを確認します。いる場合、ミドルウェアはリクエストを認証します。レスポンスでは、セッションオーセンティケータはリクエストに認証されたユーザーがいるかどうかを確認し、次のリクエストで認証されるようにセッションに保存します。

Note

セッションクッキーはデフォルトでsecurehttpOnlyに設定されていません。クッキーの設定方法の詳細については、VaporのSession APIを確認してください。

ルートの保護

APIのルートを保護する場合、従来はリクエストが認証されていない場合に401 UnauthorizedなどのステータスコードでHTTPレスポンスを返します。しかし、これはブラウザを使用している人にとって良いユーザーエクスペリエンスではありません。Vaporは、このシナリオで使用する任意のAuthenticatableタイプ用のRedirectMiddlewareを提供します:

let protectedRoutes = app.grouped(User.redirectMiddleware(path: "/login?loginRequired=true"))

RedirectMiddlewareオブジェクトは、高度なURL処理のために作成時にリダイレクトパスをStringとして返すクロージャを渡すこともサポートしています。例えば、状態管理のためにリダイレクト元のパスをリダイレクト先のクエリパラメータとして含めることができます。

let redirectMiddleware = User.redirectMiddleware { req -> String in
  return "/login?authRequired=true&next=\(req.url.path)"
}

これはGuardMiddlewareと同様に機能します。protectedRoutesに登録されたルートへの認証されていないリクエストは、提供されたパスにリダイレクトされます。これにより、単に401 Unauthorizedを提供するのではなく、ユーザーにログインするよう指示できます。

RedirectMiddlewareを実行する前に認証されたユーザーが読み込まれるように、RedirectMiddlewareの前にセッションオーセンティケータを含めてください。

let protectedRoutes = app.grouped([User.sessionAuthenticator(), redirectMiddleware])

フォームログイン

ユーザーとセッションでの将来のリクエストを認証するには、ユーザーをログインする必要があります。Vaporは、フォームを介したログインを処理するModelCredentialsAuthenticatableプロトコルを提供します。まず、Userをこのプロトコルに準拠させます:

extension User: ModelCredentialsAuthenticatable {
    static let usernameKey = \User.$email
    static let passwordHashKey = \User.$password

    func verify(password: String) throws -> Bool {
        try Bcrypt.verify(password, created: self.password)
    }
}

これはModelAuthenticatableと同じであり、すでにそれに準拠している場合は何もする必要はありません。次に、このModelCredentialsAuthenticatorミドルウェアをログインフォームのPOSTリクエストに適用します:

let credentialsProtectedRoute = sessionRoutes.grouped(User.credentialsAuthenticator())
credentialsProtectedRoute.post("login", use: loginPostHandler)

これは、ログインルートを保護するためにデフォルトの認証情報オーセンティケータを使用します。POSTリクエストでusernamepasswordを送信する必要があります。次のようにフォームを設定できます:

 <form method="POST" action="/login">
    <label for="username">Username</label>
    <input type="text" id="username" placeholder="Username" name="username" autocomplete="username" required autofocus>
    <label for="password">Password</label>
    <input type="password" id="password" placeholder="Password" name="password" autocomplete="current-password" required>
    <input type="submit" value="Sign In">    
</form>

CredentialsAuthenticatorは、リクエストボディからusernamepasswordを抽出し、ユーザー名からユーザーを見つけ、パスワードを検証します。パスワードが有効な場合、ミドルウェアはリクエストを認証します。その後、SessionAuthenticatorが後続のリクエストのためにセッションを認証します。

JWT

JWTは、着信リクエストでJSON Web Tokenを認証するために使用できるJWTAuthenticatorを提供します。JWTを初めて使用する場合は、概要を確認してください。

まず、JWTペイロードを表すタイプを作成します。

// 例のJWTペイロード。
struct SessionToken: Content, Authenticatable, JWTPayload {

    // 定数
    let expirationTime: TimeInterval = 60 * 15

    // トークンデータ
    var expiration: ExpirationClaim
    var userId: UUID

    init(userId: UUID) {
        self.userId = userId
        self.expiration = ExpirationClaim(value: Date().addingTimeInterval(expirationTime))
    }

    init(with user: User) throws {
        self.userId = try user.requireID()
        self.expiration = ExpirationClaim(value: Date().addingTimeInterval(expirationTime))
    }

    func verify(using algorithm: some JWTAlgorithm) throws {
        try expiration.verifyNotExpired()
    }
}

次に、ログインレスポンスの成功時に含まれるデータの表現を定義できます。今のところ、レスポンスには署名されたJWTを表す文字列であるプロパティが1つだけあります。

struct ClientTokenResponse: Content {
    var token: String
}

JWTトークンとレスポンスのモデルを使用して、ClientTokenResponseを返し、署名されたSessionTokenを含むパスワード保護されたログインルートを使用できます。

let passwordProtected = app.grouped(User.authenticator(), User.guardMiddleware())
passwordProtected.post("login") { req async throws -> ClientTokenResponse in
    let user = try req.auth.require(User.self)
    let payload = try SessionToken(with: user)
    return ClientTokenResponse(token: try await req.jwt.sign(payload))
}

また、オーセンティケータを使用したくない場合は、次のようなものを使用できます。

app.post("login") { req async throws -> ClientTokenResponse in
    // ユーザーの提供された認証情報を検証
    // 提供されたユーザーのuserIdを取得
    let payload = try SessionToken(userId: userId)
    return ClientTokenResponse(token: try await req.jwt.sign(payload))
}

ペイロードをAuthenticatableJWTPayloadに準拠させることで、authenticator()メソッドを使用してルートオーセンティケータを生成できます。ルートグループにこれを追加して、ルートが呼び出される前にJWTを自動的に取得して検証します。

// SessionToken JWTを必要とするルートグループを作成します。
let secure = app.grouped(SessionToken.authenticator(), SessionToken.guardMiddleware())

オプションのガードミドルウェアを追加すると、認証が成功したことが必要になります。

保護されたルート内では、req.authを使用して認証されたJWTペイロードにアクセスできます。

// ユーザーが提供したトークンが有効な場合、okレスポンスを返します。
secure.post("validateLoggedInUser") { req -> HTTPStatus in
    let sessionToken = try req.auth.require(SessionToken.self)
    print(sessionToken.userId)
    return .ok
}