OAuth2.0のアクセストークンで保護されたWeb APIの自動テストは難しいと思った話

## はじめに

認可の仕組みであるOAuth2.0のAuthorization Code Flowで発行されたアクセストークンがないとアクセスできないWeb APIを開発した際に、そのAPIテストを自動化しようと考えた。ただ、結論から言うとこれは中々難しく、断念せざる負えないと結論付けたのだが、その結論に至るまでの備忘録を残しておこうと思う。

※筆者はOpenID Connect（OAuth2.0）の専門家ではなく、OpenID Connect（OAuth2.0）を利用する側の知識くらいしか持ち合わせていないため、内容に誤りがある可能性がある。その場合はご指摘いただけると幸甚です。

## OAuth2.0とは？

OAuth2.0と、それを利用したWeb APIの利用方法については、以下の記事を参照（以下の記事ではGoogleのWeb APIを認可を受けたクライアントから呼び出すという実装をやってみている）。

https://qiita.com/yuta-katayama-23/items/b074e85504fc05ac76dc

https://qiita.com/yuta-katayama-23/items/4214114a21c11ec4f934

※OAuthの仕組みでAPIを保護する必要があるのか？については[そもそも OAuth は必要なのか](https://qiita.com/TakahikoKawasaki/items/f2a0d25a4f05790b3baa#%E3%81%9D%E3%82%82%E3%81%9D%E3%82%82-oauth-%E3%81%AF%E5%BF%85%E8%A6%81%E3%81%AA%E3%81%AE%E3%81%8B)などを参照。

## 本題　OAuth2.0のアクセストークンが必要なAPIのAPIテスト自動化はできるか？

前提としてアクセストークンがなければAPIを呼び出せない。つまり、アクセストークンを取得する必要があるが、そのアクセストークンを発行する部分に大きな課題があると思っている。

以下、アクセストークンを発行するためにTryしてみたが、ダメだった（検討した）方法をいくつか列挙する。

- ROPC（Resource Owner Password Credentials Flow）を利用する
- Cypressなどのテストツールを利用し、認可フローを回す
- リフレッシュトークンフローを回す
- 思考実験的なアイディア：裏口作戦

### ROPC（Resource Owner Password Credentials Flow）を利用する

ROPCについての詳細は割愛するが、要はID・パスワードでアクセストークンを発行できるフローのこと（ちゃんとした説明は有識者の記事：[3. リソースオーナー・パスワード・クレデンシャルズフロー](https://qiita.com/TakahikoKawasaki/items/200951e5b5929f840a1f#3-%E3%83%AA%E3%82%BD%E3%83%BC%E3%82%B9%E3%82%AA%E3%83%BC%E3%83%8A%E3%83%BC%E3%83%91%E3%82%B9%E3%83%AF%E3%83%BC%E3%83%89%E3%82%AF%E3%83%AC%E3%83%87%E3%83%B3%E3%82%B7%E3%83%A3%E3%83%AB%E3%82%BA%E3%83%95%E3%83%AD%E3%83%BC)などを参照）。

このROPCであれば認可フロー（Authorization Code Flow）とは違い、トークンエンドポイントにリクエストを送り、アクセストークンを取得できるので、APIテストをすることを考えると理想的ではある。ただ、このROPCフローはクライアントに対してID・パスワードを渡すフローであり、OAuth2.0の本質であるID・パスワードを渡さない、とはずれてる。それもあってか、基本的には多くの認可サーバーではサポート外になっており、ROPCでアクセストークンを発行するのは実質不可能な状態にある…。

試しにGoogleの認可サーバーに対して以下のようなリクエストを送ってみたが、サポート対象外というエラーになった。

```console
$ curl -X POST -d 'grant_type=password&username=.....@gmail.com&password=.....&client_id=......apps.googleusercontent.com&client_secret=.....' https://oauth2.googleapis.com/token
{
  "error": "unsupported_grant_type",
  "error_description": "Invalid grant_type: password"
}
```

※上記について、わざわざトークンエンドポイントにリクエストを送らずとも、https://accounts.google.com/.well-known/openid-configuration （OpenID Connectのディスカバリ ドキュメント）を見れば、サポートされていないことがわかる。

※私は普段、Node.jsで開発をしており、そのNode.jsで認可サーバー（正確にはOpenID Connect）を構築できるライブラリ[oidc-provider](https://github.com/panva/node-oidc-provider)があるが、そのドキュメント内でもROPCについて以下のように書かれている通り、ROPCを利用すること自体に問題がある（[Password Grant Type, ROPC](https://github.com/panva/node-oidc-provider/blob/main/docs/README.md#password-grant-type-ropc)より引用）。

> If you need it today something's wrong!
> 
> - https://www.youtube.com/watch?v=qMtYaDmhnHU
> - https://www.youtube.com/watch?v=zuVuhl_Axbs
> 
> ROPC falls beyond the scope of what the library can do magically on it's own having only accountId and the claims, it does not ask for an interface necessary to find an account by a username nor by validating the password digest. Custom implementation using the provided registerGrantType API is simple enough if you absolutely need ROPC.

### Cypressなどのテストツールを利用し、認可フローを回す

これは素直に認可フローを回してアクセストークンを取得するという発想。認証の部分が必ず出てくるが、それはCypressやPuppeteerなどのテスト・ブラウザ操作ツールを利用して行う。

ただこの方法にも課題があり、例えばGoogleの場合だと認証時に以下の記事で書かれているようにエラー（セキュリティチェックのエラー）になり、アクセストークンを発行までたどり着けないということがある。

https://qiita.com/yuta-katayama-23/items/6e41021a9e0d2c394244

※自前で認可サーバーを実装している場合、上記のような問題が発生しないこともあるだろう。その場合はテスト用のクライアントを用意して、それで認可フローを回してアクセストークンを取得してAPIテストを流す…ということも可能かもしれない。

### リフレッシュトークンフローを回す

これは[5. リフレッシュトークンフロー](https://qiita.com/TakahikoKawasaki/items/200951e5b5929f840a1f#5-%E3%83%AA%E3%83%95%E3%83%AC%E3%83%83%E3%82%B7%E3%83%A5%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E3%83%95%E3%83%AD%E3%83%BC)に書かれている通りで、認可フローを回して取得できるリフレッシュトークンを利用してアクセストークンを取得するもの。

ただし、このリフレッシュトークン自体には期限があり、それを超えると無効になるのでCIでAPIテストを自動化しようとするときには、期限切れが問題になると思われ、APIテストを自動化する上での有効な解決策といえないのではないかと思う（リフレッシュトークンを定期的に変えるオペレーションが必要なので）。

※Cypressでも[Google Authentication](https://docs.cypress.io/guides/end-to-end-testing/google-authentication)とあるように、Googleの場合は認可フローを回すことができないので、リフレッシュトークンフローでアクセストークンを取得するという方法を取っていると思われる。

### 思考実験的なアイディア：裏口作戦

この方法はGoogleなどの認可サーバーであればそもそも不可能。自前で認可サーバーを実装している場合に限ってであるかつ、あくまで思考実験の域の話。

自前で実装している認可サーバであれば、いわゆるテストのためにアクセストークンを発行する裏口を用意する、というアイディアが出てきそうだし、実現可能かと言われれば可能だとも思う。だが、これはつまりRFCにある認可の仕組み以外の仕組みでアクセストークンを発行することができることを意味し、それが問題になると思われるので選択肢としては除外されると思う。

※裏口を作るくらいであれば、前の章で取り上げたRFCに明記されているROPC（Resource Owner Password Credentials Flow）を利用して、アクセストークンを発行するべきだろう。ただ、ROPCは前の章でも見たとおり、サポート対象外にしている認可サーバーが多い仕様であり、これを利用するか？という議論もあるだろう。仕様として許されているのかわからないが、限られたクライアントのみでROPCを有効にするなどができれば、テスト用のクライアントのみで通常のクライアントではできないROPCのフローでアクセストークンを発行し、APIテストを自動化するということもできるかもしれない。

※これまた議論がありそうかつ、思考実験的に思いついた方法だが、自前で認可サーバーを実装しているなら、[In-source testing](https://vitest.dev/guide/in-source.html)の考え方を応用して、本番ビルド用のコードには、例えばROPCや独自のアクセストークン発行プロセスのコードは含まれないようにし、APIテストを行う用のビルドの際はそれらのコードが残るようにする、みたいなこともできるかもしれない。

## まとめとして

今回のことは、CIでAPIテストを自動化したいな～というのをきっかけに考えてみたことだった。最初は普通にCypressとかでログインをしてしまえばできるのでは？なんて考えていたが、Googleの例のようにそんなことはなく、色々な壁があることが分かった。

一番単純だと思っているリフレッシュトークンを利用してアクセストークンを取得する方法も、リフレッシュトークンの期限が切れるまではCIでテストが成功するだろうが、期限切れになればテストは通らなくなるので実用面で考えると、選択肢からはずはざる負えないと思う。

裏を返せば、それだけちゃんとセキュアな仕組みだとも捉えられる気もした。