認証¶
認証は、ユーザーの身元を確認する行為です。これは、ユーザー名とパスワードまたは一意のトークンのような認証情報の検証を通じて行われます。認証(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
に準拠するユーザータイプが必要です。これはstruct
、class
、または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つのオーセンティケータUserPasswordAuthenticator
とUserTokenAuthenticator
を想定しています。これらのオーセンティケータの両方がルートグループに追加されます。最後に、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)
}
// 何かを行う。
}
この例では、それぞれAdmin
とUser
を認証する2つのオーセンティケータAdminAuthenticator
とUserAuthenticator
を想定しています。これらのオーセンティケータの両方がルートグループに追加されます。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は、既存のモデルに追加できるModelAuthenticatable
とModelTokenAuthenticatable
の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)
}
}
この拡張はUser
にModelAuthenticatable
準拠を追加します。最初の2つのプロパティは、ユーザー名とパスワードハッシュをそれぞれ保存するために使用するフィールドを指定します。\
記法は、Fluentがそれらにアクセスするために使用できるフィールドへのキーパスを作成します。
最後の要件は、Basic認証ヘッダーで送信されたプレーンテキストパスワードを検証するメソッドです。サインアップ中にパスワードをハッシュするためにBcryptを使用しているため、Bcryptを使用して、提供されたパスワードが保存されたパスワードハッシュと一致することを検証します。
User
がModelAuthenticatable
に準拠したので、ログインルートを保護するためのオーセンティケータを作成できます。
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¶
UserToken
をModelTokenAuthenticatable
に準拠させます。これにより、トークンが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
セッションクッキーはデフォルトでsecure
とhttpOnly
に設定されていません。クッキーの設定方法の詳細については、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リクエストでusername
とpassword
を送信する必要があります。次のようにフォームを設定できます:
<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
は、リクエストボディからusername
とpassword
を抽出し、ユーザー名からユーザーを見つけ、パスワードを検証します。パスワードが有効な場合、ミドルウェアはリクエストを認証します。その後、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))
}
ペイロードをAuthenticatable
とJWTPayload
に準拠させることで、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
}