コンテンツにスキップ

HTTP Basic 認証

🌐 AI と人間による翻訳

この翻訳は、人間のガイドに基づいて AI によって作成されました。🤝

原文の意図を取り違えていたり、不自然な表現になっている可能性があります。🤖

AI LLM をより適切に誘導するのを手伝う ことで、この翻訳を改善できます。

英語版

最もシンプルなケースでは、HTTP Basic 認証を利用できます。

HTTP Basic 認証では、アプリケーションはユーザー名とパスワードを含むヘッダーを期待します。

それを受け取れない場合、HTTP 401 "Unauthorized" エラーを返します。

そして、値が Basic のヘッダー WWW-Authenticate を、任意の realm パラメータとともに返します。

これにより、ブラウザは組み込みのユーザー名とパスワード入力プロンプトを表示します。

その後、そのユーザー名とパスワードを入力すると、ブラウザはそれらをヘッダーに自動的に付与して送信します。

シンプルな HTTP Basic 認証

  • HTTPBasicHTTPBasicCredentials をインポートします。
  • HTTPBasic を使って「security スキーム」を作成します。
  • その security を依存関係として path operation に使用します。
  • HTTPBasicCredentials 型のオブジェクトが返ります:
    • 送信された usernamepassword を含みます。
from typing import Annotated

from fastapi import Depends, FastAPI
from fastapi.security import HTTPBasic, HTTPBasicCredentials

app = FastAPI()

security = HTTPBasic()


@app.get("/users/me")
def read_current_user(credentials: Annotated[HTTPBasicCredentials, Depends(security)]):
    return {"username": credentials.username, "password": credentials.password}
🤓 Other versions and variants

Tip

Prefer to use the Annotated version if possible.

from fastapi import Depends, FastAPI
from fastapi.security import HTTPBasic, HTTPBasicCredentials

app = FastAPI()

security = HTTPBasic()


@app.get("/users/me")
def read_current_user(credentials: HTTPBasicCredentials = Depends(security)):
    return {"username": credentials.username, "password": credentials.password}

URL を最初に開こうとしたとき(またはドキュメントで「Execute」ボタンをクリックしたとき)、ブラウザはユーザー名とパスワードの入力を求めます:

ユーザー名の確認

より完全な例です。

依存関係を使ってユーザー名とパスワードが正しいかを確認します。

これには、Python 標準モジュール secrets を用いてユーザー名とパスワードを検証します。

secrets.compare_digest()bytes か、ASCII 文字(英語の文字)のみを含む str を受け取る必要があります。つまり、Sebastián のように á を含む文字ではそのままでは動作しません。

これに対処するため、まず usernamepassword を UTF-8 でエンコードして bytes に変換します。

そのうえで、secrets.compare_digest() を使って、credentials.username"stanleyjobson" であり、credentials.password"swordfish" であることを確認します。

import secrets
from typing import Annotated

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials

app = FastAPI()

security = HTTPBasic()


def get_current_username(
    credentials: Annotated[HTTPBasicCredentials, Depends(security)],
):
    current_username_bytes = credentials.username.encode("utf8")
    correct_username_bytes = b"stanleyjobson"
    is_correct_username = secrets.compare_digest(
        current_username_bytes, correct_username_bytes
    )
    current_password_bytes = credentials.password.encode("utf8")
    correct_password_bytes = b"swordfish"
    is_correct_password = secrets.compare_digest(
        current_password_bytes, correct_password_bytes
    )
    if not (is_correct_username and is_correct_password):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Basic"},
        )
    return credentials.username


@app.get("/users/me")
def read_current_user(username: Annotated[str, Depends(get_current_username)]):
    return {"username": username}
🤓 Other versions and variants

Tip

Prefer to use the Annotated version if possible.

import secrets

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials

app = FastAPI()

security = HTTPBasic()


def get_current_username(credentials: HTTPBasicCredentials = Depends(security)):
    current_username_bytes = credentials.username.encode("utf8")
    correct_username_bytes = b"stanleyjobson"
    is_correct_username = secrets.compare_digest(
        current_username_bytes, correct_username_bytes
    )
    current_password_bytes = credentials.password.encode("utf8")
    correct_password_bytes = b"swordfish"
    is_correct_password = secrets.compare_digest(
        current_password_bytes, correct_password_bytes
    )
    if not (is_correct_username and is_correct_password):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Basic"},
        )
    return credentials.username


@app.get("/users/me")
def read_current_user(username: str = Depends(get_current_username)):
    return {"username": username}

これは次のようなコードに相当します:

if not (credentials.username == "stanleyjobson") or not (credentials.password == "swordfish"):
    # Return some error
    ...

しかし secrets.compare_digest() を使うことで、「タイミング攻撃」と呼ばれる種類の攻撃に対して安全になります。

タイミング攻撃

「タイミング攻撃」とは何でしょうか?

攻撃者がユーザー名とパスワードを推測しようとしていると想像してください。

そして、ユーザー名 johndoe、パスワード love123 を使ってリクエストを送ります。

その場合、アプリケーション内の Python コードは次のようなものと等価になります:

if "johndoe" == "stanleyjobson" and "love123" == "swordfish":
    ...

しかし、Python は johndoe の最初の jstanleyjobson の最初の s を比較した時点で、両者の文字列が同じでないと判断してすぐに False を返します。つまり「残りの文字を比較して計算資源を無駄にする必要はない」と考えるわけです。そしてアプリケーションは「ユーザー名またはパスワードが正しくありません」と返します。

次に、攻撃者がユーザー名 stanleyjobsox、パスワード love123 で試すとします。

アプリケーションのコードは次のようになります:

if "stanleyjobsox" == "stanleyjobson" and "love123" == "swordfish":
    ...

この場合、Python は stanleyjobsoxstanleyjobson の両方で stanleyjobso 全体を比較してから、文字列が同じでないと気づきます。したがって、「ユーザー名またはパスワードが正しくありません」と応答するまでに余分に数マイクロ秒かかります。

応答時間が攻撃者を助ける

ここで、サーバーが「ユーザー名またはパスワードが正しくありません」というレスポンスを返すまでに、わずかに長い時間がかかったことに気づけば、攻撃者は何かしら正解に近づいた、すなわち先頭のいくつかの文字が正しかったことを知ることができます。

すると、johndoe よりも stanleyjobsox に近いものを狙って再試行できます。

「プロ」レベルの攻撃

もちろん、攻撃者はこれらを手作業では行わず、プログラムを書いて、1 秒間に数千〜数百万回のテストを行うでしょう。そして 1 回に 1 文字ずつ正しい文字を見つけていきます。

そうすることで、数分から数時間のうちに、攻撃者は私たちのアプリケーションの「助け」(応答にかかった時間)だけを利用して、正しいユーザー名とパスワードを推測できてしまいます。

secrets.compare_digest() で対策

しかし、私たちのコードでは実際に secrets.compare_digest() を使用しています。

要するに、stanleyjobsoxstanleyjobson を比較するのにかかる時間は、johndoestanleyjobson を比較するのにかかる時間と同じになります。パスワードでも同様です。

このように、アプリケーションコードで secrets.compare_digest() を使うと、この種の一連のセキュリティ攻撃に対して安全になります。

エラーを返す

認証情報が不正であることを検出したら、ステータスコード 401(認証情報が提供されない場合と同じ)で HTTPException を返し、ブラウザに再度ログインプロンプトを表示させるためにヘッダー WWW-Authenticate を追加します:

import secrets
from typing import Annotated

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials

app = FastAPI()

security = HTTPBasic()


def get_current_username(
    credentials: Annotated[HTTPBasicCredentials, Depends(security)],
):
    current_username_bytes = credentials.username.encode("utf8")
    correct_username_bytes = b"stanleyjobson"
    is_correct_username = secrets.compare_digest(
        current_username_bytes, correct_username_bytes
    )
    current_password_bytes = credentials.password.encode("utf8")
    correct_password_bytes = b"swordfish"
    is_correct_password = secrets.compare_digest(
        current_password_bytes, correct_password_bytes
    )
    if not (is_correct_username and is_correct_password):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Basic"},
        )
    return credentials.username


@app.get("/users/me")
def read_current_user(username: Annotated[str, Depends(get_current_username)]):
    return {"username": username}
🤓 Other versions and variants

Tip

Prefer to use the Annotated version if possible.

import secrets

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials

app = FastAPI()

security = HTTPBasic()


def get_current_username(credentials: HTTPBasicCredentials = Depends(security)):
    current_username_bytes = credentials.username.encode("utf8")
    correct_username_bytes = b"stanleyjobson"
    is_correct_username = secrets.compare_digest(
        current_username_bytes, correct_username_bytes
    )
    current_password_bytes = credentials.password.encode("utf8")
    correct_password_bytes = b"swordfish"
    is_correct_password = secrets.compare_digest(
        current_password_bytes, correct_password_bytes
    )
    if not (is_correct_username and is_correct_password):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Basic"},
        )
    return credentials.username


@app.get("/users/me")
def read_current_user(username: str = Depends(get_current_username)):
    return {"username": username}