HTTP Basic認証¶
最も簡単なケースでは、HTTP Basic認証を使用できます。
HTTP Basic認証では、アプリケーションはユーザー名とパスワードを含むヘッダーを期待します。
ヘッダーを受信しないと、HTTP 401「未承認」エラーを返します。
そして、値がBasic
で、オプションのrealm
パラメータを持つWWW-Authenticate
ヘッダーを返します。
これにより、ブラウザはユーザー名とパスワードの統合プロンプトを表示します。
ユーザー名とパスワードを入力すると、ブラウザはそれらをヘッダーに自動的に送信します。
シンプルなHTTP Basic認証¶
HTTPBasic
とHTTPBasicCredentials
をインポートします。HTTPBasic
を使用して「security
スキーム」を作成します。- パスオペレーションで依存関係を使用して、その
security
を使用します。 HTTPBasicCredentials
型のオブジェクトを返します。- 送信された
username
とpassword
が含まれています。
- 送信された
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
にあるようなá
のような文字は機能しません。
これを処理するために、最初にusername
とpassword
を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
の最初のj
とstanleyjobson
の最初のs
を比較する瞬間、既にそれらの2つの文字列が同じではないことがわかるため、False
を返し、「残りの文字を比較する計算を無駄にする必要はない」と考えます。そして、アプリケーションは「ユーザー名またはパスワードが間違っています」と言います。
しかし、攻撃者がユーザー名stanleyjobsox
とパスワードlove123
を試すとします。
そして、アプリケーションコードは次のようなものになります。
if "stanleyjobsox" == "stanleyjobson" and "love123" == "swordfish":
...
Pythonは、両方の文字列が同じではないことに気づく前に、stanleyjobsox
とstanleyjobson
の両方の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}