OAuth2.0 とは
アクセストークンの要求と発行を標準化したものがOAuth2.0です。そのためユーザ認証自体は担当範囲でないため、どの条件を満たすとアクセストークンを発行すべきなのか?またその手法については自身で検討する必要があります。
またOAuth2.0では4つの認可フローが定義されており、フローによって通信の流れが変化します。
OAuth 2.0 全フローの図解と動画 - Qiitaより引用
今回のDjangoのサンプルはその中でも「 インプリシットフロー」が使われています。(インプリシットフローは脆弱性が多数報告されているので使うべきではない)
脱線: OpenID Connectとは?
- OAuth2.0の上に実装された仕様
- ユーザやクライアントが「確かにその人である」ということを証明するIDトークンを発行する
- OAuthと同じだが発行するのがアクセストークンではなくIDトークンとなる
- IDトークンはデジタル署名によって認証される
こちらの記事に詳しく書かれていますが、OAuthはあくまでリソースオーナーの代理でクライアントに対して、リソースに対する権限をアクセストークン経由で付与するプロトコルです。そのため「クライアントがいまつかっているアクセストークンが本当にクライアントのものなのか?」については判断するロジックを規定していません。
そこで登場するのがOAuth2の上に実装されたOIDC(Open ID Connect)で、アクセストークンを発行するフローと同様の流れでクライアンド自体を世の中に証明するIDトークンを発行します。(実装としてOIDCプロバイダーと認可サーバを兼任するサーバが多いようです)
そしてクライアントはこのIDトークンを使ってリソースサーバにアクセスすることで、リソースサーバはそのIDトークンの発行元の公開鍵で中身をチェックすることでクライアントの正しさを確認することができるようになります。
OAuthの配備パターン
名前 | 配置方法 |
---|---|
自力で立てる | サーバ上に認可サーバを構築する |
IDaaS利用 | OktaやAuth0など |
IDaaS利用+自力 | IDaaS+自力で認可サーバを立てる (Authleteなど) |
DjangoでOAuthを試してみる
ありがたいことにここにまとまっているので試してみましょう。
環境構築
基本は手順通りにやれば良いのですが、私の環境でやらないといけなかったことを書いておきます。(python manage.py migrate
の前)
# Djangoのインストール $ pip3 install django
ちなみにこんなエラーがでました。
$ python3 manage.py migrate Traceback (most recent call last): File "manage.py", line 10, in main from django.core.management import execute_from_command_line ModuleNotFoundError: No module named 'django' The above exception was the direct cause of the following exception: Traceback (most recent call last): File "manage.py", line 21, in <module> main() File "manage.py", line 16, in main ) from exc ImportError: Couldn't import Django. Are you sure it's installed and available on your PYTHONPATH environment variable? Did you forget to activate a virtual environment?
インストールしたDjangoがPYTHONPATH
と異なるところにインストールされていたので、環境変数を追加しました。
# パスの確認 $ python3 -c "import sys; print(sys.path)" ['', '/usr/local/lib/python3.8/site-packages', '/Users/xxx/anaconda3/lib/python37.zip', '/Users/xxx/anaconda3/lib/python3.7', '/Users/xxx/anaconda3/lib/python3.7/lib-dynload', '/Users/xxx/anaconda3/lib/python3.7/site-packages', '/Users/xxx/anaconda3/lib/python3.7/site-packages/aeosa'] $ pip3 show django Name: Django Version: 3.2 Summary: A high-level Python Web framework that encourages rapid development and clean, pragmatic design. Home-page: https://www.djangoproject.com/ Author: Django Software Foundation Author-email: foundation@djangoproject.com License: BSD-3-Clause Location: /usr/local/lib/python3.8/site-packages <-これを追加する Requires: sqlparse, asgiref, pytz Required-by: # 環境変数の追加 $ export PYTHONPATH=/usr/local/lib/python3.8/site-packages
アクセストークンの発行
URL(http://localhost:8000/api/authorization?client_id=xxx&response_type=token) にアクセスするとこのような画面になります。アクセス先は認可サーバになっており、このアクセスのことを認可リクエストと呼びます。(今回はtoken
を指定しているのでインプリシットフローになります)
そして認可レスポンスがによって画面上に認可画面↓が表示されました。この画面はDjangoによって生成されたものでOAuth2.0の中で規定されているものではありません。
続いて先ほど作成したjohn/johnでログインしましょう。
するとWeb ブラウザが認可サーバーに対して HTTP リクエストを投げ、認可サーバにて処理が行われた後にアクセストークンが発行されてこの画面になります。(ブラウザ〜認可サーバ間で認可コードの発行が行われていますが、一切見えないですね)
通信フローを整理
ここでは以下のような通信フローによって認証・認可が行われています。
まずは、ブラウザにて入力したユーザ/パスワードを使ってDjango(8000でListen)にてユーザ認証をしています。その後、Authleteに対して一時的な認可コードの発行やアクセストークンの発行を依頼指定しています。
ちなみに認可コードフローの場合はもうちょっと複雑です。
Bearerとは?
Bearer(厳密にはBearer認証)とは、トークンを利用した認証・認可です。OAuth 2.0の中ではこれをHTTPプロトコルのAuthorizationヘッダに入れることでクライアント/サーバ間で認証を行っています。
access_token
がBearer認証で使われるトークンですね。expires_in
が86400秒なので丸一日はこのトークンで認証・認可ができるということです。
アクセストークンを使ってアクセスする
最後に発行したアクセストークンを使ってクライアント(ブラウザ)からリソースサーバにアクセスしましょう。
アクセスをするとリソースサーバがAuthletに対して問い合わせにいってアクセストークンに対する認可を行い、OKがでればリソース情報をブラウザに返却してくれます。
具体的な動きは先ほどの図にも書いていますが、リソースサーバがHTTPリクエストに含まれるアクセストークンをAuthleteにて検証してもらいOKであれば自身の保有するリソースをブラウザに返却する流れとなります。
curlを叩いてみる
サイトではACCESS_TOKENに{}
が入っていましたが不要です。あると動きません。
$ ACCESS_TOKEN=8CUk77S50rR65cmK9yPK-Z2KNQ_2LVG2lKrGLX3Pk_k $ curl -sS http://localhost:8001/api/time -H "Authorization: Bearer ${ACCESS_TOKEN}" { "year": 2021, "month": 4, "day": 8, "hour": 15, "minute": 16, "second": 3 }
もしトークンをつけないとこのようになります。
curl -v http://localhost:8001/api/time -H "Authorization: Bearer XXXX" * Trying 127.0.0.1... * TCP_NODELAY set * Connected to localhost (127.0.0.1) port 8001 (#0) > GET /api/time HTTP/1.1 > Host: localhost:8001 > User-Agent: curl/7.64.1 > Accept: */* > Authorization: Bearer XXXX > < HTTP/1.1 401 Unauthorized < Date: Thu, 08 Apr 2021 15:17:44 GMT < Server: WSGIServer/0.2 CPython/3.7.3 < Content-Type: text/html; charset=utf-8 < Cache-Control: no-store < Pragma: no-cache < WWW-Authenticate: Bearer error="invalid_token",error_description="[A057302] The access token does not exist.",error_uri="https://docs.authlete.com/#A057302" < X-Frame-Options: DENY < Content-Length: 0 < X-Content-Type-Options: nosniff < Referrer-Policy: same-origin < * Connection #0 to host localhost left intact * Closing connection 0
Authleteでトークンがないよーといっていますね。
< WWW-Authenticate: Bearer error="invalid_token",error_description="[A057302] The access token does not exist.",error_uri="https://docs.authlete.com/#A057302"