仕組みから学ぶWebAuthn対応Djangoアプリ開発
こんにちは、筒井です。
Qiita Django Advent Calendar 2018 11日目は、WebAuthn(Web Authentication)認証に対応したDjangoアプリケーションの作り方について解説します。
WebAuthnとは何か
WebAuthn(Web Authentication)とは、2018年4月10日にW3C勧告候補になったウェブ認証の仕組みです。従来は本人しか知らない前提の「パスワード」による認証が主流でしたが、以下のリスクがありました。
- 推測されやすい単純な値を使っている場合にアカウントを盗まれる。
- 他のサービスと同じID・パスワードを使いまわしていると、あるサービスでパスワードが漏洩することで他のサービスでもアカウントを盗まれる。
パスワード認証の弱点を補うためによく使われる手段として、1Passwordのようなパスワード生成ツールや2段階認証がありますが、いずれも手続きが複雑になり、万人向けとは言えません。
WebAuthnでは「認証器(Authenticator)」と呼ばれるデバイスを利用することで、パスワード認証より人間の負担を減らし、かつよりセキュアに認証を行います。以下で登録・認証の流れを説明します。
【登録】
Relying Party Server(本記事ではDjangoアプリケーションがここに該当します)から生成されたPublicKeyCredentialCreateOptionsをJavaScriptコードが受け取り、WebAuthnAPI経由で公開鍵を生成・サーバーに送信し、サーバーが公開鍵を検証することで登録が完了します。
以下に全体の流れを説明する図を作りました。
【認証】
サーバーが生成したPublicKeyCredentialRequestOptionsを受け取ったJavaScriptコードはWebAuthnAPI経由で認証情報を生成・サーバーに送信し、受け取ったサーバーは公開鍵を使って認証情報を検証します。
以下に全体の流れを説明する図を作りました。
WebAuthnをサポートするブラウザについては以下を参照してください。
Web Authentication API | MDN ブラウザーの対応
最近のブラウザであれば大抵サポートしているようです(2018年12月11日現在、iOS版Safariが未サポートなのはちょっと困りますね…)。
また、上記サイトには載っていませんが、Windows 10 build 17723からはMicrosoft Edgeもサポートしています。
DjangoでWebAuthnを導入してみる
では、実際にWebAuthn対応のDjangoアプリケーションを作ってみましょう。今回は、認証器としてYubico セキュリティキーを用意しました。
Pythonのバージョンは3.6系を使っています。
使用するPythonフレームワーク・ライブラリのバージョンは以下のとおりです。
djangorestframework・django-extensionsはDjango設定ファイルの変更が必要ですが、詳細は以下にリンクしたドキュメントのInstallationを参照してください。
- django==2.1.4
- djangorestframework==3.9.0
- django-extensions==2.1.4
- pyOpenSSL==18.0.0
- Werkzeug==0.14.1
- fido2==0.4.0
djangorestframeworkは登録・認証用のAPIを作成するために使います。
また、WebAuthnAPIはローカル開発であってもhttpsを使う必要があるため、django-extensionsの runserver_plus
コマンドを--cert-file
オプション付きで使います。pyOpenSSL・Werkzeugは runserver_plus
に必要なライブラリです。
前述の登録・認証処理を楽に書けるようにfido2も使います。
登録・認証周りのコードはusers
というDjangoアプリケーションに書いていきます。
モデル定義は以下のとおりです。
AUTH_USER_MODEL
に設定する User
モデルと、WebAuthn公開鍵を管理する WebAuthnPublicKey
モデルを定義しています。1ユーザーが認証器を複数持っている場合を考慮して、1対多の関係にしています。WebAuthnPublicKeyManager
に定義した credentials
メソッドの用途は後で説明するので、とりあえず読み飛ばしてください。
まず登録処理を実装します。サーバーサイドのコードは以下のとおりです。
WebAuthn認証ではサーバーとのデータのやり取りをCBOR形式で行う必要がありますが、Django REST frameworkでは未対応のため、専用のParser・Rendererを作っています。
RegisterBeginViewSet
(PublicKeyCredentialCreateOptionsを返すためのAPI用のビュー)では、前述の credentials
メソッドを呼んで、過去に同じ username
に紐づけた認証器を登録できないようにしています。
また、 views.py
の中でセッションに challenge
を入れていますが、デフォルトの SESSION_SERIALIZER
だと 「TypeError: Object of type ‘bytes’ is not JSON serializable」エラーになるため、django.contrib.sessions.serializers.PickleSerializer
を使うようにしています。
クライアントサイドのコードは以下のとおりです。
これで登録できるようになりました。./manage.py runserver_plus localhost:8000 --cert-file cert.crt
で開発サーバーを立ち上げて、WebAuthnをサポートするブラウザでhttps://localhost:8000/users/signup/ を開いてください。
「ユーザー名」・「表示名」を入力して「サインアップ」をクリックすると、認証器を要求するダイアログが表示されます。以下はMac版Chrome71.0.3578.80で動作確認した際の画面です。
USBポートにYubico セキュリティキーを刺すと鍵のアイコンが点滅します。そこを指で触ると登録完了です。
次に、認証処理を実装します。サーバーサイドのコードは以下のとおりです(Parser・Renderer・Permissionは前述と同じコードを使っているので省略)。
views.py
の最後の行 reverse('accounts:profile')
に該当するビュー・テンプレートの定義は省略します。ログインできたことが分かるように、テンプレートの中で {{ request.user.display_name }}
を書いておけば内容は何でもいいです。
WebAuthnに対応した認証バックエンドとして users/backends.py
に WebAuthnBackend
を作成しています。 settings.py
には以下のように書いておく必要があります。
AUTHENTICATION_BACKENDS = (
'users.backends.WebAuthnBackend',
)
クライアントサイドのコードは以下のとおりです。
「ユーザー名」を入力して「サインイン」をクリックすると、登録と同じく認証器を要求するダイアログが表示されます。
認証が通ると request.user
でユーザー情報を参照できるようになります。
これで、WebAuthn対応のDjangoアプリケーションに最低限必要な機能の実装はできました。実際にサービスで使うには、以下の機能についても実装しておいたほうがいいでしょう。
- WebAuthn非サポート環境でも使える認証方法の提供
- 認証器を紛失した際に使う認証器再登録のフローの提供
また、公開鍵は生成時のドメインでしか使えません。
もし、公開中のウェブサービスのドメインを変更すると、旧ドメインのユーザーは突然ログインできなくなってしまいます。旧ドメインで登録した公開鍵がある場合に再登録を促すフローも用意しておくと、より親切なサービスになるでしょう。
参考資料
- FIDO Alliance and W3C Achieve Major Standards Milestone in
Global Effort Towards Simpler, Stronger Authentication on the
Web - Web Authentication: An API for accessing Public Key Credentials Level 1
- Web Authentication API | MDN
- マイクロソフト、WebAuthnをEdgeに実装。パスワード不要、生体認証やPINでWebサイトへログイン可能に。2018年秋のWindows 10アップデートで - Publickey
- WEB+DB PRESS Vol.107 112ページ「どんとこい! フロントエンド開発 第9回 Web Authentication API ブラウザから生体認証を行うしくみ」
- Yubico/python-fido2: Provides library functionality for FIDO 2.0, including communication with a device over USB.(本記事のコードは
examples
以下のコードを参考にして作成した)