ユーザーの資格情報の検証

ユーザーの資格情報の検証

AdonisJSアプリケーションでは、ユーザーの資格情報の検証は認証レイヤーから切り離されています。これにより、認証ガードを使用しながらユーザーの資格情報を検証するオプションが制限されることなく続けることができます。

デフォルトでは、安全なAPIを提供してユーザーを検索し、パスワードを検証できます。ただし、電話番号にOTPを送信したり、2FAを使用したりするなど、ユーザーを検証するための追加の方法も実装できます。

このガイドでは、UIDでユーザーを検索し、ログイン前にパスワードを検証するプロセスについて説明します。

基本的な例

Userモデルを直接使用してユーザーを検索し、パスワードを検証できます。次の例では、メールアドレスでユーザーを検索し、hashサービスを使用してパスワードハッシュを検証しています。

import User from '#models/user'
import hash from '@adonisjs/core/services/hash'
import type { HttpContext } from '@adonisjs/core/http'
export default class SessionController {
async store({ request, response }: HttpContext) {
const { email, password } = request.only(['email', 'password'])
/**
* メールアドレスでユーザーを検索します。ユーザーが存在しない場合はエラーを返します。
*/
const user = await User.findBy('email', email)
if (!user) {
return response.abort('無効な資格情報')
}
/**
* ハッシュサービスを使用してパスワードを検証します。
*/
const isPasswordValid = await hash.verify(user.password, password)
if (!isPasswordValid) {
return response.abort('無効な資格情報')
}
/**
* ユーザーをログインさせるか、トークンを作成します。
*/
// ...
}
}

上記のアプローチの問題点

上記の例で書かれたコードは、タイミング攻撃のリスクがあります。認証の場合、攻撃者はアプリケーションの応答時間を観察して、提供された資格情報のメールアドレスまたはパスワードが正しくないかどうかを判断できます。 AuthFinderミックスインを使用して、タイミング攻撃を防止し、より良い開発者体験を得ることをオススメします。

上記の実装により、次のような結果が得られます。

  • ユーザーのメールアドレスが間違っている場合、リクエストは短時間で完了します。これは、ユーザーが見つからない場合にパスワードハッシュを検証しないためです。

  • メールアドレスが存在し、パスワードが間違っている場合、リクエストは長時間かかります。これは、パスワードのハッシュ化アルゴリズムが時間がかかるためです。

応答時間の差は、攻撃者が有効なメールアドレスを見つけ、異なるパスワードの組み合わせを試すのに十分な情報となります。

AuthFinderミックスインの使用

タイミング攻撃を防ぐために、UserモデルにAuthFinderミックスインを使用することをオススメします。

Auth finderミックスインは、適用されたモデルにfindForAuthメソッドとverifyCredentialsメソッドを追加します。verifyCredentialsメソッドは、ユーザーを見つけてパスワードを検証するためのタイミング攻撃に対して安全なAPIを提供します。

次のように、ミックスインをインポートしてモデルに適用できます。

import { DateTime } from 'luxon'
import { compose } from '@adonisjs/core/helpers'
import { BaseModel, column } from '@adonisjs/lucid/orm'
import hash from '@adonisjs/core/services/hash'
import { withAuthFinder } from '@adonisjs/auth/mixins/lucid'
const AuthFinder = withAuthFinder(() => hash.use('scrypt'), {
uids: ['email'],
passwordColumnName: 'password',
})
export default class User extends compose(BaseModel, AuthFinder) {
@column({ isPrimary: true })
declare id: number
@column()
declare fullName: string | null
@column()
declare email: string
@column()
declare password: string
@column.dateTime({ autoCreate: true })
declare createdAt: DateTime
@column.dateTime({ autoCreate: true, autoUpdate: true })
declare updatedAt: DateTime
}
  • withAuthFinderメソッドは、最初の引数としてハッシャーを返すコールバックを受け入れます。上記の例ではscryptハッシャーを使用していますが、別のハッシャーに置き換えることもできます。

  • 次に、以下のプロパティを持つ構成オブジェクトを受け入れます。

    • uids: ユーザーを一意に識別するために使用できるモデルのプロパティの配列です。ユーザーにユーザー名や電話番号を割り当てた場合、それらもUIDとして使用できます。
    • passwordColumnName: ユーザーパスワードを保持するモデルのプロパティ名です。
  • 最後に、withAuthFinderメソッドの戻り値をUserモデルのmixinとして使用できます。

資格情報の検証

Auth finderミックスインを適用した後は、SessionController.storeメソッドのコードを次のコードスニペットで置き換えることができます。

import { HttpContext } from '@adonisjs/core/http'
import User from '#models/user'
import hash from '@adonisjs/core/services/hash'
export default class SessionController {
async store({ request, response }: HttpContext) {
async store({ request }: HttpContext) {
const { email, password } = request.only(['email', 'password'])
/**
* メールアドレスでユーザーを検索します。ユーザーが存在しない場合はエラーを返します。
*/
const user = await User.findBy('email', email)
if (!user) {
response.abort('無効な資格情報')
}
/**
* ハッシュサービスを使用してパスワードを検証します。
*/
await hash.verify(user.password, password)
const user = await User.verifyCredentials(email, password)
/**
* ユーザーをログインさせるか、トークンを作成します。
*/
}
}

例外の処理

資格情報が無効な場合、verifyCredentialsメソッドはE_INVALID_CREDENTIALS例外をスローします。

この例外は自動的に処理され、以下のコンテンツネゴシエーションルールにしたがってレスポンスに変換されます。

  • Accept=application/jsonヘッダーを持つHTTPリクエストは、エラーメッセージの配列を受け取ります。各配列要素はメッセージプロパティを持つオブジェクトです。

  • Accept=application/vnd.api+jsonヘッダーを持つHTTPリクエストは、JSON API仕様にしたがってフォーマットされたエラーメッセージの配列を受け取ります。

  • セッションを使用している場合、ユーザーはフォームにリダイレクトされ、セッションフラッシュメッセージを介してエラーを受け取ります。

  • その他のリクエストは、プレーンテキストとしてエラーを受け取ります。

ただし、必要に応じて、グローバル例外ハンドラ内で例外を処理することもできます。

import { errors } from '@adonisjs/auth'
import { HttpContext, ExceptionHandler } from '@adonisjs/core/http'
export default class HttpExceptionHandler extends ExceptionHandler {
protected debug = !app.inProduction
protected renderStatusPages = app.inProduction
async handle(error: unknown, ctx: HttpContext) {
if (error instanceof errors.E_INVALID_CREDENTIALS) {
return ctx
.response
.status(error.status)
.send(error.getResponseMessage(error, ctx))
}
return super.handle(error, ctx)
}
}

ユーザーパスワードのハッシュ化

AuthFinderミックスインは、INSERTおよびUPDATE呼び出し時に自動的にユーザーパスワードをハッシュ化するためのbeforeSaveフックを登録します。そのため、モデル内でパスワードのハッシュ化を手動で行う必要はありません。