REST framework厨のお前らならば余裕で知ってる認証失敗時の401と403の違い
これは Django Advent Calendar 2021 の 21日 の記事です。
やぁ!全国の3万236人くらいの Django REST framework厨 のみんな元気してるかな?突然だけど、下記のコードをみて欲しい。
# settings.py ----
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
]
}
# views.py ----
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
class SpamAPIView(APIView):
permission_classes = [IsAuthenticated]
def get(self, request, *args, **kwargs):
return Response({"message": "200 ok"})
spam = SpamAPIView.as_view()
このコードは見ての通り、IsAuthenticated
が指定されてるので、単純にGETでアクセスしても認証エラーになる。
この時、レスポンスのHTTPステータスコードは何になると思いますか? Let's ティンキングタイム!
... はい。終了。そうだね認証エラーの時は 401 unauthorized が返ってくる。当たり前だね。
じゃあ次のように settingsのコードを変えてみたらどうなるだろう。
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
]
}
... はい。そうだね 403 permission denied が返ってくる。まだまだ余裕だね。
じゃぁ TokenAuthentication
と SessionAuthentication
を複数指定するとどうなる?
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
]
}
そう 401 だね。当然だ。じゃぁ順番を入れ替えてみようか。
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication',
]
}
... はい。そうだね ... 403 が返ってくる...
どう? Django REST framework厨 のお前らならば余裕で知ってたよね? おじさんが気づいた時は、意味がわからなくてとても混乱したよ。
何これ?
この挙動はドキュメントに記載してある
- https://www.django-rest-framework.org/api-guide/authentication/#unauthorized-and-forbidden-responses
簡単に言うと
-
401
はWWW-Authenticate
が必須 - どちらのスタータスコードを返すべきかは認証方式により異なる
-
restframework
は、どちらを返すべきかはAUTENTICAION_CLASSES
の 先頭のクラス によって判断している
となる
最初の例だと、SessionAuthentication が先頭にくるか、TokenAuthentication が先頭にくるかで、認証エラー時のステータスコードが変わってくるということ。
WWW-Authenticate て何?
-
WWW-Authenticate
は URIリソースに対して、どういう認証方式(チャレンジ)が必要かを明示するためのレスポンスヘッダー。 - https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/WWW-Authenticate
- Basic認証とかDigest認証とか
- レスポンスヘッダーにある
WWW-Authenticate
をみてリクエスト側が、それに合わせてAuthorization
ヘッダーをセットしたりする - restframework の
TokenAuthentication
であればWWW-Authenticate: Token
がレスポンスヘッダーにセットされるようになっている。
SessionAuthentication はなんで403なの?
-
WWW-Authenticate
は ユーザー がリクエストヘッダーに明示的に認証方式に合わせて認証情報を乗せてリクエストすることで、認証が成立する。 -
一方セッション認証というのは、特にRFCとして実装方法が規定されているわけでもい。
-
大概はブラウザによるCookieの自動送信に任せてたりするので、クライアントが明示的にリクエストにセットしているわけはないという点で
WWW-Authenticate
をセットするべきではないという見解のようだ。 -
WWW-Authenticate
がないのであれば、 それは仕様上 401 にはするべきではないので 403 にしている。 -
この辺は、restframework のユーザーも混乱してイシューに何度か上がっていたりする
WWW-Authenticate は 複数入れれば良いのでは?
MDN の 説明では以下のように書かれている
1 つの WWW-Authenticate ヘッダーには複数のチャレンジが許され、
1 つのレスポンスには複数の WWW-Authenticate ヘッダーが許されます。
この仕様をみると、AUTHENTICATION_CLASSES
の先頭だけとかにせず、設定されてる全ての認証方式の WWW-Authenticate
を返せばいいのでは?とかいう疑問が出てくる。
全く同じことを考えている人がいた
端的にいうとこのアイディアは副作用があるので良くないということだった。例えば、以下のような設定の場合
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication',
'rest_framework.authentication.BasicAuthentication',
)
}
WWW-Authenticate
には トークン認証と、Basic認証を一緒にして返すことができるが、
世の中のブラウザはBasic認証などの場合、認証のダイアログが表示されてしまう。
これは開発してる側としては嬉しくない副作用であるということが理解できる。なので、主となる先頭の認証方式だけを見て判断している。
なんでこれに気づいたの?
-
django
でwebtest
を使えるようにする django-webtest というライブラリがある。 - このライブラリは、
settings
にREST_FRAMEWORK
があるかどうかを見て、自動的にDEFAULT_AUTHENTICATION_CLASSES
の 先頭 にWebtestAuthentication
というクラスを追加してくる - これにより トークン認証とかを採用してた場合、通常は
401
が返ってくるが、なぜかテストの時だけ403が返ってくる という現象が発生する。 - 多分、django-webtest の作者が今回の仕様について知らなかっただけと思われる。
- まだPRは出してない。
まとめ
- 最初は、順番に依存するとか、わかりにくい仕様だなと思ったが、割とちゃんとした理由があったので、至って落ち着いています。
- 401 とか 403とか WWW-Authenticate についてあまり良くわかってなかったので、調べてよかった。
- アドベントカレンダーの担当日を完全に勘違いしてました。申し訳ありません...
Discussion