認証とセキュリティ
認証・認可とか、認証周りの構成とか注意点について時間が経つとすぐ忘れてしまうので、備忘録かねて記事で残しておこうかと思います。
参考記事
認証と認可
まず認証周りの整理から。
認証と認可は別なものです。よく聞く OAuth2.0 は 3rd party 向けの認可の仕組みを定義したものであって、認証の仕組みではないのでこの差を理解しないまま流用すると誤った認証や攻撃対象となってしまうリスクを伴います。
認可とは
認可とは、あるリソースに対してアクセスを許可することです。あるリソースとは例えば Google photo の画像かもしれないし、Github のリポジトリのソースかもしれません。名前やメールアドレスなどの個人情報を含むものかもしれません。認可はこれらのリソースに対してリソースオーナーが許可することを指しています。
身近なもので言うと、切符なんかがよく例に出されます。駅に入るには、切符を持ってる人だけであってその人の身分の証明であったり、その切符を自分で買ったか人にもらったのかは関係ありません。この切符をアクセストークンに置き換えれば、システムにおける認可のイメージがしやすいかと思います。
3rd party のサービスがユーザーの許可を得て、ユーザーの Twitter に代理投稿するのも認可になります。ツイッターに投稿するのは本人もしくは本人の許可を得た 3rd party なサービスであって、必ずしも本人しかリソースにアクセスできるわけではないのです。
認証とは
一方で認証とは、通信してる相手が誰かを確認・証明するものです。パスワードや生体情報、SMS による認証など個人を確実に特定するためにいくつかの認証方法が存在します。似たようなフローを経て認可を行うことも多く、認証を持ってリソースへのアクセスを許可してたりするため、認証と認可は混在しがちです。英語表記も認証= Authentication と認可= Authorization で似てて略すとどちらも Auth となりそうで、日本人にとってはより混乱しやすい原因になってる気がします(少なくとも僕は一般的に Auth と呼んでるのがどっちかわからなくなりました)。
用語説明
名称 | 例 | 説明 |
---|---|---|
リソース | Google Photo の画像 | 保護されたリソース。 |
アクセストークン | - | リソースへのアクセスを許可するトークン。リクエストに付与して利用する。検証を挟まず安易に保存するとセキュリティホールになる。 |
ID トークン | - | 認証完了時に発行されるトークン。トークンの中身を検証して、成功したら ID とセッションを紐づけて利用したりする。 |
リソースオーナー | エンドユーザー | リソースの所有者。 |
リソースサーバー | Google Photo | リソースを保持・管理してるサーバー。アクセストークンなどを持ってアクセスが許可される。 |
クライアント(RP) | 画像加工の Web サービス | OAuth や Open ID Connect を利用し、サービスを提供する Web アプリケーションなど。Replying Party(RP)と呼ばれることもある。 |
認可サーバー | Google アカウントの認証サービス | アクセストークンの発行を担う。 |
idP | Google アカウントの認証サービス | ID トークンの発行を担う。 |
OAuth2.0
OAuth2.0は前述の通り、認可のための仕組みで、具体的にはアクセストークンというものを発行するための仕様です。RFC6749 で定義されています。時々聞く「OAuth 認証」と言うのは誤用の場合も多く、あまり区別せずに認証に OAuth を利用するとセキュリティーホールになってしまうので十分注意しましょう。
OAuth を安易に認証に利用した場合のリスクや攻撃については、多くの記事が存在するのでいくつか参考記事をリンクしておきます。これらの記事はちょっと長かったり難しかったりしますが、そもそも認証周りや攻撃はどう頑張っても難しいところなので何度も読んで理解するよう努めたいところです。
具体的にはこれらの記事にあるような脆弱性につながる可能性があります。後述の#参考にもいくつかまとめてあるので、そちらも見てみてください。
Open ID Connect
Open ID Connect(以下 OIDC) は OAuth2.0 を拡張した、認証のための仕様です。ユーザー自身の身分証明と同時に、一般的に利用されるようなプロフィール情報の取得までが定義されています。また、認証周りの攻撃対策に利用されるような検証すべき情報も取得できます。この検証すべき情報というのはよしなにやってくれるわけではないので、クライアント側で検証処理を実際に行わなければいけません。
3 種類の ID トークン発行フロー
OIDC では実際の検証を含むフローについても定義しており、以下の 3 種類の処理フローが定義されています。
- Authorization Code Flow(認証コードフロー)
- Implicit Flow(暗黙的フロー)
- Hybrid Flow(ハイブリットフロー)
どれも ID トークンを発行するのは同じですが、メリット・デメリットが存在するので、概要をまとめておきます。
どのフローを利用するかは、idP への Authoraization リクエストのresponse_type
に含まれる値によって決定されます。
response_type | Flow |
---|---|
code | Authorization Code Flow |
id_token | Implicit Flow |
id_token token | Implicit Flow |
code token | Hybrid Flow |
code id_token token | Hybrid Flow |
Authorization Code Flow
トークンはクライアント(サーバー)と idP のみでやりとりし、ブラウザに渡さない処理フローです。エンドユーザーとクライアント、idP は認可コードを共有することで一連の認証を行います。
state
パラメータを付与して#CSRF 攻撃対策をすることが推奨されています。
Implicit Flow
認可コードではなく直接 ID トークンやアクセストークンをやりとりする処理フローです。リプレイ攻撃対策のため、このフローではnonce
の検証が必須となります。
Hybrid Flow
1 度の認証で 2 つのトークンを発行することが可能なフローです。スコープの異なる 2 つのアクセストークンを発行したり、ID トークンを先に検証することで 2 つ目のアクセストークンのセキュリティレベルを高めたりする際に利用するようですが、勉強不足のため省略します。
ID トークンから取得できるもの
ID トークンから取得できるものを簡単に説明したものを以下に示します。さらに詳細が知りたい場合は、仕様書の定義を参照してください。
項目名 | 必須・非必須 | 説明 |
---|---|---|
iss | 必須 | トークンの発行者(= issuer)を表す URL。 |
sub | 必須 | クライアントが利用可能なユーザーの識別子。24400320 や AItOawmwtWwcT0k51BayewNvutrJUqsvl6qs7A4 のような文字列。 |
aud | 必須 | OAuth2.0 のクライアント ID。ID トークンが誰に向けたものかを表しており、これを検証することで別クライアント向けに発行されたトークンでないことを保証できる。 |
exp | 必須 | ID トークンの有効期限。 |
iat | 必須 | ID トークンの発行日時。 |
auth_time | リクエスト次第 | 認証が発生した時刻。max_age がリクエストに含まれている場合、この項目は必須。 |
nonce | リクエスト次第 | リクエストにnonce が含まれていた場合、そのままこの項目に格納される。クライアント側でこの項目を検証することでリプレイ攻撃を防ぐのに利用する。 |
acr | 非必須 |
認証コンテキスト と呼ばれる文字列が格納され、パスワード認証以上のものを行なったかどうかなどの認証レベルを表す。 |
amr | 非必須 | 認証手法を表す文字列。具体的な仕様については利実装者間で取り決めることとされている。 |
azp | 非必須 | ID トークンの発行を要望したクライアントと実際に利用するクライアントが異なる場合にこの項目が利用される。クライアント ID が格納されている。 |
ID トークンの検証
上記の情報を ID トークンから取得するには、JWT の検証と以下の検証を行う必要があります。
jwt 改竄の検証
jwt の検証は多くの場合ライブラリ側で対応されるので、大まかな流れのみ記載します。
ざっくり言うと、秘密鍵で再度署名して一致する=改竄されてないと言うことです。
- jwt は
.
を区切りとして、ヘッダー・ペイロード・署名の 3 つが含まれている - ヘッダーを base64 でデコードし、署名アルゴリズムの指定を取得する
- 指定されたアルゴリズムと秘密鍵を用いて、jwt に含まれるヘッダーとペイロードを署名する
- 手順 3 で作成した署名と jwt に含まれている署名が一致すれば検証は成功
iss, aud 検証
iss
は、ID トークンの issuer(発行者)を表す https な URL で、aud
はクライアント ID です。この 2 つが想定通りか検証することで、「誰が」「誰のために」作った ID トークンか確認できます。
これが想定通りでないということは、別なクライアントのために発行された ID トークンや第三者が生成した ID トークンである可能性があり、不正な認証として扱うべきです。
exp 検証
exp
は ID トークンの有効期限を表すため、この期限を超えて ID トークンを受け入れるべきではありません。
nonce 検証
リクエスト時にnonce
にユーザーのクライアントセッション値由来のハッシュ値を付与すると、ID トークンにそのnonce
が含まれます。ID トークンのnonce
をクライアント側でセッションから再度ハッシュ値生成したものと一致するか検証することで、「認証を求めたユーザー」と「ID トークンを受け取ったユーザー」が同一であることを保証できるので、リプレイ攻撃(#リプレイ攻撃を参照)対策になります。
この検証後、クライアントセッションは破棄する必要があります。
認証を保存する
ID トークンを検証し、認証が完了したら認証状態とアクセストークンを保存したいことが多いかと思います。この際の保存方法として、以下の 4 つについて考えてみます。
- ID トークンを Cookie に保存(非推奨)
- アクセストークンを local storage に保持(非推奨)
- アクセストークンを Cookie に保持(?)
- アクセストークンをセッションに保持(推奨)
ID トークンを Cookie に保存(非推奨)
ID トークンは認証を検証するためのものであって、認証状態を保存するためのものではありません。ID トークンのデータ量はそれなりに大きいため、保存には不向きです。また、ID トークンには個人情報を含められることが仕様でも明記されており、base64 で簡単にデコードできるので、Cookie を参照できるセキュリティホールがあった場合、個人情報を第三者が入手可能な状態になります。そのため、ID トークン自体を Cookie に直接値として保持すべきではありません。
アクセストークンを local storage に保持(非推奨)
まずアクセストークンを local storage などの、いわゆる web storage に保存する方法ですが、こちらはリスクが伴います。というのも、web storage は JS からアクセス可能なため XSS が発生したらアクセストークンを抜き取ることが容易になります。こちらも避けるべき選択肢かと思われます。
アクセストークンを Cookie に保持(?)
cookie にHttpOnly
/SameSite
/Secure
属性を付与することで、JS からのアクセスを防ぎつつ CSRF 対策などもできるので local storage より堅牢な対策が可能です。一方、Cookie はブラウザにもよりますが最大 4kb と容量の制限がかなり厳しいため、アクセストークンを丸ごと入れてしまうと容量を圧迫するというトレードオフが発生します。
セッションのように強制ログアウトも難しいし、可能なら避けるべきかと思います。
(が、 SPA とかではしょうがない気もしています。この辺は有識者の方いたら教えてください。。。)
アクセストークンをセッションに保持(推奨)
最後に、アクセストークンをセッションに保持することについてです。調べてた限り、おそらくこれが現在のベストプラクティスになるんじゃないかと思います。セッション ID を保持する Cookie を作成し、セッションストアでアクセストークンを保持することで、Cookie 容量を節約することが可能です。トレードオフとしてはセッションストアの管理がサーバー側で必要になってくることでしょうか。
また、Cookie の有効期限については当然ながらアクセストークンの有効期限を超えてセッション保持することはできないので、アクセストークンの有効期限より前で有効期限を設定し、HttpOnly
/SameSite
/Secure
属性も適切に設定しましょう。
まとめ
長々書いてきましたが、端的にまとめると以下の通りかと思います。
- OIDC は ID トークンを発行するための仕様で、ID トークンを検証することで認証が行える
- 認証が成功したら、アクセストークンをセッションに保存するのが良さそう
- セッション Cookie には、
HttpOnly
/SameSite
/Secure
を適切に設定する必要がある
感想としては、やはり具体的な攻撃や脆弱性をイメージできないとそれぞれどんな意味のある検証なのか理解できなかったので、認証周りの難しさは攻撃を理解するというところからもあるように感じました。
参考
CSRF
**CSRF(Cross-Site Request Forgery)**は、有名な攻撃手法の一つです。CSRF というと攻撃者が被害者になりすまして SNS 投稿するなどのイメージが強い方も多いと思いますが、OAuth の場合は逆に攻撃者のリソースアクセス権を被害者環境で有効にします。これにどんな意味があるかというと、画像を鍵付きの SNS 投稿したつもりが、攻撃者のリソースに意図せず連携してしまうなどが挙げられます。
これを防ぐにはstate
パラメータに Client のセッションに紐づく値を検証することで、認可を行ったユーザーとアクセストークンを取得しようとしているユーザーが一致していることを保証されます(ユーザーが一致してることを確認してるだけなので、state の検証成功=認証ということではありません)。
リプレイ攻撃
Implicit Flow において、ID トークンはエンドユーザーに直接返却されます。攻撃者がこの ID トークンを何かしらの方法で盗聴した場合、ID トークンを元に適切なログインかどうか検証しているクライアントは攻撃者と被害者の ID トークンに差がないので見分けられません。このように、ID トークンを盗聴し自分の ID トークンを置き換えることで不正なログインを試みる攻撃をリプレイ攻撃と言います。
そのため、Implicit Flow では前述のnonce
の検証が REQUIRED となっています。
トークン置き換え攻撃
OAuth2.0 において、Implicit grant Flow というブラウザなどに直接アクセストークンを渡す方式があります。この際、攻撃者が被害者のアクセストークンを不正に入手することに成功したら、クライアント側は正しいアクセストークンを受け取ってるように見えるので被害者と攻撃者が見分けられません。このように、被害者のアクセストークンを盗んで置き換えることでなりすましが可能なこの攻撃を、 トークン置き換え攻撃(Token Replace Attack) と言います。
これはstate
パラメータを適切に設定・検証することで対策は可能ですが、そもそも Implicit grant Flow 自体現在では非推奨のようです。
Discussion