🐱

REST framework厨のお前らならば余裕で知ってる認証失敗時の401と403の違い

2021/12/22に公開

これは 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 が返ってくる。まだまだ余裕だね。

じゃぁ TokenAuthenticationSessionAuthentication を複数指定するとどうなる?

  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厨 のお前らならば余裕で知ってたよね? おじさんが気づいた時は、意味がわからなくてとても混乱したよ。

何これ?

この挙動はドキュメントに記載してある

簡単に言うと

  • 401WWW-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認証などの場合、認証のダイアログが表示されてしまう。

これは開発してる側としては嬉しくない副作用であるということが理解できる。なので、主となる先頭の認証方式だけを見て判断している。

なんでこれに気づいたの?

まとめ

  • 最初は、順番に依存するとか、わかりにくい仕様だなと思ったが、割とちゃんとした理由があったので、至って落ち着いています。
  • 401 とか 403とか WWW-Authenticate についてあまり良くわかってなかったので、調べてよかった。
  • アドベントカレンダーの担当日を完全に勘違いしてました。申し訳ありません...

Discussion