DjangoでPasskey実装する

皆さん、お疲れ様です。
プラットフォーム事業部のTです。
今回は「DjangoでPasskey実装する」について、
Passkeyの仕組みとライブラリ導入に焦点を当てて紹介します。
この記事は以下の方を対象としています。
★5 Djangoの開発経験が3年以上。
★4 Djangoの開発経験が1年以上。
★3 Webサイト開発経験あり。これからDjangoを学習します。
★2 Python 初級者。簡単なプログラムコードが書けます。
★1 プログラミング未経験。
Passkeyとは
従来のパスワードを使用したログインとは異なり、スマートフォンやPCに保存された「鍵」を用いてログインを行う仕組みです。ユーザーはパスワードを入力する代わりに、端末の生体認証(Face ID/Touch ID)やPINで本人確認を行い、端末内に保存されている鍵によってサーバーへ署名(本人であることの証明)を送信することでログインが成立します。
ユーザーは生体認証を行うだけでよく、パスワードの記憶や入力自体がなくなるため、パスワード漏えいのリスクを大幅に低減できます。このメリットは非常に大きいです。
理想的なパスワードは、「大文字・小文字・数字・記号をすべて含む12文字以上の文字列」であり、さらにサイトごとに使い回さないことが推奨されています。しかし、このようなパスワードをツールなしで人間の記憶だけで管理するのは困難です。
さらに、Passkeyの利用はユーザーだけでなく、アプリ開発者にもメリットがあります。
Passkeyの仕組みとメリット
Passkeyは、公開鍵暗号方式のキーペア(公開鍵と秘密鍵)を利用した仕組みです。
公開鍵はサーバーに保存され、署名の検証に使用されます。この公開鍵単体ではログインはできず、秘密鍵を推測することも不可能です。そのため、仮に公開鍵が漏えいしたとしてもセキュリティ上の問題はありません。
一方、秘密鍵は、ユーザーの端末などで安全に管理され、サーバーからのリクエストに応じて署名を生成するために使用されます。秘密鍵そのものがWebサービスのサーバーへ送信されることはなく、さらにサイトやアプリごとに異なるキーペアが生成されます。
- 公開鍵だけでは不正ログインできない
- サイト・アプリごとにキーペアが作成される
これら2つのメリットについて説明します。
- 公開鍵だけでは不正ログインできない
上述した通り、公開鍵だけでは認証に必要な処理は行えないため、仮にデータベースが漏えいしたとしても、それだけで不正ログインにつながることはありません。 - サイト・アプリごとにキーペアが作成されること
これはフィッシング対策として有効です。キーペアは通常、「example.com」のようなドメインに紐づいて生成されます。そのため、「exmp1e.com」のような偽サイトから認証要求が行われた場合、正規サイト用のキーペアを利用することはできません。つまり、ユーザーが誤って偽サイトにアクセスした場合でも、認証情報が悪用されにくい仕組みになっています。このような保護は、従来のパスワード認証では実現できない大きな利点です。
ここまで、Passkeyの仕組みとメリットを見てきました。
次に、実際の処理フローとDjangoでの実装について説明します。
ログイン時の処理フローおよびDjangoでの実装範囲
ログイン時の処理フローは以下の通りです。
※登録時のフローは省略しています

上記フローにおけるDjangoの実装範囲は、主に以下の3点です。
- Challengeを発行し、セッションと紐づける
- ブラウザから返却された署名を、公開鍵で検証する
- 認証に成功した場合、通常のログイン処理を実行する
なお、セッション認証など、Djangoの認証基盤自体は従来と変わりません。
ライブラリ導入について
上述したPasskeyの実装を行うため、ライブラリを導入します。今回の実装では、django-otp-webauthnというライブラリを使用して、Passkeyでのログインを実現します。使用するバージョン0.8.0です。
settingsへの追加設定
まずは、settings.pyへの追加設定を見ていきます。
INSTALLED_APPS = [
...
"django_otp",
"django_otp_webauthn",
...
]
INSTALLED_APPSに”django_otp”と”django_otp_webauthn”を追加します。Passkeyの実装であるため、django_otpは無関係のように見えますが、django_otp_webauthnがdjango_otpを基盤としているため、INSTALLED_APPSへの追加が必要です。django_otp_webauthnを追加することで、必要なモデルのマイグレーションやテンプレートタグ、Challengeの発行や認証結果の検証に必要なビューが有効になります。
MIDDLEWARE = [
...
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django_otp.middleware.OTPMiddleware",
...
]
次に、MiddlewareにOTPMiddlewareを追加します。OTPMiddlewareは、request.userにotp_deviceやis_verified()を追加するためAuthenticationMiddlewareのあとに設定する必要があります。今回の実装では、Passkey単体でのログインも可能な構成を想定しているため、OTPMiddlewareによって追加されたotp_deviceやis_verified()は使用しませんが、ライブラリが想定する構成に従って追加します。
AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend",
"django_otp_webauthn.backends.WebAuthnBackend",
]
さらに、AUTHENTICATION_BACKENDSを設定します。これは「認証に使用するバックエンドのリスト」で、パスワード認証とPasskey認証の両方を利用するために必要です。
OTP_WEBAUTHN_RP_NAME = env("OTP_WEBAUTHN_RP_NAME", default="app")
OTP_WEBAUTHN_RP_ID = env("OTP_WEBAUTHN_RP_ID", default="localhost")
OTP_WEBAUTHN_ALLOWED_ORIGINS = env.list(
"OTP_WEBAUTHN_ALLOWED_ORIGINS",
default=["http://localhost:8000"],
)
最後に、環境変数を設定すればsettings.pyへの追加設定は完了です。
各環境変数の効果は以下のとおりです。
| キー | 効果 |
| OTP_WEBAUTHN_RP_NAME | 認証ダイアログに表示される名前 |
| OTP_WEBAUTHN_RP_ID | パスキーを紐付けるドメイン |
| OTP_WEBAUTHN_ALLOWED_ORIGINS | ceremony を許可する Origin |
注意点として、RP_IDと許可するOriginには、以下の2つの制約があります。
- localhostを除き、設定するOriginはHTTPS必須であること。
- 設定するOriginは、RP_ID自身または、そのサブドメインであること。
例) RP_ID = example.com の場合
OK: https://example.com (RP_ID 自身)
OK: https://app.example.com (サブドメイン)
NG: https://example.org (別ドメイン)
NG: https://notexample.com (サブドメインではない別物)
urls.pyへの追記
urlpatterns = [
path("webauthn/", include("django_otp_webauthn.urls", namespace="otp_webauthn")),
]
urls.pyにパスを追加するが必要があるため、include(“django_otp_webauthn.urls”)を使用し、django_otp_webauthnが提供するURLをwebauthn配下に追加します。追加されるURLとその役割は、以下のとおりです。
| URL名 | 実パス | 役割 |
| credential-registration-begin | /webauthn/registration/begin/ | 登録用の challenge を発行 |
| credential-registration-complete | /webauthn/registration/complete/ | 登録結果を検証して保存 |
| credential-authentication-begin | /webauthn/authentication/begin/ | ログイン用の challenge を発行 |
| credential-authentication-complete | /webauthn/authentication/complete/ | 署名を検証してログイン |
| js-i18n-catalog | /webauthn/jsi18n/ | JavaScript 用の翻訳カタログ |
おわりに
今回はPasskeyの概要と、DjangoでPasskeyを実装するためのライブラリの導入について解説しました。ライブラリの導入までとなりましたが、次回は具体的な実装について解説します。
最後までお読みいただき、ありがとうございました。
