PythonのAuthlibを使ったOAuth2クライアント認証

2023-08-27(Sun) Python

PythonでOAuth2認証ライブラリと言ったら、多分outhlibがよく使わていると思われます。

私も何度もお世話になってましたが、最近仕事で自動化をしていたときにoauthlib(request-oauthlib)ではうまくいかないAPIがありまして、別のOAuth認証ライブラリのauthlibを使ってみることにしました。

Authlib 1.2.1 documentation

※当人はOAuth2認証の知見は素人レベルなので、間違いがあったら教えてもらえたら嬉しいです🙏

TL;DR

コードを見た方が早い人は、こちらのリポジトリがサンプルコードです。

https://github.com/hrsano645/python-authlib-example

うまくいかないAPIと出会う

本業で利用しているMFクラウド請求書のAPIが発端です。自動化をする上でAPIは非常に便利に利用できます。

そのAPIのバージョンアップをすることになったようで、新しい方法でAPIの認証を用意することに。

マネーフォワード クラウド請求書API スタートアップガイド(v3ご利用者さま向け) | マネーフォワード クラウド請求書サポート

v3になったところで、クライアントの認証方式がclient_secret_basicclient_secret_postが選べるようになったようです。

ここでいうこの2つの認証方式はOAuth2クライアント認証のことをさしているようです。

ざっくりいうと、client_seret_postはリクエストパラメーター(URIに入れるパラメーター)でクライアントIDとクライアントシークレットを送っています。

https://qiita.com/TakahikoKawasaki/items/63ed4a9d8d6e5109e401#13-client_secret_post

client_secret_basicはクライアントIDとクライアントシークレットをbase64でエンコードした上でhttpヘッダーへ追加する、いわゆるHTTP Basic認証を使っている方法です。

https://qiita.com/TakahikoKawasaki/items/63ed4a9d8d6e5109e401#14-client_secret_basic

今回のAPIではこの2方式が選べるのですが、Pythonのoauthlib(2023-08-27現在のstable版)ではこのクライアントの種類をうまく識別できない模様です。client_secret_postがAPIの前バージョンの方式だったようなので設定してコードを動かしたらエラーで動作せず。

Traceback (most recent call last):
  [oauthlibのOAuth2Client.fetch_tokenを動かした時]
  File "/Users/hiroshi/Documents/workspace/bproj1/.venv/lib/python3.11/site-packages/requests_oauthlib/OAuth2_session.py", line 366, in fetch_token
    self._client.parse_request_body_response(r.text, scope=self.scope)
  File "/Users/hiroshi/Documents/workspace/bproj1/.venv/lib/python3.11/site-packages/oauthlib/OAuth2/rfc6749/clients/base.py", line 427, in parse_request_body_response
    self.token = parse_token_response(body, scope=scope)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/hiroshi/Documents/workspace/bproj1/.venv/lib/python3.11/site-packages/oauthlib/OAuth2/rfc6749/parameters.py", line 441, in parse_token_response
    validate_token_parameters(params)
  File "/Users/hiroshi/Documents/workspace/bproj1/.venv/lib/python3.11/site-packages/oauthlib/OAuth2/rfc6749/parameters.py", line 448, in validate_token_parameters
    raise_from_error(params.get('error'), params)
  File "/Users/hiroshi/Documents/workspace/bproj1/.venv/lib/python3.11/site-packages/oauthlib/OAuth2/rfc6749/errors.py", line 399, in raise_from_error
    raise cls(**kwargs)
oauthlib.OAuth2.rfc6749.errors.InvalidClientError: (invalid_client) [A157304] The client authentication method is 'client_secret_post' but the request does not include a client secret.

どうやらサーバー側からクライアント認証の方式がそのとおりになっていないとして弾かれているようです。

この認証のコードはこちらを参考にしています。

https://requests-oauthlib.readthedocs.io/en/latest/examples/google.html

※oauthlibのクライアントを作るときに適切なパラメーターを入れたら動くかもしれません。検証不足ではあります。MFクラウド請求書v3のガイドを見た限りだと、CURLを使ったガイドでは 似たような方法でした (修正:2023-08-27)BASIC認証のようでした。

https://biz.moneyforward.com/support/invoice/guide/api-guide/a04.html#:~:text=POST%E3%80%81PUT%E3%80%81DELETE)-,2.%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E3%81%AE%E7%99%BA%E8%A1%8C,-%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E4%BD%9C%E6%88%90

※oauthlibはclient_secret_basicを扱うっぽいですが、(追記:2023-08-27)実際に認証とトークン取得まではうまくいくものの、リフレッシュトークンでトークン更新を行おうとすると上記と似たようなエラーで弾かれてしまってました。

Authlibを使う

ここで動かないとなると、本業が何も進まなくなって影響が大きく困ってましたが、PythonのOAuth認証向けのライブラリはいくつか種類があるようで、別のものを使ってみようと調べてみるといくつか上がっています。

OAuth Libraries for Python

ここにあるAuthlibが利用できそうです。ドキュメント上にクライアント認証のタイプについての言及もあるので、サポートは良さそうです。(AuthlibはOSS版と有償版があります)

クライアント

https://docs.authlib.org/en/latest/client/oauth2.html#client-authentication

requestsとの統合もされていて、 requests-oauthlibの代替できるようなクラスの作りにもなっています(が渡すパラメーター名や操作方法に違いがあるので注意が必要)

あとはサンプルコードにて

Authlibで実際にサンプル的なコードを作ってみました。実際に利用しているコードとほとんど同じもので、

https://github.com/hrsano645/python-authlib-example

MFクラウド請求書APIでは、デフォルトはclient_secret_basicなので、これに対応したものにしています。

Authlibのドキュメントでは、リフレッシュトークンの扱いとトークン自体の保存についてはあまり言及がなかったので苦労しましたが、トークンを適切に保存して、再利用するように作れば問題なく扱えるようです。

まとめ

PythonのOAuth認証ライブラリのAuthlibを使ってみた話でした。日本語圏ではあまり記事になっていなかったので、参考になりましたらと思います。