概要
個人開発をしよう、あるいは公開しようとおもっていつもぶち当たるのが認証である。
ここを完全に理解できていないせいで公開に至っていないというのも過言ではないくらい壁が厚い。そして高い。
逆に言えばここを掌握すれば公開に至るまでの道のりはベリーイージーである。 つまりどういうことかというと、やれということである。
基礎知識がないと追いかけるのも一苦労なので、足りない知識をここで補っていく。
Cookie認証の大まかな流れ
基本的には、ユーザーのログイン状態をセッションとしてサーバー側で保管する。 フロントでは、ブラウザのクッキーにセッションのIDを保存する
- ログインページからログイン情報を保存する
- サーバーサイドで検証を行い、セッションに保存を行う。
- 保存した後、セッションIDをブラウザに渡す。
- ブラウザはクッキーに渡ってきたセッションIDを保存する。
- 以降のリクエストにはセッションIDを含めたやり取りを行う。
- セッションIDを元に「誰か」をサーバーサイドで特定し、それに応じたデータを返す。
- ログアウトする場合は、サーバサイドとフロントのセッションを破棄する
所謂ステートフルな通信というやつ
例えば、サーバーが複数あるような環境の場合、それぞれの環境でクライアント状態の同期をしなくてはいけないので、スケールアウトが難しい認証である。
モバイルアプリケーションとトークンの認証
モバイルアプリの場合を考えてみる。 アプリの場合は、一般的な構成として、アプリ + APIサーバー(or サーバーレス)の構成になる。 もしくはAPIGateWayを用いてRailsなどのフレームワーク(APIモード)と接続し、ハイブリッドで使用する。 モバイルアプリの場合は、ほとんどがトークン認証を行っている(と思う)。
トークン認証の流れ
- アプリ側から認証情報を送信する
- 認証情報をサーバーサイドで検証し、トークンを作成する。
- トークンをアプリに送信する。
- 取得したトークンをアプリが受け取り、保存する
- 以降のやり取りはトークンを含めてやり取りを行う。一般的にはAuthorizationヘッダーに埋め込む形を取る?
- トークン付きのリクエストをサーバーで受け、検証を行う。
トークンの扱い方として、DBで保存を行ったり、トークン自体にユーザ情報(そのままはあれなのでここではJWTなどのことを言います。)を埋め込んだりしてそのまま送ることもあるみたい。
このへんはどういった方法で認証を行うかに依存した実装になる。
こういった認証機構を提供してくれるOAuthやOpenIDConnectなどはより詳しい情報が公式で提供されているので実際に使用する場合はドキュメント読むと幸せになれます。
詳細は当然として、クッキー認証とは異なる点ももちろんある。
トークン認証はシステム側で「セッション情報」というものを持たないので、クッキー認証に対して「ステートレス」な認証であると言える。
使用している側(ユーザー)からは違いはわからない。ログアウトする場合、クライアントサイドのトークンを削除することでログアウト処理とすることができる。
クッキー認証と違ってクライアントサイドに認証情報が用意されているので、スケールアウトしやすい構成ではあるが、検証にDBアクセスをする場合は毎回検証することになるのでそのあたりがネックになると思われる。
キャッシュなどを使用して検証をパスするような仕組みを実装する必要がでてきそう。
最近の構成
最近自分が関わっているプロダクトでは、構成としてRailsだけで完結するアプリケーションを作成することはほとんどなく、フロントにVueやReactを採用してSPAで構成するケースがほとんどになってきました。
部分的にコンポーネントを埋め込む構成もやっていたことがありますが、いろいろと辛みがでてきてしまい、どうせやるなら最初からSPAにしたほうがよくね?という流れができつつあります。 プロダクトの仕様としても動きのあるものが多いため、後々のことを見越してもSPAで作るのが現状ベターな選択肢です。
SPAの場合、上記で挙げたセッション認証/クッキー認証のどちらも採用することができる。 フロントエンドとバックエンドが分離されていない構成のアプリの場合は、ほとんどのケースでセッション認証を利用します。
一般的には、SPAとしてのページ以外に認証用のページを用意し、認証が終わった段階でSPAを表示するような流れです。 Railsでいえば認証基盤はDeviseに寄せて、それ以外はSPAという構成になります。 フロントとバックエンドが分離されている構成の場合は、トークン認証を使用するのが良さそう。
この場合、デプロイ先は別々になります。必然的にセッション認証を行うよりも、認証の実装は多少複雑になります。 利点としては、認証が一体化していないので、web -> モバイルという展開がローコストで実現できたりします。
とりあえずwebでファーストフェーズのリリースを行って、将来的にはモバイルアプリも~みたいな要件の場合は、トークン認証を採用してバックエンドとフロントエンドを分離して作成するのがとても良いと思います。
ただやはり初期の実装コストは一体型に比べると大きいのでPOと相談してビジネスサイドと合意した上で採用するか決めるのがよいです。 このような分離した認証のことをIDPって言うらしい。(Identity Provider)有名なのだとみんな大好き「Googleでログイン」とか。 自作する場合はAuth0とかが候補に上がってきそう。あと使ったことないけどAWSCognitoとかも
前提知識としての座学
認証と認可
ここがごちゃついてると今後の流れについてこれないかもしれないので改めて簡単に補足
認証というのは「お前はだれ?」ってやつです。運転免許証 身分証です。
認可というのは、「許可される?」ってやつです。〇〇できる権限をもってるか?ということですね、
家の鍵をなくしたとします。扉に免許証を見せてもロックは外れません。一方、居住者でなくても、鍵さえ持っていれば「誰か」ということは関係なく家に入れてしまいます。つまりそういうことです。
英語では
- 認可 Authorization
- 認証 Authentication と書きます。
このあたり混乱する原因として、認可の中にユーザー情報が入ってる場合に、それを認証として使えるっていうのがありそう。
今回はOAuth2を使って[認可]の仕組みを作っていきます。
OAuth2
登場人物を整理してみる
- ユーザー ... あなた
- クライアント ... 認可を受けてアクセスするアプリ
- 認可サーバー ... 認可を管理する
- リソースサーバー ... 認可によってアクセスする先のサーバー
認可サーバーとリソースサーバーは同じサーバーを使っていることがほとんど。なので実質ユーザー、クライアント、認可サーバーの三個を覚えとけばいい。
OAuth2 のトークン発行フロー
- Authorization Code Flow ... サーバーサイドで使う、一般的なやつ?
- Refresh Token Flow ... アクセストークンを再発行するやつ
- Implicit Flow ... モバイルとか。アクセストークンがクライアントサイドから見れるので認証には使ってはいけない
Authorization Code Frow
多分よく見るタイプのやつ。「〇〇と連携しますか?」 -> 「XXからのリクエストを許可しますか?」 -> XXで〇〇からとってきた情報が表示されるみたいな流れのやつ。
連携を承認した場合、認可コードと呼ばれるものをブラウザに渡し、それをアプリが受け取り、アクセストークンを取得します。
認可コードはブラウザから見えますが、アクセストークンは直接見ることはありません。アプリはアクセストークンを利用してリソースサーバーから情報を取得し、適宜加工してブラウザに表示させます。
この時取得されるリソースからユーザー情報を取得し、認証を行うことで、認証も一緒に行うことができる。これがOAuth2の認証の流れ。
Implicit Flowの場合は連携の承認の後、認可コードを省略し、アクセストークンをブラウザに直接渡す。なのでトークンが見れてしまい、セキュリティホールになりうる。
Reflesh Token Flow
アクセストークンは一度発行したら終わりというわけではなく、期限がついています。ずっと同じトークンが利用できてしまうとセキュリティ上都合良くないですね。
アクセストークンを取得する際に、リフレッシュトークンと呼ばれるアクセストークンを更新するためのトークンも取得することができます。
期限が切れた場合は、リフレッシュトークンを使用して、新しいアクセストークンを取得することで引き続き利用できます。
- Implicit Flowではリフレッシュトークンは取得できないので注意。むしろ取得できたらやばみ
Implicit Flowで認証してはいけない理由
上2つで補足しているので大体わかってるとは思いますが、アクセストークンをユーザーが直接見れてしまうということは、「〇〇と連携しますか?~XXからのリクエストを許可しますか?」のフローをぶっ飛ばしてリソースを取得できる方法が取れてしまうということです。
対策としては、
- アクセストークンの発行対象を確認するようにする
- OpenID Connectのトークンを使用する
などして、「このトークンはどのアプリに対して発行されたものなのか?」を判別することです。ただ、この仕組みはOAuth2では提供されてないっぽいので、ここでOpenID Connectのトークンを使うことになります。
なので結論現状ではOAuth2の Implicit Flowのみで認証を行うのは非推奨扱いになってると思われる。
OpenID Connect
ちょこちょこ名前が出てきたOpenID Connectについても見ていく。 OpenID ConnectはOAuth2とは異なり、認証の仕組みを提供する仕様。
OpenID Connect = OAuth2(認可の仕組み) + 認証
と表現されてるとこが多い。
OpenID Connectを利用することで、「誰向けのトークンなのか?」という判別ができるようになり、Implicit Flowであったような脆弱性に対応できます。
フロントとバックの分離したアプリなどでフロントエンドで認証を行う場合はこれを使用するのが良さそう。
OpenIDの特徴として、IDTokenというものが使えるようになる。
OpenIDの発行フローを見ていく
OpenID Connectの発行フローとしては、response_type, scopeの組み合わせで決まる。 認可コード + アクセストークン + IDトークンの組み合わせ、あるいはnoneを指定できる。
クライアントサイドで認証認可を行いたい場合は、アクセストークン(token)と、IDトークン(id_token)を取得するように実装する必要がある。
また、それとは別に、認可エンドポイントとトークンエンドポイントの2つから何が取得できるのかを指定することができる。
今回の場合は認可エンドポイントから、アクセストークンと、IDトークンを取得する。IDトークンの中には、「誰向け?」「有効期限」「発行者はだれ?」という情報が入っています。(JWT)
このIDトークンを利用して、各バックエンドサーバーにアクセスする。バックエンドではIDトークンを検証して、レスポンスを返す。
JWT(JSON Web Token)
ジョット
大事なことなので最初に言いますが、JWTはジョットとよみます。ここテストに出るので忘れないように。
JWTは2者間で安全に情報を伝達するためのトークンである。 含まれている情報はデジタル署名しているので、改ざんできません。(したらわかるよ)
発行者と発行者保有する情報を持っている者だけがチェックできる。共通鍵とか公開鍵とかのあれ。
JWTは「署名」であって「暗号化」ではないのでユーザー情報が入ってる場合、閲覧自体はだれでもできる。
暗号化のオプションもあって、 JSON Web Encryption(JWE)というらしい。
また、サイズが小さいことも特徴の一つ。 URLやPOSTパラメーター、ヘッダーに入れて送信することができる。
JWTの中身
JWTはドット(.)で区切られていて、3要素から成り立っています。
- Header
- Payload
- Signature
順番も上記の順でドットでつながっている。 例で言うと hogehoge.hugahuga.piyopiyo
Header = hogehoge Payload = hugahuga Signature = piyopiyo
https://jwt.io/ ここで検証するのがおすすめ
Header
ヘッダーの中身はトークンの種類と、アルゴリズムが記載されている。
{
"alg": "H256",
"typ": "JWT"
}
Payload
ペイロードにはキーと値のペア情報の形で色々なデータを詰め込めます。ユーザー情報とかもここで入れます。Claim(クレーム)っていうらしい。
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
iss => Issuer。発行者のこと。Googleとか。 sub => Subject。誰を認証したのか?という情報。UserId1の人とか。 aud => Audience。利用者のこと。自分のアプリ。 nonce => Number Used Once リプレイアタック対策の文字列
Signature
ヘッダーとペイロード、シークレットを組み合わせたもの。改ざんされていないかどうかを検証するために使われる部分。
JWTのフロー
Googleログインを例にあげます。
- Googleでログインをクリックする
- ログインしたら、JWTが返ってくる
- JWTをローカルストレージとかクッキーなどに保存する
- ヘッダーにJWTを埋め込んでアプリにリクエストを飛ばす
- JWTを読み取って改ざんされていないかを確認する
- 問題なければJWTの中のユーザー情報を使用して処理する
3の、ローカルストレージ vs cookieはどっちがいいのかわからない。
ローカルストレージの場合は、同一生成元ポリシーによって保護されている。cookieに保存する場合はなんか対策が必要そう。
それとは別途XSSの危険性もあるので、そこは気をつける。Railsであればhtml_safeとかしない限りはフレームワークが守ってくれたりするので、対策しましょう。VueとかReactも対策してくれていますが ```dangerouslySetInnerHTML````とかするところは注意しましょう。
コード書いてないけど長くなったので一旦ここまで。