SPAに認証機能を付ける!!ついでにOAuth2.0の認可コードフローの全体概要を理解する

ネット上の記事や技術本を参考にFirebaseやGoogleの認証を組み込んでログイン機能をSPAに付けた経験があるフロントエンドエンジニアの方は多いと思います。
しかし、認可コードフローを理解してそれらを利用している方はそこまで多くないかもしれません。

(そこまでフロントエンドエンジニアに求めないのが普通なのかもしれませんが、、、)

 

僕が開発リーダーをやっている開発チームのメンバーには認証認可周り詳しい人がおらず、
リリース間際の脆弱性診断で認証認可周りの突っ込みを受けて自分で一から調べて自分で対応するという悲惨な目にあいました、、、、

 

そんな悲惨な経験から
SPA開発に携わるエンジニアの方なら最低限理解しておくべきだと感じた、認可コードフローの全体概要をまとめました。

この記事を書いている僕はシステムエンジニア8年目です。
最近はnuxt(vue.js)でフロントエンド、spring(java)でAPIの開発をしたり、DB周りのチューニングをしたり、認可サーバ周りの設定をしたり、開発リーダーをしたりしてます。

構築したシステムの構成

長くなるので、今回は実際に認可サーバの設定、フロントエンド、リソースサーバの細かい設定や開発周りは省略しますが、
アプリの構成はこんな感じで、ローカル環境でこれらを動かして動作確認した際の実際のリクエストやレスポンスを例に交えながら説明していきます。

FireBaseでもGoogle認証でもないkeycloakというミドルウェアを例に説明するのは少し微妙かもしれませんが、、
OAuth2.0、OIDCなどの仕様を満たすように認可サーバは構築されているのが一般的なはずなので、
認可コードフローの全体概要を抑えることを目的とするならこのシステム構成でも問題ないと思います。

SPAでログインしてリソースサーバからリソースを取得するまでのフロー概要

絵にするとこんな感じ

上のフローで重要なポイントに絞って説明していきます。

認可リクエスト

keycloak-jsを利用して未ログイン状態でユーザがSPAにアクセスしたら、
認可リクエストパラメータを作成して認可エンドポイントにアクセスします。
ちなみに該当処理はこの辺りです

>>認可エンドポイントリクエストのパラメータを作成しているところ

>>認可エンドポイントにアクセスしているところ

ここではパラメータについての詳細な説明は省略しますが、
興味がある方はAuthlete川崎さんの>>OAuth 2.0 全フローの図解と動画を参考にするのがオススメです。

認可エンドポイントリクエスト例
GET http://localhost:8180/auth/realms/レルム名/protocol/openid-connect/auth
 ?client_id=frontend
 &redirect_uri=http%3A%2F%2Flocalhost%3A3000%2F
 &state=1cda14d9-cd93-4744-89b6-808b799c8013
 &response_mode=fragment
 &response_type=code
 &scope=openid
 &nonce=62e3d51f-5f96-40a8-8e33-34fed33dc08a
 &code_challenge=x7dfF7VqbW1oA0HvuLgZATVwkgfoo_3zeGukAPXKsgo
 &code_challenge_method=S256

認可コード取得

ログイン画面でIDとパスワードを入力してログインすると認可決定エンドポイントにアクセスして認可コードが返却されます。

認可決定エンドポイントレスポンス例
HTTP/1.1 302 Found
Location:http://localhost:3000/
 #state=1cda14d9-cd93-4744-89b6-808b799c8013
 &session_state=718f7bc5-da43-4fa7-b86f-6a936475203e
 &code=1e5712f4-8d9d-4009-9089-32639892d871.718f7bc5-da43-4fa7-b86f-6a936475203e.a58f25ac-2660-414d-ae3f-725ec78465b1

アクセストークンリクエスト、アクセストークン返却

keycloak-jsでアクセストークンを取得してリソースサーバ呼び出しのために、例えばaxiosの共通ヘッダのAuthorizationに設定しておきます。

アクセストークンエンドポイントリクエスト例
POST http://localhost:8180/auth/realms/resona/protocol/openid-connect/token
 code: 1e5712f4-8d9d-4009-9089-32639892d871.718f7bc5-da43-4fa7-b86f-6a936475203e.a58f25ac-2660-414d-ae3f-725ec78465b1
 grant_type: authorization_code
 client_id: frontend
 redirect_uri: http://localhost:3000/
 code_verifier: E1c0gRF13FrdGf06WxhpB4qlzftePoTAHhf2ZJ7GxygcEKvu81FUAnhFqvSJTa6G3zBq2caFfX1XFWQnaOevdxAEaw6hL2t7
アクセストークンレスポンス例
{"access_token":"eyJhbGciOiJSUzIH・・・"
 ,"expires_in":60
 ,"refresh_expires_in":140
 ,"refresh_token":"eyJhbGciOiJIUzI1・・・"
 ,"token_type":"Bearer"
 ,"id_token":"eyJhbGciOiJSUzI1・・・"
 ,"not-before-policy":0
 ,"session_state":"718f7bc5-da43-4fa7-b86f-6a936475203e"
 ,"scope":"openid profile"}

今回はアクセストークンをkeycloak-jsライブラリの変数で保持させていますが、
localStorageや変数で保持させているとXSS攻撃を受けるとアクセストークンにアクセス(抜き取られる)できてしまいます。

その対策としてはそもそものXSSに対する根本対応を取るべきだと僕は考えています。

ただ、、、
アクセストークンを盗まれてもそれだけでは不正にリソースサーバにアクセスできないようにDPoPを導入するか、、
フロントエンドにアクセストークンを晒さないようにBFFを用意してそこで認可サーバとやり取りをさせる仕組みにした方がよりセキュアです。(が、、、対応工数と効果と求められるセキュリティ要件とを天秤にかけて僕が携わっているプロダクトでは現状そこまでやっていないです)

リソースアクセス、トークン検証

SPAはAuthorization ヘッダ付きでリソースサーバのエンドポイントをコールし、
リソースサーバはspring-boot-starter-oauth2-resource-serverを利用して認可サーバのトークンイントロスペクションエンドポイントを利用してアクセストークンを検証します。

ちなみに上記の②~⑦までのアクセストークン発行までの仕様を定めているものがOAuth2.0と呼ばれるものであり、
それを発展させてIDトークン発行の仕様を定めているものがOIDCです。

まとめ

ここまで、アクセストークンが発行されるまでの流れや、
発行されたアクセストークンをリソースサーバでトークンイントロスペクションで検証してリソースを返却する流れを見てきました。

SPAの開発に携わるエンジニアの方(バックエンド、フロントエンド、インフラに関わらず)は最低限この辺りはざっくり理解しておくことで、認証認可周りのどこかで問題があったときに原因調査のあたりがつけやすくなります。(多分。。。)

さらに、
今回説明した認可コードフローの概要がつかめたら、 Financial-grade API(FAPI)といったさらに高セキュアな仕様もそこまでつまることなく理解できるはずです。
(ちなみにkeycloakはFAPIや他にもCIBA等にも対応しており、まだまだ使い倒したいと思っています。)

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください