OAuth 2.0をrequests_mockでテストしてみた

こんにちは。AWS事業本部プロダクトグループのmuroです。

OAuth 2.0でWeb APIを利用するアプリケーションを開発する機会があり、requests_mockを使ってテストしてみました。

前提

  • アプリケーションはPythonで実装します。
  • このアプリケーションはサーバサイドで動作する機密クライアントです。
  • Web APIを利用するにはOAuth 2.0のクライアント・クレデンシャル・グラントで認可を受けます。
  • リフレッシュトークンには対応していません。

Pythonとライブラリのバージョン

サンプルAPI

次のような二つのWebAPIがあるとします。

  • POST /token
    アクセストークンを取得します。

      {
          "access_token": "dummy_access_token",
          "token_type": "bearer",
          "expires_in": 10800,
          "scope": "dummy_scope"
      }
      

  • GET /users/{user_id}
    ユーザー情報を取得します。

      {
          "userId": "1",
          "name": "muro",
          "mailAddress": "muro@example.com"
      }
      

サンプル実装

以下のコードをexample.pyとして保存します。なお、プロダクトコードではエンドポイント情報と認証資格情報はご自身の環境の情報に書き換えるとともに、外部パラメータ化するようにしてください。

from requests_oauthlib import OAuth2Session
from oauthlib.oauth2 import BackendApplicationClient


class OAuth2ConfidentialClientExample:

    # エンドポイント情報
    token_endpoint = 'https://example.com/token'
    user_endpoint = 'https://example.com/users/{user_id}'
    # 認証資格情報
    credentials = {
        'client_id': 'my_client_id',
        'client_secret': 'my_client_id_secret'
    }

    def init_session(self):
        """
        OAuth2セッションを開始します。
        """
        client = BackendApplicationClient(
            client_id=self.credentials['client_id'])
        self._sess = OAuth2Session(client=client)

    def fetch_token(self):
        """
        アクセストークンを取得します。
        """
        return self._sess.fetch_token(
            token_url=self.token_endpoint,
            client_id=self.credentials['client_id'],
            client_secret=self.credentials['client_secret']
        )

    def get_user(self, user_id):
        """
        ユーザー情報を取得します。
        """
        return self._sess.get(self.user_endpoint.format(user_id=user_id))

requests_mock を利用したテスト

それではサンプルコードを動かしてみましょう。 Pythonを起動し、サンプルコードとrequests_mockをインポートします。

$ python
>>> from example import OAuth2ConfidentialClientExample
>>> import requests_mock

1. トークンを取得してみる。

まずはダミーのトークンを用意します。

>>> dummy_token = {
...     'access_token': 'dummy_access_token',
...     'token_type': 'bearer',
...     'expires_in': 9999,
...     'scope': 'dummy_scope'
... }

OAuth 2.0セッションを開始します。

>>> target = OAuth2ConfidentialClientExample()
>>> target.init_session()

トークンを取得します。この時requests_mockを使って、トークンエンドポイントがダミートークンを返すようにします。

>>> with requests_mock.mock() as m:
...     m.post(target.token_endpoint, json=dummy_token)
...     target.fetch_token()
... 
{'access_token': 'dummy_access_token', 'token_type': 'bearer', 'expires_in': 9999, 'scope': ['dummy_scope'], 'expires_at': 1558495687.1986227}

トークンを取得できました!

2. ユーザー情報を取得してみる。

続いてユーザー情報を取得します。ここでもダミーのユーザーを用意します。

>>> dummy_user = {
...     'userId': '1',
...     'name': 'muro',
...     'mailAddress': 'muro@example.com'
... }

requests_mockを使って、ユーザーIDが"1"のダミーユーザーを返すようにします。

>>> with requests_mock.mock() as m:
...     input_user_id = '1'
...     m.get(target.user_endpoint.format(
...         user_id=input_user_id), json=dummy_user)
...     target.get_user(input_user_id).text
... 
'{"userId": "1", "name": "muro", "mailAddress": "muro@example.com"}' 

ユーザー情報を取得できました!

3. トークン有効期限切れになった時の動作を確認してみる。

さて、ユーザー情報を取得しようとした時にトークンの有効期限が切れていた場合はどのように動作するのでしょうか? トークンの有効期限を短く設定して試してみます。

まずはダミートークンの有効期限を1秒に設定します。

>>> dummy_token['expires_in']=1

先ほどと同じようにトークンを取得します。

>>> with requests_mock.mock() as m:
...     m.post(target.token_endpoint, json=dummy_token)
...     target.fetch_token()
... 
{'access_token': 'dummy_access_token', 'token_type': 'bearer', 'expires_in': 1, 'scope': ['dummy_scope'], 'expires_at': 1558485808.333808}

1秒間しか有効でないトークンが返ってきました。もうこの瞬間には無効になっているはずです。先ほどと同じようにユーザー情報を取得してみます。

>>> with requests_mock.mock() as m:
...     input_user_id = '1'
...     m.get(target.user_endpoint.format(
...         user_id=input_user_id), json=dummy_user)
...     target.get_user(input_user_id).text
... 
oauthlib.oauth2.rfc6749.errors.TokenExpiredError: (token_expired)

トークン有効期限切れエラーになりました!

まとめ

requests_oauthlibを利用することで、OAuth 2.0のクライアント・クレデンシャル・フローのクライアント実装をすっきりと書くことができました。またrequests_oauthlibのOAuth2Sessionがどのように動作するのかをrequests_mockを使ってテストすることができました。

実際の開発では、認可サーバ・リソースサーバのそれぞれの仕様を調べた上で、実サーバと通信して動作を確認しなければなりませんが、トークンの有効期限切れや認可エラーはそう簡単には試せないことが多いでしょう。requests_mockのようなモックライブラリを使って、テストしにくい環境を簡単に用意できると、エラー処理の実装・テストが捗りますね!

以上です。