DRFソースコードの旅:カスタム認証ばかり作っていた私が「組み込み認証」を再発見するまで

1. DRFは愛用しているが、組み込み認証は信用していなかった



DjangoとDRF(Django Rest Framework)は私の開発人生の良き相棒です。しかし、認証(Authentication)に関しては、常にこだわりがありました。私は主にJWTAPI Key、あるいはOAuth2方式を好みます。最新のアプリ環境では、これらが標準だからです。

ところが、DRFの公式ドキュメントを開いてみると、私が使うようなモダンな方式よりも、Basic, Session, Token, Remoteといった、やや「古く見える」認証クラスがメインを占めています。「有能なDRFが、なぜわざわざこのようなものを内蔵しているのだろう?」と疑問に思うことも少なくありませんでした。そのため、私は常にBaseAuthenticationを継承し、自分だけのカスタム認証器を作って使っていました。

2. なぜ今、「組み込み認証」のソースコードを再び開くのか?

カスタム認証器を作るうちに、ふとこんな考えが浮かびました。

「私が作った認証器は、果たしてDjangoとDRFの哲学に完全に溶け込んでいるのだろうか?」

単にrequest.userにユーザーを格納する機能を超えて、DRFが設計した認証スキーマの「自然さ」を見落としているのではないか、という懸念が生じたのです。そこで今日から数回にわたり、DRFの組み込み認証器4つを徹底的に掘り下げてみようと思います。

  • BasicAuthentication (HTTP基本認証)
  • SessionAuthentication (Djangoセッション活用)
  • TokenAuthentication (単純トークン認証)
  • RemoteUserAuthentication (外部認証連携)

3. 「使いづらさ」の裏に隠された「哲学」を探る



正直なところ、最新のサービスでBasicAuthenticationをそのまま使うのはためらわれます。セキュリティに脆弱に見えますし、毎回IDとパスワードを送信する構造が不安です。しかし、そのソースコードを紐解いてみると、「認証失敗時にクライアントとどのようにコミュニケーションを取るか」について、非常に精巧な設計が盛り込まれていることがわかります。

例えば、単に403 Forbiddenを返すのと、authenticate_headerを通じて標準規格である401 Unauthorizedを促す違いのようなものです。このようなディテールこそが、「Djangoらしい」クラスを作る上での核心だったのです。

4. このシリーズの目的:カスタム認証を「自然に」DRFに溶け込ませる

このシリーズの終わりで私が得たいのは、単なる知識ではありません。

  1. インスピレーション(Inspiration): 組み込み認証器がなぜそのような構造になっているのか、その設計哲学を学びます。
  2. 移植(Porting): その哲学を、私が愛用するJWTやOAuth2のカスタム認証器に移植します。
  3. 調和(Harmony): そして、私が作ったカスタム認証器が、DRFの認証システムの中で、まるで最初からそこにあったかのように自然に機能するようにすることです。

単に機能を実装する開発者を超え、フレームワークの哲学をコードに溶け込ませることができる開発者になるための第一歩。DRF認証器ソースコード分析シリーズ、今、始まります。

開発者が古い南京錠を解剖しているイメージ

5. ソースコードを覗いてみる:BasicAuthentication

前置きが長くなりました。本格的にBasicAuthenticationの核心部に迫ってみましょう。


DRFのrest_framework/authentication.pyを開くと、このクラスの正体に出会えます。思ったより簡潔ですが、その中にはしっかりとしたルールが込められています。

class BasicAuthentication(BaseAuthentication):
    """
    HTTP Basic authentication against username/password.
    """
    www_authenticate_realm = 'api'

    def authenticate(self, request):
        auth = get_authorization_header(request).split()

        if not auth or auth[0].lower() != b'basic':
            return None

        if len(auth) == 1:
            msg = _('Invalid basic header. No credentials provided.')
            raise exceptions.AuthenticationFailed(msg)
        elif len(auth) > 2:
            msg = _('Invalid basic header. Credentials string should not contain spaces.')
            raise exceptions.AuthenticationFailed(msg)

        try:
            try:
                auth_decoded = base64.b64decode(auth[1]).decode('utf-8')
            except UnicodeDecodeError:
                auth_decoded = base64.b64decode(auth[1]).decode('latin-1')

            userid, password = auth_decoded.split(':', 1)
        except (TypeError, ValueError, UnicodeDecodeError, binascii.Error):
            msg = _('Invalid basic header. Credentials not correctly base64 encoded.')
            raise exceptions.AuthenticationFailed(msg)

        return self.authenticate_credentials(userid, password, request)

    def authenticate_credentials(self, userid, password, request=None):
        credentials = {
            get_user_model().USERNAME_FIELD: userid,
            'password': password
        }
        user = authenticate(request=request, **credentials)

        if user is None:
            raise exceptions.AuthenticationFailed(_('Invalid username/password.'))

        if not user.is_active:
            raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))

        return (user, None)

    def authenticate_header(self, request):
        return 'Basic realm="%s"' % self.www_authenticate_realm

なぜ「ヘッダー」なのか?

私たちは通常、データを送信する際にrequest.POSTrequest.dataをまず思い浮かべます。しかし、認証情報に限ってはヘッダーに含めるのが一般的です。なぜでしょうか?

認証はすべてのHTTPメソッド(GET, POST, PUT, DELETEなど)に柔軟に対応する必要があるからです。GETリクエストにはボディが一切なく、DELETEも本文が空である場合が多いです。もし認証情報をボディに入れる必要があったとしたら、私たちは参照する際にも無理やりPOSTを使うような奇妙な設計をしなければならなかったでしょう。認証をヘッダーに含めることは、「どのドアを開けるにしても、身分証明書は手に持っておけ」という約束のようなものです。


冷厳な現実:セキュリティの脆弱性

この方式の最大の欠点はBase64です。暗号化ではなく、単なる「エンコーディング」に過ぎません。誰でもデコードサイトで1秒あればあなたのIDとパスワードを知ることができます。

結論: HTTP環境では絶対に使用禁止です。HTTPSという安全なトンネル内で、しかも非常に限定的な用途でのみ使用すべきです。


最初のインスピレーション:マシン間通信(M2M)における費用対効果の高いセキュリティ

それでも、この古い認証器が魅力的に思える瞬間があります。それはサーバーとサーバーが通信する時です。 HMACや複雑なAPI Keyシステムを構築するにはリソースがもったいなく、かといって生データを送るのは不安な場合、私たちはこの構造を「少し」ひねって利用することができます。

  • カスタム暗号化: Base64の代わりに、両サーバーだけが知っている共通鍵で暗号化されたバイナリコンテンツを送信します。
  • 簡単な実装: 複雑なハンドシェイクなしでもBasic <Encrypted_Data>の形式だけで、素早く安全な通信が可能になります。「私たちだけの秘密のパスワード」をやり取りする効率的な通路となるのです。

第二のインスピレーション:失われた歯車 authenticate_header()

最も注目すべきメソッドはauthenticate_header()です。ソースコードのどこを見ても、このメソッドを直接呼び出している箇所はありません。しかし、このメソッドはDRFの例外ハンドラーの中で「401 Unauthorized」を生成する核心的な鍵なのです。

カスタム認証器を作る際、私たちがしばしば見落としがちなのが、このメソッドを省いてしまうことです。実際、このメソッドを省いても認証(Authentication)という行為自体には何ら影響がないためです。しかし、この些細な違いがビュー(View)側のコードをどれほど複雑にするかを比較してみる必要があります。

Case 1. authenticate_headerを省略した場合(手動方式)

このメソッドがない場合、DRFは「どのような方法で認証すべきかガイドを提供できない」と判断し、認証失敗時に401ではなく403 Forbiddenを返します。これを避けるためには、ビューで以下のような「手作業」が必要になります。

# ❌ authenticate_headerが欠けている場合
class MyPostAPIView(APIView):
    # IsAuthenticatedを使うと必ず403が返されるため、401のために一旦許可します。
    permission_classes = [AllowAny] 

    def post(self, request):
        # ビュー内部で認証状況を一つ一つ確認し、401を手動で返す必要があります。
        if not request.user or not request.user.is_authenticated:
            return Response(
                {"detail": "ログインが必要です。"}, 
                status=status.HTTP_401_UNAUTHORIZED
            )

        # ... 実際のロジック開始 ...

Case 2. authenticate_headerを実装した場合(DRF哲学の実装)

一方、カスタム認証器にたった一行の案内文(authenticate_header)を記述しておけば、DRFの巨大な認証スキーマがそれを認識し、自動的に401レスポンスを組み立てます。

# ✅ authenticate_headerが存在する場合
class MyPostAPIView(APIView):
    # これでDRFが自動的に401を返してくれるので、宣言的に権限を制御できます。
    permission_classes = [IsAuthenticated] 

    def post(self, request):
        # ビューは「ビジネスロジック」にのみ集中します。
        # 認証失敗の処理は既に上位層(認証器のauthenticate_headerメソッド)で完了しているためです。
        ... 

これこそが「Djangoらしい」美学です。断片化されたコードをビューに散りばめる代わりに、フレームワークが設計した歯車の間に自分のコードを完璧に組み込む感覚です。このメソッド一つで、「認証」と「認可」の役割を明確に分離し、ビューをより軽量に保つことができます。


DRFの真の魅力:静かなる譲歩(Noneの美学)

最後に、ソースコードのこの部分をもう一度見てみましょう。

if not auth or auth[0].lower() != b'basic':
    return None

形式が合わない時にエラーを吐き出すのではなく、Noneを返して静かに引き下がります。この些細な処理がDRFの柔軟性を生み出しています。おかげで私たちはAUTHENTICATION_CLASSESリストに複数の認証器を順番に格納できるのです。「ああ、私の方式じゃないね?じゃあ次の認証器を確認してみて!」と配慮する開発者の痕跡が感じられませんか?


まとめ

BasicAuthenticationは古く見えるかもしれませんが、その構造は現代的な認証システムの根幹を成しています。特に例外処理と多重認証サポートの方式は、私たちがカスタム認証器を作る際に必ず吸収すべき資産です。

さて、最初の山を越えました。次のポストでは、私たちにはお馴染みでありながらも、まだ謎の多いSessionAuthenticationを深く掘り下げていきます。

「セッション認証を使いながらも知らなかった事実」、ご期待ください!