コンテンツへスキップ

HTTP Basic認証

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

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

ヘッダーを受信しないと、HTTP 401「未承認」エラーを返します。

そして、値が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()を使用しています。

簡単に言うと、`stanleyjobsox`と`stanleyjobson`を比較する時間と、`johndoe`と`stanleyjobson`を比較する時間は同じになります。パスワードについても同様です。

このように、アプリケーションコードで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}