フラミナル

考え方や調べたことを書き殴ります。IT技術系記事多め

DjangoとAuthleteを使ってOAuth/OIDCを理解する

OAuth2.0 とは

f:id:lirlia:20210409012813p:plain

アクセストークンの要求と発行を標準化したものがOAuth2.0です。そのためユーザ認証自体は担当範囲でないため、どの条件を満たすとアクセストークンを発行すべきなのか?またその手法については自身で検討する必要があります。

またOAuth2.0では4つの認可フローが定義されており、フローによって通信の流れが変化します。

f:id:lirlia:20210409015341p:plain

OAuth 2.0 全フローの図解と動画 - Qiitaより引用

今回のDjangoのサンプルはその中でも「 インプリシットフロー」が使われています。(インプリシットフローは脆弱性が多数報告されているので使うべきではない)

脱線: OpenID Connectとは?

  • OAuth2.0の上に実装された仕様
  • ユーザやクライアントが「確かにその人である」ということを証明するIDトークンを発行する
  • OAuthと同じだが発行するのがアクセストークンではなくIDトークンとなる
  • IDトークンはデジタル署名によって認証される

こちらの記事に詳しく書かれていますが、OAuthはあくまでリソースオーナーの代理でクライアントに対して、リソースに対する権限をアクセストークン経由で付与するプロトコルです。そのため「クライアントがいまつかっているアクセストークンが本当にクライアントのものなのか?」については判断するロジックを規定していません。

ritou.hatenablog.com

そこで登場するのがOAuth2の上に実装されたOIDC(Open ID Connect)で、アクセストークンを発行するフローと同様の流れでクライアンド自体を世の中に証明するIDトークンを発行します。(実装としてOIDCプロバイダーと認可サーバを兼任するサーバが多いようです)

そしてクライアントはこのIDトークンを使ってリソースサーバにアクセスすることで、リソースサーバはそのIDトークンの発行元の公開鍵で中身をチェックすることでクライアントの正しさを確認することができるようになります。

OAuthの配備パターン

名前 配置方法
自力で立てる サーバ上に認可サーバを構築する
IDaaS利用 OktaやAuth0など
IDaaS利用+自力 IDaaS+自力で認可サーバを立てる (Authleteなど)

DjangoでOAuthを試してみる

ありがたいことにここにまとまっているので試してみましょう。

qiita.com

環境構築

基本は手順通りにやれば良いのですが、私の環境でやらないといけなかったことを書いておきます。(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の中で規定されているものではありません。

f:id:lirlia:20210409000749p:plain


続いて先ほど作成したjohn/johnでログインしましょう。

するとWeb ブラウザが認可サーバーに対して HTTP リクエストを投げ、認可サーバにて処理が行われた後にアクセストークンが発行されてこの画面になります。(ブラウザ〜認可サーバ間で認可コードの発行が行われていますが、一切見えないですね)

f:id:lirlia:20210409000912p:plain

通信フローを整理

ここでは以下のような通信フローによって認証・認可が行われています。

まずは、ブラウザにて入力したユーザ/パスワードを使って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"