アクセストークンに有効期限を設けるべきかどうか
OAuthプロバイダを提供することになったとして、アクセストークンに有効期限を設けるべきかどうかについて考えたい。OAuth 2.0の仕様にはアクセストークンの期限切れに関係する仕様が定義されているし、セキュリティをより強固にするためにアクセストークンは一定期間で期限切れにするべきだという主張があったと思う (確認していないので無いかもしれない)。しかしながら、例えばGitHub API v3ではアクセストークンに有効期限を設けていない。この投稿では、アクセストークンの有効期限に関係して起こり得る問題を取り上げる。
アクセストークンに有効期限を持たせておくとちょっと安全
アクセストークンが悪意のある第三者に漏洩してしまった場合、そのアクセストークンに認可されているあらゆる操作が実行可能になってしまうという問題がまず存在する。ここでもしアクセストークンに有効期限が存在していたとすれば、その操作が実行可能な期間が一定期間に限られることになる。例えば30日間で期限が切れてしまうアクセストークンが発行後15日目に盗まれたとして、その被害を被る可能性があるのは残り15日間だけである、という具合。盗んだところでどうせ数日間しか利用できないのであれば盗むのをやめておこうという気持ちが働くことを期待すると、予防にもなると言える。
有効期限が切れても再ログイン無しで再発行できる
ユーザにログインしてもらって30日間有効なアクセストークンを発行して、30日後にはまたユーザはログイン画面に戻されるのかと言うとそれは面倒すぎるし、実際そういう訳ではない。OAuth 2.0にはリフレッシュトークンというこの手間を解消するための仕組みが用意されている。
このリフレッシュトークンの仕組みに則って実装すると、アクセストークン発行時に同時にリフレッシュトークンも発行される。クライアントアプリはアクセストークンと共にこのリフレッシュトークンも記憶しておく。もしアクセストークンの期限が切れた場合、ユーザに再ログインしてもらう代わりに、リフレッシュトークン + アクセストークン発行に利用したクライアント用の秘密鍵をサーバに送ることで、新しいアクセストークンを発行できるようにしている。
リフレッシュトークンの仕組みもそこまで最高ではない
悪意のある盗人から見ると、アクセストークンを盗んでも有効期限が切れたら使えなくなるし、リフレッシュトークンは盗んでいないので更新することはできない。アクセストークンはAPIへのリクエストの中に毎度含める値なので盗みやすいが、リフレッシュトークンは通信に含まれる機会が少ないので盗みづらい。もし仮に運良くリフレッシュトークンを盗めたとしても、秘密鍵も盗まなければ意味が無い。
ところで、盗人にとって幸運なことに、Androidアプリなどの最近のクライアントアプリはツールを利用して解析すれば簡単にソースコードの中身を読み取ったり推測したりできる。もし秘密鍵を(例えばXMLや文字列定数なんかを利用して)平文でソースコードに埋め込んでいたとしたら、この秘密鍵は簡単に盗むことができる。もし暗号化されていたとしても、結局クライアントアプリが利用するときには復号しないといけないのだから、盗人が何らかの手段で秘密鍵を復元することは可能だろう。秘密鍵の含まれるデータをクライアントに配布してしまったことが運の尽きだったのだ。
有効期限を導入するととても複雑になる
概念としてアクセストークンが状態を持つので、その分複雑になることは避けられない。このアクセストークンは期限が切れているかもしれないし、それはAPIサーバにリクエストを送ってみるまで分からない。アクセストークンの有効期限が切れていた場合にリフレッシュトークンを利用して適切にアクセストークンを再発行する実装をしなければならないし、不具合があっては困るので期限切れのフローのアプリのテストもちゃんとしなければならない。ログイン周りで何か不具合があって原因が分からないときも(恐ろしい)、もしかしたらリフレッシュトークン周りの実装に不具合があるかもしれないということを疑ってかかる必要がある(そして大抵の場合は何か問題がある)。
有効期限の概念は複雑だが、社内APIであれば社内アプリしか使わないはずなので、社内の開発者が頑張って勉強して実装すれば良いことではある。しかしもし外部の開発者に公開するようなAPIだとしたら、その開発者達に有効期限に関して理解してもらわなければならないし、これは非常に骨が折れる。そもそも認証周りがそんなに複雑なAPIは誰も使ってくれないかもしれない。もし社内用APIとしてまあ外部には公開しないでしょうという気持ちで作っていたとしても、提携先の信頼できる企業から「ちょっとビジネス的に価値があるからAPIを利用させてくれない?」と言われるケースは容易に想像できるし、簡単にしておくに越したことはない。
アクセストークンのレコード数は増えやすい
アクセストークン発行用のAPIリクエストを送信された回数だけアクセストークンがDBに保存されることになる。もし特定のクライアントに対して大量のアクセストークンが生成されていた場合、そのクライアントを停止させれば (停止させて関係するアクセストークンを全て削除すれば) 対処できるが、例えばGitHubのAPIでは特定のクライアントに紐付かないアクセストークンというのも発行できるようになっている。GitHubのAPIではユーザごとに1時間に5000回までリクエストが送信できるので、ユーザからリクエストを送られ続ければどんどんDBが肥大化していくということになる。
アクセストークンに有効期限を持たせない場合、クライアントアプリの登録が削除されるか明示的に削除命令が発行されない限り、1度生成したアクセストークンを消すことはできない。そのため、APIのユーザがアクセストークンを発行する数だけDBにアクセストークンが溜まっていくということになる。
アクセストークンに有効期限を持たせた場合は消せるかというと、残念ながらそういうわけにもいかない。有効期限が切れたアクセストークンも前述の再発行のために利用される可能性があるため、結局保存しておく必要がある。勿論、期限切れのアクセストークンに頻繁にアクセスされることは考えにくいので保存効率は良いが検索性能の悪い場所に隔離しておくとか、アクセストークンのレコードのうち再発行に使う情報は一部だけなのでその部分だけ残しておくとか、そういった工夫は可能だが根本的な解決にはならないだろうと思われる。