コンテンツへスキップ

HTTP Basic認証

最も単純なケースでは、HTTP Basic認証を使用できます。

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

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

そして、値がBasicで、オプションのrealmパラメーターを持つヘッダーWWW-Authenticateを返します。

これはブラウザに、ユーザー名とパスワードの統合プロンプトを表示するように指示します。

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

シンプルなHTTP Basic認証

  • HTTPBasicHTTPBasicCredentialsをインポートします。
  • HTTPBasicを使用して「securityスキーム」を作成します。
  • そのsecurityパス操作内の依存関係で使用します。
  • これは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}
🤓 その他のバージョンとバリアント
from fastapi import Depends, FastAPI
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from typing_extensions import Annotated

app = FastAPI()

security = HTTPBasic()


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

ヒント

可能であれば`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: HTTPBasicCredentials = Depends(security)):
    return {"username": credentials.username, "password": credentials.password}

URLを初めて開くとき(またはドキュメントの「実行」ボタンをクリックするとき)、ブラウザはユーザー名とパスワードを要求します。

ユーザー名の確認

より完全な例を以下に示します。

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

このため、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}
🤓 その他のバージョンとバリアント
import secrets

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

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}

ヒント

可能であれば`Annotated`バージョンを使用することをお勧めします。

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を比較するまさにその瞬間に、すでにそれらの2つの文字列が同じではないことを知っているため、Falseを返します。「残りの文字を比較するのにこれ以上計算を浪費する必要はない」と考えているからです。そして、アプリケーションは「間違ったユーザー名またはパスワード」と表示します。

しかし、攻撃者はユーザー名stanleyjobsox、パスワードlove123で再度試します。

そして、アプリケーションコードは次のようなことを行います。

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

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

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

その時点で、サーバーが「間違ったユーザー名またはパスワード」という応答を送信するのに数マイクロ秒長くかかったことに気づくことで、攻撃者は何かが正しかったこと、つまり最初のいくつかの文字が正しかったことを知るでしょう。

そして、johndoeよりもstanleyjobsoxに似ている可能性が高いことを知って、再度試すことができます。

"プロフェッショナルな"攻撃

もちろん、攻撃者はこれを手作業で行うのではなく、プログラムを作成して実行し、おそらく毎秒何千または何百万ものテストを行います。そして、一度に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}
🤓 その他のバージョンとバリアント
import secrets

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

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}

ヒント

可能であれば`Annotated`バージョンを使用することをお勧めします。

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}