Authlib で OAuth 2.0 の仕組みや使い方を学ぶ

Web サービスで認証するにあたり、OAuth 2.0 という言葉をよく耳にするようになりました。

一方で、OAuth 2.0 を体系立てて説明している記事が少なかったり、実際にどうやって実装すれば良いのかわからなかったりしました。

そこで本記事では、以下の順で OAuth 2.0 を理解し、実装できるように執筆しました。

  1. OAuth 2.0 の仕様を紹介
  2. Authlib ライブラリを利用し、OAuth 2.0 サーバーとクライアントを実装
SSO 関連記事

OAuth 2.0 とは

OAuth 2.0 OAuth 2.0とは、ユーザーに代わって、アプリがアクセストークンを取得する仕様です。
アクセストークン アクセストークンとは、サーバーのリソースにアクセスするCredential(アクセス許可証) です

OAuth 2.0 のフローとロール

OAuth 2.0 には 4つのロール (登場人物) が存在します。

ロール説明
Resource Ownerユーザー
ClinetResource Owner の代わりにリソースにアクセスするアプリ
Authorization ServerResource Owner を認証し、Client にアクセストークンを発行
※Client 側で認証するには OIDC が必要
Resource Serverリソースを保有するサーバー。アクセストークンを検証して応答

OAuth 2.0 は以下のフローでアクセストークンを取得します。

token = アクセストークン
https://docs.authlib.org/en/latest/oauth/2/intro.html#intro-oauth2
1. Login 画面の例
2. Resource owner grant 画面の例

OAuth defines four roles:

・resource owner
・resource server
・client
・authorization server

The interaction between the authorization server and resource server is beyond the scope of this specification.

https://tools.ietf.org/html/rfc6749#section-1.1

以降では、上記の図を常に頭の片隅に置きながら読み進めてください。

Grant Type (アクセストークンを取得する種類)

Grant Type Grant Type とは、アクセストークンを取得する方法の種類です。

Grant Type は、以下の OAuth 2.0 のフローの①、②の動作を決定します。

https://docs.authlib.org/en/latest/oauth/2/intro.html#intro-oauth2

Grant Type には以下の4つのタイプがあります。

Grant Type用途・備考
Client Credentials Grant一般的なリソースにアクセスする場合
Authorization Code Grantユーザーリソースにアクセスする場合
ユーザーが都度同意(ログイン)したい場合
Resource Owner Password Credentials Grant使用禁止/Client にパスワードが渡るため
Authorization Code Grant でいい
Implicit Grant非推奨/アクセストークンが URL に露出するため
SPA(Single Page Application で利用)
JWT (RFC 7523 で追加)推奨
ユーザーリソースにアクセスする場合
管理者が事前に同意(都度ログインしたくない)

This specification defines four grant types -- authorization code, implicit, resource owner password credentials, and client credentials -- as well as an extensibility mechanism for defining additional types.

https://datatracker.ietf.org/doc/html/rfc6749

Client Credentials Grant

Client Credentials Grant Client Credentials Grant とは、Client が Credentials でアクセストークンを取得する方法です。つまり、ユーザーはパスワードを入力しません。

事前準備

認可サーバーにクライアントを登録すると、次の Credentials を発行されます。

  • client_id:クライアント名
  • client_secret:パスワード

Client Credentials Grant でアクセストークンを取得する手順

https://datatracker.ietf.org/doc/html/rfc6749#section-4.4
Client Authentication には Credentials を使用
  1. Client は Credentials で、認可サーバーにアクセストークンをリクエスト (A)
  2. 認可サーバーはアクセストークンを返す (B)

Authorization Code Grant

Authorization Code Grant Authorization Code Grant とは、Authorization Code でアクセストークンを取得する方法です。

事前準備

認可サーバーにクライアント (redirect_uri) を登録すると、次の Credentials を発行されます。

  • client_id:クライアント名
  • client_secret:パスワード

Authorization Code Grant でアクセストークンを取得する手順

https://datatracker.ietf.org/doc/html/rfc6749#section-4.1
- Client Identifier = どの Client か
- Redirection URI = Client の callback URI を指定 (C でクライアントにリダイレクトさせる)
- User authenticates = ユーザー名/パスワードなどで認証
  1. ユーザーが Client の「ログインページ」をクリックすると認可サーバーのログイン画面に遷移
    1. Client が URL に client_id と redirect_uri を含めてブラウザに 302 リダイレクト(A)
    2. ブラウザが認可サーバーにリダイレクト (A: Client Identifier & Redirection URL)
    3. ユーザーにリダイレクト先のログイン画面を表示 (B)
  2. ユーザーがユーザー名/パスワードなどで認証 (B: User authenticates)
  3. 認証に成功すると認可サーバーは Authorization Code を返す (C)
  4. アプリは認可サーバーに Authorization Code を渡し、アクセストークンをリクエスト (D)
  5. 認可サーバーはアクセストークンを返す (E)

C の時点でアクセストークンを直接貰わず、Authorization Code を貰うのはセキュリティの都合
(クエリパラメータはログに残ったり、Referer で外部に送られたり。そのため、client_secret + Authorization Code をアクセストークンの引換券とする。)

Resource Owner Password Credentials Grant

Resource Owner Password Credentials Grant Resource Owner Password Credentials Grant とは、クライアントがユーザーの入力したパスワードでアクセストークンを取得する方法です。

Authorization Code Grant はブラウザ経由なのに対して、Resource Owner Password Credentials Grant はクライアント経由でパスワードを認可サーバーに送ります。(クライアントのメモリにパスワードが乗るのでセキュリティ的に使用禁止になった)

Resource Owner Password Credentials Grant でアクセストークンを取得する手順は以下のとおりです。

https://datatracker.ietf.org/doc/html/rfc6749#section-4.3
  1. ユーザーが Password Credentials (パスワード) を入力する (A)
  2. アプリケーションは、Authorization Server に Password Credentials を渡し、アクセストークンをリクエスト (B)
  3. Authorization Server は、アクセストークンを返す (C)

Implicit Grant

非推奨となる見込みのため、省略します。

The implicit grant (response type "token") and other response types causing the authorization server to issue access tokens in the authorization response are vulnerable to access token leakage and access token replay as described in Section 4.1, Section 4.2, Section 4.3, and Section 4.6.

https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics-14

詳細を知りたい方はこのあたりを見てください。

RFC 6749 - The OAuth 2.0 Authorization Framework 日本語訳
RFC 6749は、OAuth 2.0認証フレームワークに関する文書で、第三者アプリケーションがユーザーの代わりにリソースサーバー上のリソースにアクセスするための認証と承認のプロセスを定義しています。このフレームワークの目的は、安全にユーザ...

JWT Bearer Grant

JWT Bearer Grant JWT Bearer Grant とは、JWT でアクセストークンを取得する方法です。

事前準備

  1. 秘密鍵と公開鍵を生成
  2. 公開鍵を認可サーバーに登録
  3. 公開鍵に対応する client_id が認可サーバーから発行
  4. 秘密鍵で JWT署名
    • JWT = [ヘッダー][Claim][署名] のフォーマット
    • Claim = iss(client_id), aud(認可サーバーのURL), sub(ユーザー名), exp(JWT の有効期限)

JWT Bearer Grant でアクセストークンを取得する手順

       +---------+                                  +---------------+
       |         |                                  |               |
       |         |>---(A)-- JWT Assertion --------->| Authorization |
       |  Client |          (signed with            |     Server    |
       |  アプリ  |           private key)           |  認可サーバー   |
       |         |                                  |               |
       |         |<---(B)-- Access Token -----------|               |
       |         |                                  |               |
       +---------+                                  +---------------+
  • アクセストークンの有効期限が切れると、JWT で再発行
  • JWT の有効期限が切れると、クライアントでJWT を再生成

OAuth 2.0 サーバーの実装

以下の GitHub のコードを利用して、OAuth 2.0 サーバーを実装します。

GitHub - authlib/example-oauth2-server: Example for OAuth 2 Server for Authlib.
Example for OAuth 2 Server for Authlib. Contribute to authlib/example-oauth2-server development by creating an account o...

なお、上記のコードでは Flask を利用してます。Flask については以下の記事をご覧ください。

実装する OAuth 2.0 サーバーは以下のとおりです。

  • Authorization Server:
    • http://127.0.0.1:5000/oauth/token (アクセストークン発行)
    • http://127.0.0.1:5000/oauth/authorize (Authorization Code 発行)
  • Resource Server:
https://docs.authlib.org/en/latest/oauth/2/intro.html#intro-oauth2

FlaskでOAuth 2.0 認可サーバー

git clone https://github.com/authlib/example-oauth2-server.git
cd example-oauth2-server/
pip3 install -r requirements.txt
export AUTHLIB_INSECURE_TRANSPORT=1
flask run -h 0.0.0.0 -p 5000

これで OAuth 2.0 サーバーの構築は完了です。

Authorization Server に Client を登録

Authorization ServerClient (アプリケーション) を登録します。

まずは http://127.0.0.1:5000/ にアクセスし、Resource Owner (ログインユーザー) を登録します。

今回は「test」ユーザーを作成し、[Login/Signup] をクリック

次に [Create Client] をクリックして、Client (アプリケーション) を登録します。

入力する情報は以下のとおりです。

https://github.com/authlib/example-oauth2-server

[Submit] をクリックすると、 作成した Client の情報が表示されます。

export client_secret='<上記で取得した client_secret の値>'
export client_id='<上記取得した client_id の値>'
export username='test'

OAuth 2.0 クライアントの実装

今回は次の2つの方法で OAuth 2.0 クライアントを実装します。

https://docs.authlib.org/en/latest/oauth/2/intro.html#intro-oauth2

curl で OAuth 2.0 を実装

curl を利用して、以下の2種類の Grant Type でアクセストークンを取得します。

Resource Owner Password Credentials Grant を利用

curl + Resource Owner Password Credentials Grant でアクセストークンを取得して、REST API を実行します。

https://datatracker.ietf.org/doc/html/rfc6749#section-4.3
Resource Owner = ユーザー、Client = アプリケーション
OAuth 2.0 のフローの①、②に相当

以降では、上記の図のフローに従って実際にクライアントの curl を操作します。

  • 上図 (A) の Resource Owner は、curl のコマンドを入力するあなた自身です。
  • 上図 (C) は、curl のレスポンスです。
curl -u ${client_id}:${client_secret} -XPOST http://127.0.0.1:5000/oauth/token -F grant_type=password -F username=${username} -F password=valid -F scope=profile
{"access_token": "afsdgabcdefghijklmn", "expires_in": 864000, "refresh_token": "fagshabcdefghijklmn", "scope": "profile", "token_type": "Bearer"}

curl + Resource Owner Password Credentials Grant でアクセストークンを取得できました。

アクセストークンを利用して REST API を叩く
export access_token='<上記で取得した access_token の値>'
curl -H "Authorization: Bearer ${access_token}" http://127.0.0.1:5000/api/me
{"id":1,"username":"test"}

アクセストークンを利用して、/api/me API が実行できたことが確認できます。

補足:アクセストークン無い時
curl http://127.0.0.1:5000/api/me
{"error": "missing_authorization", "error_description": "Missing \"Authorization\" in headers."}

REST API の実行を許可しないことが確認できます。

Authorization Code Grant を利用

curl + Authorization Code Grant で、アクセストークンを取得して API を実行します。

https://datatracker.ietf.org/doc/html/rfc6749#section-4.1
Resource Owner = ユーザー、User-Agent = ブラウザ、Client = アプリケーション

1. Authorization Server (http://127.0.0.1:5000/oauth/authorize?response_type=code&client_id=${client_id}&scope=profile) にアクセス (A)

2. (User-Agent (= ブラウザ) が自動で) Authorization Server のページを表示 (B)

今回はパスワードを必要としない実装です

3. Consent にチェックを入れ、Submit を押下し、ブラウザが認可サーバーにリクエスト (B)

4. (User-Agent (= ブラウザ) が自動で) Authorization code を表示 (C)

赤線で囲った文字列が Authorization code
code=<上記で取得した Authorization code の値>
curl -u ${client_id}:${client_secret} -XPOST http://127.0.0.1:5000/oauth/token -F grant_type=authorization_code -F scope=profile -F code=${code}
{"access_token": "123456789ABCDEFGHIJK", "expires_in": 864000, "scope": "profile", "token_type": "Bearer"}
アクセストークンを利用して REST API を叩く
export access_token=<上記で取得した access_token の値>
curl -H "Authorization: Bearer ${access_token}" http://127.0.0.1:5000/api/me
{"id":1,"username":"test"}

Requests (Python) で OAuth 2.0 を実装

Python の Requests ライブラリを利用して、Resource Owner Password Credentials Grant の Grant Type でアクセストークンを取得します。

https://datatracker.ietf.org/doc/html/rfc6749#section-4.3
Resource Owner = ユーザー、Client = アプリケーション
OAuth 2.0 のフローの①、②に相当

Resource Owner Password Credentials Grant の実装

pip3 install requests
vim password_flow_requests.py
from authlib.integrations.requests_client import OAuth2Auth
from requests.auth import HTTPBasicAuth
import requests
import json
import os

###認可サーバーからアクセストークンを取得するために、Basic (パスワード) 認証を行う
#アドレスとポート番号に注意
authorization_endpoint = 'http://localhost:5000/oauth/token' #Authorization Server
basic_auth = HTTPBasicAuth(os.environ['client_id'], os.environ['client_secret']) #環境変数をセットしてください。パスワード認証に必要な情報

form_data = { #Authorization Server に渡す情報の設定
    'grant_type': 'password',
    'username': os.environ['username'], #環境変数をセットしてください。
    'password': 'valid',
    'scope': 'profile'
}

response_authorization = requests.post(authorization_endpoint, auth=basic_auth, data=form_data) #Authorization Server から Bearer 用のアクセストークンを取得
json = json.loads(response_authorization.text) #JSON 文字列を dict に
access_token = json['access_token'] #アクセストークンを取得

###Resource Server の /api/me API を叩くために、アクセストークンを利用して、Bearer 認可を行う
#アドレスとポート番号に注意
resource_server_api = 'http://localhost:5000/api/me' #自分の id とユーザー名を表示する API
token = {'token_type': 'bearer', 'access_token': access_token} #アクセストークンをセット
auth = OAuth2Auth(token) #認可リクエストの作成
response_api = requests.get(resource_server_api, auth=auth) #bearer トークンを使って API を叩く

print (response_api.text) # API の結果
python3 password_flow_requests.py
{"id":1,"username":"test"}

リソースサーバーの /api/me API を叩き、結果を取得していることが確認できます。

OAuth 2.0 のフローと Python のソースコードの対応

OAuth 2.0 のフローと、Python のソースコードの対応関係は以下のとおりです。

https://docs.authlib.org/en/latest/oauth/2/intro.html#intro-oauth2
  • ①:requests.post(authorization_endpoint, auth=basic_auth, data=form_data)
  • ②:response_authorization
  • ③:requests.get(api_url, auth=auth)
  • ④:response_api

Resource Owner Password Credentials Grant と Python の対応

Resource Owner Password Credentials Grant と Python ソースコードの対応関係は以下のとおりです。

https://datatracker.ietf.org/doc/html/rfc6749#section-4.3
Resource Owner = ユーザー、Client = アプリケーション
OAuth 2.0 のフローの①、②に相当
  • (A):${client_id}:${client_secret} をユーザーが入力している時
  • (B):requests.post(authorization_endpoint, auth=basic_auth, data=form_data)
  • (C):response_authorization

関連記事

SSO (シングルサインオン) 入門記事の続きは以下のとおりです。


参考資料

https://datatracker.ietf.org/doc/html/rfc6749

Introduce OAuth 2.0