もともと内部向けですが、誰かの役に立つかもしれないので公開します。(セキュリティ面から一部削除)図表などは時間ができたら追加していきます!
認可・検証の流れ
アカウント情報の作成(準備)
App ID
アカウントの認可を行う場合、当然ながら、基となる識別情報が必要になります(App ID
などと呼ばれていることが多いのでここでもそう呼びます)これを作成するルールは知り得る限り存在しませんが、ある意味簡単に識別できて、ある意味簡単に盗まれないことが必要です。例えば、以下に例示するようにそれぞれ設定していくと、あまり考えなくて済みます。
簡単に識別可能
IDがランダムな文字列だとユーザー(APIを利用している開発者)が管理するときに分けが分からなくなるので、それが App ID
だと分かるような識別子をつけておく
推測の困難性
IDがほぼ公開情報とはいえ、セキュリティの一端を担っていることもわすれないように、ある程度ランダムな文字列を生成させておく(メールアドレスとかでも良いけどAPIの認証に使う場合などは頻繁にネット上を行き来するため非推奨というか気持ちが悪い)
公開鍵と秘密鍵
アカウント情報の主キーが App ID
だとして、それを引き出すリクエストの送り主が本当にそのIDなのかを検証するのが公開鍵と秘密鍵になります。公開鍵は public key
や site key
などと呼ばれていることが多いのですが、これは読み取る気になれば簡単にウェブサイトのソースコードなどで読み取ることが可能な形で設定されるからです。この鍵があるおかげで、AJAXを用いたトークンのリクエストなどが可能になります。また逆に秘密鍵は公開鍵のような状態で読み取られないような鍵のことを指しています。
これらの鍵は、原則として推測されないものを生成して使用するようにします。例えば、乱数を利用してソルトを加えてからHS256でハッシュ化したものを提供したりします。ここからは弊社のアルゴリズム保護のため、ハッシュ値生成の詳細は省きますが、やり方は簡単です。なんならHTTP通信でやり取りできればハッシュ値でなくともよく、考えるのが面倒なのでハッシュ値を利用しているだけです。例えば、ギリシャ文字を知る人がいない環境では、ギリシャ語の単語を適当に並べるだけで暗号になります。
App IDと公開鍵
ここで、公開鍵と App ID
のどちらか一つがあればよさそうなものですが、それもある意味当たっています。ただ、公開鍵が盗まれてしまった場合、この二つを分けておけば、公開鍵の再発行だけで App ID
の変更はせずに盗まれた鍵情報を更新できます。なので、 App ID
と紐づいているであろう情報とのリンクに影響を与えずにセキュリティが保たれる仕組みになります。 語弊を恐れずに言えば、 App ID
は見られてもよい情報で、公開鍵は盗まれてもまだ何とかなる情報と言えます。
トークンの発行
認可サーバーに対して最初のアクセスを行う部分です。 GET
が多いと思うので GET
での処理を前提としますが、 POST
でも HEAD
でも良いです。(データの渡し方が変わります)
ここのステップでは、 App ID
と公開鍵を送信してもらい、それに基づいてアクセス権の有無を下調べします。ここで App ID
と公開鍵が一致しない場合ははじかれて、次の認可獲得に進めません。
App ID
と公開鍵が一致した場合、認可サーバーはそれを証明するためのチケット(アクセストークンと呼ばれることが多いのでここでもそれに倣います)をリクエストに対して応答します。
トークンによる認可
アクセストークンが得られた次のステップとして、認可獲得のプロセスに進みます。アクセストークンが付与されたのがオンラインでつながっているクライアント、平たく言うとウェブブラウザなどの場合、秘密鍵を持ち合わせていないはずです。(ローカル環境の場合以外は持ってたらアウト)なので、秘密鍵を持っているサーバー(Appサーバーとしておく)にアクセストークンを渡して認可してもらうリクエストを出します。Appサーバーには秘密鍵があるはずなので、受け取ったアクセストークンとIDと秘密鍵を添えて認可サーバーに認可のリクエストを出します。
Appサーバーから認可のリクエストを受け取った認証サーバーは、受け取った情報の整合性(App IDから本当にリクエストがさっきあって、いま受け取ったアクセストークンを本当に発行したかどうか)を確認して、その確認が取れたら認可情報を添えて応答します。
認可情報を受け取ることができたAppサーバーは、そのことをリクエストしてきたクライアントに伝え、それを受けたクライアントはCookieなどで保存(Webアプリの場合はこのプロセスをAppサーバー側で行うことが多い)して、認可を受けた権限で情報にアクセスを開始します。ちなみに認可を受ける(受けている)権限のことを界隈では scope
と呼んでいます。
Appサーバーと認可サーバー
Appサーバーと認可サーバーは同一のサーバーであることも多いですが、開発時ていると何が何だか分からなくなってくるので、ソースファイルは分けておいたほうが賢明です。また、設計・学習時も明確に区別するようにしてください。
リダイレクト( redirect_uri
)
通常、認可サーバーは認可情報の検証と同時に、予め決められたアドレス以外には認可情報を送信しない設定を実装することでセキュリティを確保することができます。この情報は、初回のアクセストークン送信を含めて適当なタイミングで redirect_uri
を指定して検証するか、認可サーバーであらかじめ決められた宛先以外にのみ返答するか、いずれかの方法で実装してください。
状態( state
)
例えば、初回アクセストークンリクエスト時のブラウザに付与されていたCSRFトークンなどを state
として最後まで変数に含めて保持させておくことで、最初のリクエストからの一貫性を検証することができます。状態変数はウェブアプリだとCSRFトークンが便利ですが、識別できれば何でも良いです。ただし、state
の確認は通常は認可サーバーの仕事ではなく、Appサーバーの仕事です。
認可情報のフォーマット
JWTというフォーマットで発行されることが多いです。これは、検証可能性や必要な情報が規格化されていて使いやすいのと、含める情報の自由度も高く、要するにあまり考えなくても済むためです。もしこれよりも好ましいフォーマットがあればそれを使っても大丈夫です。ここではJWTの想定で説明します。
認可情報(JWT)は情報本体(ヘッダーとペイロード)と、その情報を検証するためのハッシュ値(シグネチャ)の3つがドットで繋げられています。ここでJWTが便利なのは、ヘッダーに暗号アルゴリズム(HS256など)が含まれていて、その情報を素にしてヘッダーとペイロードが改ざんされていないかをシグネチャで検証できる点にあります。ただ、ヘッダーに暗号アルゴリズムがあったり、BASE64デコードで情報本体がだれでも見れてしまうので、その部分のリスクは認識しておいたほうが良いです。
認可情報は応答ヘッダーや本文を通して渡せば良いです、使いやすい形で渡してください。
認可後の処理
JWTの検証
JWTの検証はAppサーバーでヘッダーとペイロードからハッシュ値を計算してシグネチャと照合することで行われます。これは毎回ではなく必要な場合のみで構いませんが、通常、サーバーから新しい情報を引き出したり変更を与える操作(CRUD)を行う都度に検証を行うようにします。
リフレッシュトークン
JWTは有効期限が設定されていますが、認可サーバーはこの部分ではシグネチャでの検証を可能にしているだけで、有効期限の検証の仕事をしないので、Appサーバーが行うことになる点に注意してください。
有効期限を超えてアクセスしようとした場合、通常はアクセスを拒否することになるのですが、それでは不便なことも多くあります。なので、初回JWT発行時にリフレッシュトークンというものを発行しておいて、それを保持している限りは認可を継続するという処理が実装できます。
なぜそんな回りくどいことをと思うかもしれませんが、JWTはすでに発行されてしまった認可なのに対し、リフレッシュトークンはその認可が本当に有効期限内に正当に発行されたかどうかを検証するために利用されるトークンということになります。なので、リフレッシュトークンの発行方法と保存方法をうまく設計すれば、リフレッシュトークンがある限り期限を延長して認可を続ける処理が可能になります。
実装の注意点
認可サーバーはJWTの発行と同時にリフレッシュトークンを発行するのですが、例えばリフレッシュトークンをJWTのヘッダーやペイロードに含めてしまったらどうでしょう?答えはNGで、JWTのヘッダー・ペイロードは誰でも見ることができるので、不正が起こっても仕方がない状態になります。なので、JWTとリフレッシュトークンは分けてクライアントに渡してください。
次に検証方法ですが、Appサーバーは期限切れのJWTを発見したら、クライアントから受け取ったリフレッシュトークンを添えて期限の延長を申請することになります。この申請を行うかどうかはAppサーバーの設定次第ですが、いずれにしても期限延長のリクエストを受け取った認証サーバーは該当するJWTとリフレッシュトークンの整合性を検証することになります。この検証部分はまた弊社のセキュリティ部分に係るので詳細は伏せますが、概要は以下の通りです。
- リフレッシュトークン発行時にJWTと紐づけて認可サーバーに記憶
- 記憶した情報(例えばハッシュ値)を受け取ったリフレッシュトークンを使って検証
- 整合性が検証出来たら認可の延長許可(認可で渡す情報が更新されている可能性がある場合は、このときにJWTをリフレッシュして発行しても良い)
注意点
ハッシュ値の生成
例えばメールアドレスを使う場合など、定数を用いて生成する場合、元のデータからハッシュ値が再生されてしまいます。逆に、ハッシュ値が再生できないと検証ができないです。なので、秘匿したいハッシュ値は悪意のある攻撃者が再生できないか時間がかかるように生成する必要があります。
ハッシュ値を悪意のある攻撃者から守るようにするため、何かもう一つ他の暗号を潜ませておく必要があり、それは例えば固有の文字列や、SSL証明書のようなものを元データに繋げてハッシュ値を生成させることで解決します。 この場合、そのハッシュ値の保有者は生成過程を知っているため、その手順に従うことで目的のハッシュ値を再生することができますが、それを知らない場合は膨大な計算が必要になってきます。
これを踏まえ、適切な生成方法でハッシュ値を生成するように設計してセキュリティを確保するように心がけてください。
ペネトレーションテスト
時間が空いた時にペネトレーションテストを実施して検証してください。SSL化してCookie(セッション)情報も適切に管理していると思っていても、意外とインジェクションのような古典的な部分(入力時)に穴があったりします。