認証・セキリュティまわり
トークン認証・セッション認証について改めて基礎から学び直すスクラップ。
有益な記事がたくさんあるので、適当に読み漁り、自分が見返すように要点をコメントするのがメイン。
ベースはこちらブログ
当たり前ですがJWTであるアクセストークンは改ざんチェックしかしてないので本人認証としては弱い。なんらかの問題で流出したらアウト。秘密鍵を変えるか有効期限が過ぎるのを待つしか無いです。
たとえリフレッシュトークンを使って再発行しても古いアクセストークンをステートレスに無効にすることは出来ない。やるならブラックリストデータを状態として持つしかない。
なので理想的にはアクセストークンはワンタイムトークンとして振る舞うのがセキュリティ的には良いと思います。仕様上それは出来ないから1分以下とか超短期の有効期限にするのがベスト。
これならアクセストークンが流出してもあまり問題はないはず。リフレッシュトークンはサーバサイドで状態を持ってるので万一流出したら破棄することが出来るので通常のセッションと同程度の長さで問題はないし明示的なログアウトも作れます。
ただし、アクセストークンの有効期限が短いと認可サーバへの問い合わせ回数が増えるのでここはトレードオフですね。認可サーバの負荷的には従来のセッションによると大差無い気もするけどクライアント側の通信コストが問題。ユーザが多いと署名コストもボトルネックになるかもです。
この辺のさじ加減は作ってみないと分からないのでバランスはサービス次第になるかと思います。
JWTをセッションで使う場合の問題点として挙げられている項目
- サーバー側でセッションの無効化ができない
- 秘密鍵を更新すれば、発行済みの全セッションを無効することは可能
- ログアウトしてもユーザーのストレージからトークンが削除されるだけであり、トークンそのものが無効になるわけではない
- CSRF 対策にはなるが、セッションハイジャックは防げない
- 有効期限を明示的にペイロードに含まない限り、セッションが恒久的に有効になる
- 秘密鍵を知っていれば任意のユーザに対するセッションハイジャックが可能
- 秘密鍵は定期的に更新しなければならない
- JWT 特有の罠がある
- alg の柔軟性 (alg=none 許容で即死など)
- JWE で利用可能な暗号アルゴリズムが微妙 (らしい)
- トークンのサイズが大きくなる場合があり、 cookie にデータを保存しにくい
- Web Storage API を利用
- cookie の HttpOnly フラグの保護を受けない
- cookie の secure フラグの保護を受けない
- Web Storage API を利用
この記事へのアンサー記事
JWT認証のメリットはその実装のシンプルさとステートレスなことにあります。現実的には実際はDB参照とか必要になったりするんですが、ほとんど改ざん検証だけで済むのは魅力的です。トレードオフでリアルタイムでユーザー無効化ができないことくらいですかね。ライブラリなんて使う必要ないほどシンプルだし、トレードオフさえ許容できればむしろ、なぜこれ以上に複雑な認証システム使わないといけないの?複雑さゆえにライブラリが必要になったり、そのライブラリが脆弱性を抱えていたり、そもそも使い方を間違えてしまったりするんでしょう。
- 明示的にログアウトするにはサーバー側の秘密鍵の変更が必要
- 秘密鍵を変更した場合
- 誰か一人でもログアウトしたいときは全員ログアウトになる
- 秘密鍵を変更しない場合
- なりすましログインが発覚しても、トークンの有効期限が来るまではそのセッションは無効にできない
- 秘密鍵を変更した場合
- ↑ の回避策
- 明示的にログアウトしたユーザーを無効なトークンとして管理する
- ユーザーがパスワードを変えたらトークンも再発行の必要があるようにする
- 結局のところサーバーサイドでのステートフルなセッション管理
- JWTのメリットであるステートレスを捨てている
コメントからそのまま記載
多分もう見ていないと思いますし、私が答える事でもないと思いますが、似たことを考えて実装をしたことがあるので…
"内部でセッションIDを無効化することでJWT自体も無効化可能"
この点については文字通りの意味だと思います。
まず、今まで通りのセッションIDを考えてください。
サーバサイドでデータストア(RDBMS,Memcache,Redis等)でセッションIDとユーザを紐づけることをやっていると思います。
なので、セッションIDをデータストア上から削除すれば無効化できますよね。JWTは「署名をつけることもできる規格化された入れ物」です。なのでJWTの中にセッションIDを持つことは可能です。
JWTからセッションIDを取り出してデータストアに突き合せて使うことができます。そのセッションID自体は上記の操作で無効化できます。ただそれだけだと思います。わざわざJWTで包むと何がうれしいのかというと、書かれている通りなんですが、JWT(正確にはJWSによる署名を含む)により「(署名の鍵が盗まれていない限り)JWTを発行した人が信頼できること」「内容の改ざんがないこと」「JWTによる期限」といったことが検証できます。
これらはデータストアに問い合わせることなく可能なので、場合によっては負荷が軽減します。署名検証分の負荷は増えますが、ネットワークレイテンシと比べたら些細です。また、悪意あるユーザからの攻撃に対してデータストアの負荷が軽減するといった効果があります。(セッションIDだけではとりあえずデータストアに突き合せないといけないですからね。)また、セッションIDは作り方が課題です。
セッションIDには乱数が用いられたりしますが、脆弱な実装だと乱数を推測できたり固定化することができてしまいます。特にOSSなフレームワークを使っていると分かると突かれることがあります。
極端な話ですがセッションIDを適当に作っていたらログイン出来ちゃったみたいなことも考えられます。
(もちろん、今はほとんどフレームワークが強固な方法で用意してくれており、メジャーな対策は含めているはずなので気にすることはあまりないかもしれません)そんなセッションIDをJWTで包んでやると署名がつくので、HMAC等の署名技術が破られない限りそういった攻撃は困難です。
(署名も適当に作ったら通っちゃったみたいなケースは考えられますが、セッションIDと署名を同時に当てるのはセッションIDをあてるよりも絶対に困難だと思います。もちろん署名の作り方にもよりますが脆弱な方法を使わないといった一般的な話として・・・)それだと攻撃者によって改ざん前にJWTがコピーされていた場合に無力だなと思いました
その通りです。
これはセッションハイジャックであり、セッションIDだろうがJWTで包もうが、秘匿されるべき情報が盗まれてはどうしようもありません。
これに耐性がある仕組みは存在しないので、セッションハイジャックは起きないように実装すべきでしょう。なお、JWTに利用者のIPv4を埋め込むことで、通信者のIPとJWT内のIPが一致しないと通さないといった実装もできますが、IPは変わりますからセッション管理に使うには厳しいでしょう(他のユースケースでは使えるかもしれません)
JWT とは
- JWTはとにかく色々なデータ、例えば構造化されたものからバイナリデータまでを複数のサービス、システム間でやりとりするため、URLセーフな文字列にエンコードする仕組みです。 また、そのエンコード結果の文字列自体をJWTと呼ぶこともあります。
- さらに、JSON Web Signatureという署名をつける仕組みを利用することで改ざん検知が可能になります。JSON Web Encryptionという暗号化を施すことにより、センシティブなデータのやりとりが可能になります。 この2つのうち、よく使われているのを見かけるのがJSON Web Signatureの方でしょう。
- Google、Appleでサインインの裏側で動いているOpenID Connectの中で、ID Tokenというユーザーの属性情報や認証時の情報などをやり取りする際にJWTが利用されています。
- もともとSAMLというXMLベースのID連携の仕組みで使われていたXML署名という仕組みがあらゆるユースケースに対応できるようにした結果複雑になってしまったという経緯から、JWTはより容易に実装できてコンパクトに表現できるセキュリティトークンを目指しました。
JWT は次の特徴を持つエンコードフォーマットです。
- 様々なデータをURL Safeにエンコードできる
- JWS(RFC7515) で署名をつけたり、JWE(RFC7516) で暗号化したりできる
- 構造化されたデータをやりとりするための標準的な claim が定義されている(RFC7519)
- 署名や暗号化のアルゴリズム(RFC7518)、鍵の表現(RFC7517)も豊富
アクセストークンの要求方法とそれに対する応答方法を標準化したものが OAuth 2.0 である
OAuth 2.0 はアクセストークンを発行するための処理フローを定めていますが、それを流用し、ID トークンも発行できるようにしたのが OpenID Connect なのです。
そもそも ID トークンは何のためにあるのでしょうか? それは、ユーザーが認証されたという事実とそのユーザーの属性情報を、捏造されていないことを確認可能な方法で、各所に引き回すためです。 一ヶ所で(=一つの OpenID プロバイダーで)ユーザー認証をおこない、発行された ID トークンを引き回すことができれば、別の場所で何度もユーザー認証を繰り返す必要がなくなります。 短く言うと、『ID 連携』ができます。
結論としては、XSS を 100% 防ぐことはできないよね、という内容。
ただ、LocalStorage だろうと、Cookie だろうと、XSS された時点でどうしようもない気がする...。
確かに Cookie の場合は、HttpOnly
フラグをつければ、JS からはアクセスできないが、トークンを使った操作はできる。トークンが盗まれることよりも、こっちが本題。
同じようなことが書かれている。
攻撃者にとって真に重要なのは認証トークンでは無く,認証トークンを使って何をするか,ということのはずだ.
つまり,ペイロードが評価されたと同時に攻撃する方が効率的だし,この場合クッキーは送信されればよく,読む必要すら無い.このことを踏まえると,XSSが発生した場合,クッキーのHttpOnly属性が効果的だとも思えない.
超入門:ウェブサイトのパスワード保護~ウェブサイトでパスワードを保護する方法~
- パスワードは暗号化ではなくハッシュ化されることが多い理由
- 暗号化の場合、鍵の管理が難しい
- パスワードをサイト管理者にも知られたくない
- ただのハッシュ化の場合、同じパスワードだとハッシュ値も同じになる問題がある
- ソルトによる対策
- ソルトとはパスワードに追加する文字列
- 当然ソルトはユーザーごとに変える
- ソルトは通常ハッシュ値と一緒に保存
- ストレッチング
- ハッシュの計算を繰り返すこと
- 十分長いパスワードの場合、ストレッチングは必要ない
- 弱いパスワードの救済
- ペッパー
- パスワード情報が漏洩する状況では、ハッシュ値とともにソルトも漏洩する
- そこで、パスワードにペッパー (固定の秘密文字列) を追加してからハッシュ値を求める
- ペッパーは DB 以外の場所に保存
クロスサイトスクリプティング(XSS)対策としてCookieのHttpOnly属性でどこまで安全になるのか
- HttpOnly 属性のおさらい
Set-Cookie: secretname=ockeghem; HttpOnly
- HttpOnly 属性をつけると JS から参照・更新できない
- ただ、XSS で起動した JS が fetch した場合、リクエストに Cookie は付与される
- 同一オリジン・同一サイトのため
- SameSite 属性も当然効果なし
TCP/IPを理解している人ほど間違いやすい 常時SSLでもCookieのSecure属性が必要な理由
- Secure 属性のおさらい
- Secure 属性のついた Cookie は HTTPS の場合のみ送信される
- TCP 80ポートをとじれば、Secure 属性はなくても良いというのは誤解
- TCP 443 ポートに HTTP リクエストを送信させる方法がある
<img src="http://example.com:443/man.png" />
- 同一オリジンポリシーとは
- オリジンとは
- スキーム + ホスト + ポート
- スキーム: http
- ホスト: example.com
- ポート: 80
- 他のオリジンからリソースにアクセスできないように制限するもの
- オリジンとは
- CORS
- クロスオリジンのリソースアクセスを提供
- オリジンを信頼する・しないは、サーバーがブラウザに Access-Control-Allow-XX ヘッダを用いて伝える
- CORS の保護戦略
- リクエストを送る前にお伺いを立てる = プリフライトリクエスト
- 帰ってきたレスポンスを JS に渡して良いことを許可する
- プリフライトリクエスト
- リクエストの内容からブラウザが自動的に API サーバーに許可を求める
ステートフルなセッション識別子は、ログアウト後にはサーバー上で無効とされるべきである。ステートレスなJWTトークンは、攻撃者の機会を最小にするために、むしろ短命であるべきである。寿命の長いJWTの場合は、アクセスを取り消すためにOAuth標準に従うことが強く推奨される。
JWTの説明と一緒に良く出てくる認証や暗号化の話は、ここまで一切でてきていません。このことからも分かるようにJWT自体は認証や暗号化に関してなにも規定していません。単にスペースに制約のあるHTTPヘッダーにJSONを載せるために、URLセーフでデータを小さくするJSONのデータフォーマット(表現形式)を決めているだけです。
後ほど出てくる話ですが、参考までに先にお伝えしてくと、JWTと認証の関係は、アプリケーション間で認証データをやり取りする伝達手段としてJWTが都合が良いために使われているだけで、JWTと認証は本来なんら関係はありません。
JWSではこのようにシグニチャを復号化した結果とヘッダーとペイロードを比較し、改ざんされていないかを検証します。
- 防御方法
- トークン
- SameSite 属性を指定した Cookie
- Strict: 同一サイトのみ送信
- Lax: 異なるサイト間の場合、GET の場合だけ送信
- None: どんな場合でも送信
- SameSite 属性利用のデメリット
- 古いブラウザが対応していない
- GET の場合は Cookie が送られてしまう (Lax)
- GET で状態変更できることがよくない
- Cookie を利用しない場合は使えない
- 認証不要な場合など
- Origin ヘッダー
- リクエストの Origin ヘッダーの値とサーバー側のホスト情報を照合
- Sec-Fetch-* ヘッダー
- Sec-Fetch-Dest: fethc するリソースを使う形態。document や image、font...
- Sec-Fetch-Mode: リクエスト形態。navigate、cors...
- Sec-Fetch-Site: リクエスト元 Origin との関係。same-origin、corss-site...
よく使う Cookie 属性
Cookie Prefixes 知らなかった
SameSite が Strict であっても、スキームだけ違うケースやサブドメインなど、100% 安全ではない
超簡単にいうと、ユーザーが http://example.com/ にアクセスしようとしたとき、ブラウザが自動で https://example.com/ に置き換えてアクセスしてくれる機能のことらしいです。
この徳丸さんのコメントは重要かも
認可コードフロー
OAuth のフローで1番分かりやすかった
state と PKCE 周りが特に
OAuth のクライアント実装
OIDC はこっち
app.use((req, res, next) => {
// post である場合は origin と sec-fetch-site をチェック
if (req.method === "post") {
// origin は必ずチェック
if (req.headers.origin !== "https://sns.example") {
return res.send(400)
}
// sec-fetch-site は、存在した場合だけチェック
if (req.headers.secFetchSite && req.headers.secFetchSite !== "same-origin") {
return res.send(400)
}
}
return next()
})
// デフォルトに頼らず Cookie に Lax を明示
// 理想は read と write に cookie を分け write を Strict にする
app.use(session("Lax"))
// 副作用のある API は必ず POST にする
app.post("/post", async (req, res) => {
await createPost(req.body)
// ...
})