認証についてわからなくなったときに見る雑メモ(自分用)
認証の話があがるたびに何回も同じこと調べてる気がするのでまとめておく。まとめておきたいのは以下。
- 用語の整理
- OAuth
- JWT
- OpenID connect
- firebase Authentication
- NextAuth
- Auth0
- Cognito
- アクセストークンとリフレッシュトークンとIDトークンの扱い方
- サーバー側でやらなきゃいけないこと
認証と認可
認証と認可は違うよっていろんなところで言われてる。
認証はユーザーが正しいユーザーか検証すること。
認可はリソースにアクセスすることができるかの権利的な話。
詳しくは以下によく書かれている。
いろんな認証方式
以下の記事がよくまとまっていて助かる。
Basic認証
サーバーにリクエストするとサーバーからBasic認証であることが伝えられユーザー名とパスワードを求められる。入力したユーザー名とパスワードはBase64でエンコードされてAuthorizationヘッダにセットされて送られる。ので機密性はない。
Digest認証
Basic認証では盗聴が問題になるためパスワードをランダムな文字列(nonce)と組み合わせてハッシュ化している。nonceはサーバ側が送ってくるのとクライアントで生成するcnonceが登場する。サーバー側の検証はDBに保存されているパスワードとnounce、cnounceを使用してハッシュ化し送られてきたハッシュ値と比較することで検証する。
サーバー側で生パスワード保存するのかと思ったけどちゃんとハッシュ値を保存するっぽい。nonceが可変なのにハッシュ値変わらないの??
nonceとソルトの理解が混ざった。nonceの目的は同じ認証情報を再利用する攻撃であるリプレイ攻撃を防ぐのが目的。短命。サーバー側で送ったnonceとクライアントから送ってもらったnonceが一致することを確認する。だからハッシュ値をDBなどに保存しておいてクライアントからのハッシュ値との比較で問題ない。nonceの一致も確認する。厳密にいうとハッシュ値、nonce、HTTPメソッドなどを使用して再計算したハッシュ値を比較するっぽい。
Basic認証よりはましだが中間者攻撃に対して脆弱。HTTPSを使用して対策すべき。
フォーム認証
JavaのSpringみたいなサーバーで全部やるスタイルのwebアプリ開発から始めた人はここが入り口のはず。ユーザー名、パスワードをHTTPのPOSTリクエストで送る。パスワードはHTTPSで暗号化されてる。あとはハッシュ値として保存されてるサーバー側の値と比較して検証。認証が成功するとセッションIDをクッキーにセットしてセッション管理する。サーバー側でもセッションIDを保存しておいてクッキーのセッション IDの検証もする。サーバー側のセッションIDの保存場所はSpring Securityではデフォルトではメモリ内のセッションストア、パフォーマンス上げるのにRedisなども使用できる。
- 依然として中間者攻撃などの脅威があるためHTTPSは必須。
- サーバー側が保存するハッシュ値にはソルトを含めてレインボーテーブル攻撃になどに対応する必要がある。
- セッションのライフサイクルを適切に設計するのも必要。
Bearer認証
いわゆるアクセストークンを使用した認証。クライアントは入手したトークンをAuthorizationヘッダーにBearer <アクセストークン>
をセットしてリクエストすることでサーバー側はトークンの検証をし、問題なければリソースを返す。Bearer認証はOAuth認証などの認証フレームワーク内で使われるのが一般的。
フロントとバックエンドを分けて開発するようになったり、モバイルとwebのように複数のクライアントとやり取りするAPIを開発することになってきたからかこのようなアクセストークンを使用した認証が増えてきた。
Bearer認証を使うならば、次に説明するOAuth認証などが使われることが多くなる、はず。
OAuth認証
OAuthは認可の仕組み。認可サーバーとのやり取りでアクセストークンを発行してもらい、そのアクセストークンを用いてアクセストークン発行元のAPIを使用する。このアクセストークンを認証の仕組みとして使うのはとてつもないアンチパターンなので今すぐやめよう。
OAuthを認証につかいたいならOAuthに加えてOpenIDを組み合わせたOpenID Connectを使用する。
以下の記事がとてもわかりやすい
- アクセストークンがなりすましサイトで抜き取られた場合、なりすましできてしまう。
- アクセストークンだけでは誰に対して発行したトークンかがわからない。
なりすましへの疑問
OAuthの認可サーバーが発行したアクセストークンだけではだれを認証しようとしてるのかがわからない。アクセストークンが悪意のあるユーザーにもし漏れてしまいもし、アクセストークンを認証に使用していた場合、悪意のあるユーザーがなりすましできてしまう。
これはフロントだけで完結するアプリならそこまで問題にならないが、バックエンドがあるアプリだとバックエンドのAPIを呼び出す時に認可サーバーに発行してもらったアクセストークンをつけてしまいがち。そして、そのアクセストークンで取得したユーザー情報で認証する場合は上記のなりすましができてしまう。
なので、認証に使いたいならOpenID Connectを使用し、IDトークンを認可サーバーに発行してもらい、これを認証に使う。サーバー側はこのIDトークンを検証することで誰が認証しようとしているのかを知ることができる。DBにユーザー情報を持たせる場合は、IDトークンを使用して取得できるユーザー情報から何かしらのユーザー識別子を取り出して紐づければよさげ。
こういったトークンを使用した認証はサーバー側でセッションを管理することもない。クライアントはアクセストークンやリフレッシュトークンが失効してた場合、リフレッシュトークンを使用して更新する。なのでセッション管理はクライアント側に委ねることになる。
と思ったけどトークンIDが漏洩した場合にアクセストークン同様なりすましのリスクがあるんじゃないか?アクセストークンだけだとだれを認証しようとしているかがわからないのでそれをトークンIDを使用したOpenID Connectでだれを認証しようとしているかが検証できるようにしたけど、結局トークンが漏洩したらなりすましのようなリスクになる、よね?
認証フロー
OIDCの認証フローは8つくらいあるらしい。よく聞くのはAuthorization Code Flow
やImplicit Flow
。ちょっと詳しくは調べてない。とりあえずトークンのもらい方、扱い方、認証の仕方が複数あるくらいの理解で。
Firebase Authentication
firebaseの認証マネージドサービス。GoogleやTwitter、Facebookなどの代表的なプロバイダを使用してOAuthによる認可やOpenID Connectを使用した認証などができる。SDKが用意されているので比較的容易に認証、特にxxxでログインするみたいなソーシャルログインを実現できる。
Googleプロバイダーを使用する例
Googleプロバイダーを使用する例。SDKを使用することでGoogleでログイン
みたいなUIも提供される。ログインしようとするとGoogleの認可サーバーにリダイレクトされ、許可を求められる。許可するとアクセストークン、リフレッシュトークン、IDトークンがfirebaseに払い出される。firebaseはそれらのトークンを適切に管理してくれ、firebase Authenticationトークン(JWT)をクライアントに発行する。このトークンはfirebase SDKを使用して検証、ユーザー情報の取得ができる。
なので、サーバーとAPIでやり取りするようなアプリケーションの認証にこの仕組みを使う場合、firebasae AuthenticationトークンをAuthorizationヘッダーに含めてサーバーリクエストする。サーバー側はfirebase SDKもしくはAPIを使用してトークンの検証する。検証に成功したら、ユーザーを識別できるユーザー識別子とユーザー情報をDBなどで紐づけたりすればよさげ。
ちなみにIDトークンの更新はfirebaeが勝手にやってくれる。アクセストークンはリフレッシュトークンを用いて更新する必要がある。
Auth0
firebaseより高機能でカスタマイズ性が高そう。その分コストも高そうなのでfirebaseの機能で不十分になってきたらAuth0を検討するとかありそう。
1番情報は少なそうだけど、ドキュメントは1番充実してそうでSDKやAPIも充実してそうだった。
Cognito
AWSなのでAWS寄りの組織、プロダクトなら選ぶのはありかも。firebaseの認証フローと同様、アプリケーションにログインするにはCognito認可サーバーからリダイレクトされた認証フォームで認証するとアクセストークン、IDトークン、リフレッシュトークンが発行される。あとはIDトークンを使いサーバー側で検証を行う流れ、たぶん。
参考までに
NextAuth
Next.js用に設計された認証ライブラリ。
NextAuthについて
NextAuthからAuth.jsに名称は変更されているっぽい。Auth.js自体はOSSのライブラリなので費用はかからない。firebaeやCognitoのようにGoogleなどからIDトークンを取得することは可能なようなのでIDトークンをサーバーAPIに送ることはできる。サーバー側のIDトークンの検証は...
Next詳しくないからあれだけどクライアントで何かしらのIdPで認証した後のJWTトークンの設計、セッション処理など細かく設定できるようで、サーバー処理までNextで管理できる。セッションは当然Cookieにセットされる。
たぶんセットされたセッションの検証までNextAuthの方でやってくれるのだろう。やっぱり独自のサーバーAPIを裏側に持たず、Next完結型のアプリの認証ではまず使用される技術なのだと思う。独自のサーバーAPIを裏側に持つならIDトークンの検証、セッション管理などをサーバー側でやらないといけない(これは前段にCloudfrareを必ず通り、認証処理がCloudfrareで完結するならエッジで済むようになるのかもしれない)のでNextAuthをクライアント側の認証UIの作成に使ったとしてもサーバー側で検証処理やセッション管理がしやすいようにfirebaseやAuth0、Cognitoなどの方がやりやすいような気がしなくもない。
どれを使おうがOAuthの仕組みで認証するなら、サーバー側でトークンの検証をする必要がある。
各種認証サービスの感想
OpenID Connectを使用した認証サービスは上記の通りいろいろある。とりあえず難しいこと考えずにサクッと導入したいならfirebaseがよさげ。Auth0は本気で認証基盤作るときに候補に上がりそう。使ったことないからわからんけど。Cognitoはまあ、AWSどっぷりな組織なら、使ったことないから知らんけど。NextAuthは最近のNextを軸にしたフロントの技術の中に入ってるのを見かけてとりあえず調べてみた。TSで完結するアプリを作ってるなら真っ先に候補にあがるよなという感想。あと、料金かかんないの?これ。コスト的にも有利。バックエンドがGoとかTS以外でAPI作られていても各種プロバイダーからIDトークン取得はできるので自前で検証処理いれるならTS完結型じゃなくてもありか?自前でJWTトークン検証すればいいんだよね?できるよね?
自前でIDトークンの検証はできるけど複雑な検証処理を自前で実装すべきではない気がする。NextAuth使うならやはり裏側にサーバーAPIがない場合もしくはTSだけで成り立ったアプリという気がする。
OpenID Connect(OIDC)におけるIDトークンの検証
アクセストークン同様、悪意のある偽サイトがトークンIDをもし盗んだ場合、その悪意のあるユーザーは盗んだIDトークンを使用してなりすましができてしまう。
それを解決するためにサーバー側はIDトークン内に含まれるaud(Audience)クレームを検証することでどのクライアントに発行されたトークンであるかを検証することができるためなりすましサイトかどうかを知ることができる。
それ以外にもIDトークンの検証というのは思った以上にやることが多く複雑。
参考
IDトークン 検証方法 メモ
OpenID Connect の JWT の署名を自力で検証してみると見えてきた公開鍵暗号の実装の話
このような複雑な検証処理の自前実装は脆弱性を容易に生むので可能な限りライブラリやSDKを頼りたい。
例えば、firebase Authenticationを使用するならトークンをverify()
みたいなSDKの関数にトークンを渡すだけであとはfirebaseが全部やってくれる。ちなみにfirebaseを使用したときのaudの値はfirebaesプロジェクトのプロジェクトIDになるみたい。
細かく調べてないけどAuth0やCognitoを使う場合も同じような感じで細かい検証実装はSDK側でやってくれるのだろうと思う。ありがてー
サーバー側のユーザー情報の持ち方
フロントとバックエンドが分かれてるタイプのアプリケーションでOAuthというかOIDCで認証処理を行う場合を考える。前述したように何かしらの認証サービスを用いてGoogleなどのプロバイダーからIDトークンをまずは取得する。取得したトークンはHTTPヘッダーにセットしてバックエンドAPIとやり取りする。
バックエンドはそのトークンが有効なトークンであるかを検証する。検証するにはJWTトークンを複合化し前述したaudクレームなどを検証するが自前でやるのは怖すぎるので可能な限りSDKやライブラリに頼りたい。
検証が成功した場合、ユーザーがだれなのか、正しいクライアントアプリからのアクセスであること、認証されたユーザーであることなどが証明されるのでそのユーザー情報のユーザー識別子、ユーザーを特定できるものをDBのユーザー情報に紐づけるなどする。ユーザー識別子は例えばgoogleのメールアドレスとか認証に使ったプロバイダー側のIDなどが考えられる。
例えば、ユーザーテーブル的なものをDBに用意するとして、そのIDはアプリケーションユーザーのIDとして適当に振っておいて、トークンから取得したユーザー識別子もそのレコードに紐づけておく。
これで次回以降ログインを再度するときにトークンIDから取得できるユーザー識別子を使用してユーザー情報を取得できる。
ログイン状態を維持するために伝統的なセッションベースのアプローチとトークンベースのアプローチがあり、トークンベースのアプローチの方が現代的。ただ、フロント側でトークンをローカルストレージなどに保存する必要があり、その保存先はよく考える必要がある。
こんな流れでたぶんGoogleみたいなソーシャルログイン、およびメールアドレスとパスワードのようなよくみるログインの仕組みは作れて、フロントとサーバーのやりとりはトークンを使い、サーバー側で適切に検証することで比較的に安全にユーザー情報を管理できる。
あとはログイン状態の保持にフロントはトークンをどこかに置いておく必要があり、その置き場所にはよく気をつける必要がありそう。
SAML(おまけ)
SAMLもOIDC同様ID連携のための標準規格。OIDCがjWTを使ったJSONベースである一方、SAMLはXMLベースらしい。OIDCの方が新しく、エンプラ系ではSAMLの方がよく見かけるらしい。
その他
- シングルサインオン 複数のログイン管理を関連づけて行うという理解。
- 多要素認証
- パスキー
NextAuth x firebase Authentication
firebaseで認証して、取得したIDトークンをNextのサーバー処理で検証する。検証に成功したらNextAuthを使用してセッション管理する。セッションはCookieに格納。
しずかなインターネットがこの組み合わせで認証してそう。TS完結型のアプリならこの組み合わせで認証するのが良さげっぽい。
フロントとバックエンド分かれているタイプのプロジェクトでfirebase使って認証するとき、IDトークンの検証まで成功したらセッション管理するの?毎回トークン送ってもらって検証するのパフォーマンス落ちそう。
トークンそのままキーにしてRedis使ってるのみたことあるけどそれがセッション管理か。トークン一回検証しててクライアントがそのままどこかのストレージとかで管理してる場合、とりあえず毎回トークン送ってもらってredisにあったら認証済みとできる。redisの値にユーザーIDとか入れておけばいいのか?
セッションIDをCookieにセットしてみたいなのでもたぶんいいのだけど、Goでそこらへんいい感じにやってくれる認証フレームワークがないので自前実装しないといけない。サーバー側でセッション保持しなきゃいけなくてクライアントのセッションIDを検証しないといけない、はず。
普通にセッションIDをredisに保存しといてあるかないかだけやればいいのか。
セッションライブラリ探したらあった。gorilla/sessions
が今のところ1番スター数は多そう。
Cloudflare zero trust x KV
Cloudfrare zero trustとKVを使って認証をやってるというのもたまにみかける。あんまり理解してないので軽く調べる。CloudfrareのKVを使用することでセッション管理をエッジでできるメリットがあるらしい。
Cloudfrare Access
社内の開発環境といったクローズドな環境への経路構築。VPN運用のコストがリモートワークで高まってきたからか注目されていたよう。既に利用しているIDプロバイダーと連携したりもできるみたい。クライアントアプリのWARP(PC, スマホ)が利用できる。認証とは関係ないけどこれもCloudfrare zero trustのサービスの一つ。50ユーザーまで無料。
クローズドな環境の認証
管理画面や公開した開発環境にアクセス制限をかけたいとかそういったときにBasic認証とかやりたくないし複雑な認証処理を作りたくない。そんなときにCloudfrare Accessでメール認証するとサクッと実現できるよう。
時雨堂が使用している技術を見て調べ出したけど時雨堂も管理画面の認証に使っていてそのセッション管理をKV使ってやってるんだと思う、たぶん
いずれにせよアプリケーションの認証というか社内向けの認証を考えるときに使用されそうな技術という理解をしました。
セッションについて
通常認証が成功した場合、毎回検証処理をするとパフォーマンス影響が出るためセッション管理したい。セッション管理には伝統的なCookieを使った管理とトークンベースが考えられる。
Cookieベースのセッション管理
サーバー側で作成したセッションIDをCookieにセットして毎回セッション管理をする。サーバー側で保持するセッションは通常メモリ上に保存されることが多いがRedisなどに保存することでよりパフォーマンス向上させることができたりセッションを永続化することができる。CookieベースはHTTP通信に限られる。Cookieを使うといわゆるセッションハイジャック的なセキュリティ面を気にする声も多い。
トークンベースのセッション管理
Googleなどのソーシャルログインを導入している場合、OIDCを利用することになるのでアクセストークンやIDトークンがクライアントに発行される。このトークンを毎回API通信に含め、サーバー側ではそのトークン情報を毎回検証することでセッション管理する。トークンをCookieのセッションID代わりに使うイメージ。この場合、サーバー側でセッション管理をする必要がなくなる。代わりにクライアントはトークンを適切な場所で保持する必要がある。パフォーマンス的にもサーバーは毎回トークンの検証をする必要が出てくる。HTTP通信に限定されないのでネイティブアプリがgRPC通信する場合なども使える。
アクセストークンもIDトークンもCookieも中間者攻撃のリスクは抱えている。IDトークンはどこに発行したかの情報まで含んでいるので中間者攻撃のリスクは低いかもしれない。そう考えるとCookie抜き取られるリスクもあるので毎回トークン検証したほうが安全なのかもしれないけどパフォーマンス影響も考えなきゃいけない。なにが正解というわけでもないけど上記のようなことを考えた上で認証、セッション管理を実装する必要がある。
NextAuthについてよく書かれている
何回目だよっていう認証とセッション
伝統的にフォーム認証などで認証が成功した場合、セッションIDをCookieにセットする。サーバー側ではセッションストアでそのセッションIDとユーザー情報を持っておく。セッションストアはインメモリであることが多いがRedisなどのリモートキャッシュも利用されることがある。サーバーはクライアントから送られてきたセッションIDでセッションストアからユーザー情報を取り出す。ユーザー情報を使うかどうかは置いておいてセッションストアに送られてきたセッションIDがあることで認証済みとみなす。ここら辺FWでやるならばセッションIDの更新とかもたぶんされる、たぶん、セキュリティ的にはそうしたほうが安全。
ここまでが昔の話。現在はwebとモバイルのように複数のクライアントとAPIベースでサーバーとやり取りするのが主流でかつGoogleやXといったソーシャルログインがユーザー体験的にも実装的にも楽なためそういったソーシャルログイン対応が主流。
で、ソーシャルログインを実現するにはOAuthとOIDCの仕組みを使うことが一般的でAuth0やCognito、firebase AutheticationなどのサービスSDKを使ったりしてまずクライアントがアクセストークン、リフレッシュトークン、IDトークンを取得する。
クライアントはIDトークンをAPIリクエストと一緒に送ってサーバーはIDトークンを検証する。IDトークンはJWTであることが想定され、トークンを発行したIDプロバイダーから署名を検証するための公開鍵を取得しJWTの署名を検証する。
検証はこの公開鍵の取得とか以外にもいろいろやってそうで自力でやるイメージがわかない。SDKなどで用意されてる検証メソッドで済めばいいなと思ってる。NextAuthなんかはたぶんNextAuthでクライアントとサーバー側両方の処理を含んでるのでサーバー処理もTSでやる感じなら体験いいだろうなという感じ。
で、IDトークンの検証が終わったらセッション管理かと思うが、このような複数クライアントとのやり取りかつトークンベースの認証の場合、セッションベースでなくそのままトークンを使う。
これはセッションがステートフルでサーバー側で管理するのに比べて、トークンベースはステートレスでサーバー側で管理する必要がない。
セッション管理にはいろいろやり方がありそう。例えば、IDトークン検証後一緒に送られてきたアクセストークンをユーザー情報と紐づけて保存しておく。以後、アクセストークンと一緒にAPIリクエストすることで認証する。アクセストークンの期限が切れたらリフレッシュトークンで更新してセッションも更新する必要がある。
そもそも毎回IDトークンの検証をすればいいのではないか?
ちょっとまだOIDCを使用したOAuth認証時のセッション管理はピンときてないけどだいたいそんな感じ。のはず。
Laravelの認証
PHPのフレームワークであるLaravelに入門し、認証実装を勉強した。
Laravelの認証方法においてもセッションベースの認証とトークンベースの認証にわかれておりトークンはBarerトークンを使ったアクセストークン認証とOAuthを使用した認証がありそうだった。
SPAのようなフロントとAPI通信をすることだけを考えるなら何かしらで認証成功したらCookieにセッションIDくっつけてセッションベースの認証管理をするのが一般的っぽい。
マイクロサービスやモバイルとの通信でHTTPではなくgRPCなどが使われることも増えてきているのでその場合、トークンベースのセッション管理が必要になる。なにかしらの方法で認証したあとはクライアントに送ったトークンを毎回送ってもらいサーバーも毎回検証することでセッション管理する。
ただし、トークンを毎回検証するのではなくトークンをkeyにRedisのようなリモートキャッシュなどにユーザー識別子を紐づけておけば、比較的簡単に検証できる。
Larabelでは発行するBarerトークンはハッシュ化してDBに保存しており、送られてきたトークンをハッシュ化してDBと比較することで検証している。Laravel以外で似たようなトークン認証を実装するなら、トークンをハッシュ化して保存したり、トークンをどこに保存するかなどに気をつける必要がありそう。