はじめに
記事を閲覧いただきありがとうございます。普段は仕事でSpring Bootを用いたバックエンドと、React・React NativeなどによるWeb/モバイルアプリの開発を行っています。今回、突然の長期休暇をもらったのを機に、以前から理解の浅さを感じていた「認証・認可」を改めて学び直すことにしました。昨年も書籍でOAuthやOIDCを学び、自身の手でログイン機能は実装できるようになっていましたが、他者から質問を受けた際やSpring SecurityのFilterChainをカスタムする場面では知識不足を痛感していました。そこで今回は「Security FilterChainを自分でカスタムできる」ことを目標に学習し、その過程を備忘録としてまとめます。
本記事の内容
本記事では、認証・認可のうち「認可」にあたるOAuthをテーマに、学んだ知識と実務経験を交えて整理します。最終章では筆者が扱うWEB・モバイルアプリケーションのそれぞれでどのグラントタイプを使っているのかについて整理していますので、参考になればと思います。OAuthの詳細な仕様やコード解説は、すでに基本を理解していることを前提として省略しますので、必要に応じて書籍や公式ドキュメントを参照してください。
学習に使用した書籍について
今回の学習では、こちらの書籍を活用しました。OAuthで登場するさまざまなグラントタイプについて、シーケンス図を交えて丁寧に解説されており、フローの全体像がつかみやすい一冊です。OAuthをこれからしっかり理解したい方には、ぜひ手に取って読んでみることをおすすめします。
OAuthについて
ここからは、今回の学習で整理したOAuthの概要を簡潔にまとめます。
OAuthとは
書籍では、OAuthを「サードパーティアプリケーションによるHTTPサービスへの限定的なアクセスを可能にする認可フレームワーク」と説明しています。
たとえば、私たちが開発するアプリがGoogleフォトと連携して写真を取得するとします。このとき、「写真への閲覧権限」など必要な範囲だけを許可し、アプリからGoogleフォトへアクセスできるようにするーーこれがOAuthの役割と理解しました。
OAuthがないと起こる問題
OAuthについてざっくり理解したところで、逆にOAuthがないと何が起こるのでしょうか。
OAuthを使わない場合、Googleフォトを利用するためのユーザー名とパスワードを、私たちが作ったアプリ内で保持する必要があります。これにより次のような問題が発生します。
1. クレデンシャル(ユーザー名とパスワード)が漏洩するリスクが高まる
2. クレデンシャルを入手した悪意あるユーザーがGoogleフォトに対するすべてのCRUD操作を実行できてしまう
3. 漏洩に気づいた際は、クレデンシャルを変更せざるを得ず、ユーザーに負担をかける
OAuthではこの問題を解決するために以下の特徴を持ったアクセストークン発行を発行します。
1. 有効期限付き
2. オーナーが許可した範囲に限定されたアクセス権限
3. アプリごとのアクセス許可設定
これにより、ユーザーのパスワードをアプリに直接渡すことなく、必要な範囲だけGoogleフォトへ安全にアクセスできるようになります。
クライアントに応じた適切なグラントタイプの選定
いよいよ本記事のメインとなる章です。ここではOAuthのどのグラントタイプを開発中のアプリに適用すべきかについて、理解した内容をまとめます。なお、各グラントタイプの詳細説明は割愛し、実務視点での選び方にフォーカスします。
説明上の登場人物
書籍にも同様の図が出てきますが、ここでもOAuthのロールを簡易的に示します。
クライアントの種別
我々が開発するアプリに対応するクライアントは、主に以下の2種類に分類されます。
-
クレデンシャルクライアント(Confidential Client)
クライアントシークレットを安全に保持できる環境のクライアント
(例:サーバーサイドアプリ、バックエンドAPI) -
パブリッククライアント(Public Client)
クライアントシークレットを安全に保持できない環境のクライアント
(例:SPA、モバイルアプリ)
後に説明しますが、この分類が適切なグラントタイプの選定や認証機能の設計に重要な情報となってきます。
グラントタイプの選定
ここでは筆者が以前開発していたWEB・モバイルアプリケーションの構成をもとにどのグラントタイプを使っているのか理解を深めます。
フロントエンド(WEB/React)、バックエンド(Spring Boot)のパターン
このパターンではフロントエンドがパブリッククライアント、バックエンドがクレデンシャルクライアントにあたります。バックエンドはクライアントシークレットを安全に保持できるため、認可コードグラントを用いてログインを処理します。
各ロールの関係を理解しやすくするため、簡略化した図を作成しました(細かいフローは省略しています)。
フロントエンド(Web)はバックエンド経由でログインし、セッション管理で認証状態を保持します。また、認可サーバーとやりとりし、トークンを処理するのは全てバックエンドです。このことから、この構成では専用のリソースサーバは登場しないことがわかります。このことは、後にSpringBootで実装する際にspring-security-oauth2-resource-serverの依存を使用しないことと大きく関係しています。
フロントエンド(mobile/Swift,ReactNative)、バックエンド(SpringBoot)のパターン
このパターンも同様に、フロントエンドがパブリッククライアント、バックエンドがクレデンシャルクライアントです。よって先ほどと同様の認可コードグラントでいけるのでは???と思いつつ、筆者の感覚ではバックエンドを介さずにフロントエンドだけでアクセストークンを取得できていたことを思い出し、混乱していました。
そこで書籍や参考サイトを読み進めてみると、モバイルアプリはOAuthライブラリ(MSAL、AppAuthなど)が充実しており、フロントで直接トークンを扱いやすい例があるからとのこと。この場合、先に述べた通りフロントエンドではクレデンシャルを安全に保持できないため、クライアントシークレットを使わずにトークンの取得が可能なインプリシットグラントを選ぶことになります。しかし、インプリシットグラントは認可コードの横取りが起こることから非推奨となっています。その代わりに、ランダムな文字列(コードベリファイア)を使用してトークン発行までが可能な認可コードグラント+PKCEを使用することが現在推奨されているそうです。つまり、このパターンでは認可コードグラント+PKCEを使用していることがわかりました。
こちらについても簡略化した図を作成しました。このパターンでは認可サーバーからトークンを取得するのはフロントエンド、トークンを処理するのはバックエンドです。よってバックエンドでは専用のリソースサーバーが必要になります。後にSpringBootで実装する際にはspring-security-oauth2-resource-serverの依存を使用します。
まとめ
今回の学習を通じて、OAuthの役割やアクセストークンの重要性、クライアントの種類によって適切な認可グラントタイプが異なることを理解できました。
特に、モバイルアプリではセキュリティ上の理由からPKCEを用いた認可コードグラントが推奨されている点は重要です。これにより、クライアントシークレットを保持できない環境でも安全に認証・認可が実現できます。
また、バックエンドでクライアントシークレットを安全に保持できる場合は、従来の認可コードグラント単体での実装も可能であり、アプリの設計に応じて使い分けが必要です。
今後は、これらの知識を踏まえつつ、Spring SecurityのFilterChainをカスタムし、自分で認証・認可の挙動を制御できるようになることを目指します。