API連携を安全にする OAuth 2.0/OpenID Connect 権限スコープの設計と実践
はじめに:API連携と権限管理におけるスコープの重要性
現代のシステム開発において、他のサービスとのAPI連携は不可欠です。ユーザーデータの取得、外部サービスの機能利用など、様々なシナリオでAPIが活用されています。しかし、API連携においては、連携先のシステムに対して「どこまでの操作を許すか」という権限管理が極めて重要になります。不適切な権限付与は、情報漏洩や意図しないデータ操作といった深刻なセキュリティリスクを招く可能性があるためです。
ここで中心的な役割を果たすのが、OAuth 2.0およびOpenID Connectにおける「スコープ」という概念です。スコープは、クライアントアプリケーションが保護リソース(APIなど)に対して要求する、あるいは認可サーバーが付与する権限の範囲を定義します。適切にスコープを設計・利用することで、アプリケーションが必要最低限の権限のみを要求・使用できるようになり、セキュリティの向上に繋がります。
本記事では、経験数年程度のソフトウェアエンジニアの皆様が、日々の開発業務で直面するAPI連携時の権限管理の課題解決に役立てていただけるよう、OAuth 2.0およびOpenID Connectにおけるスコープの基本概念から、実践的な設計方法、そしてアプリケーションでの具体的な使い方までを詳しく解説します。
OAuth 2.0とOpenID Connectにおけるスコープの基本概念
スコープとは何か?
スコープ(Scope)は、OAuth 2.0やOpenID Connectにおいて、クライアントアプリケーションがアクセスできる保護リソース(例えばAPI)の範囲や、実行できる操作の種類を限定するために使用される識別子です。認可サーバーは、ユーザーの同意やポリシーに基づいて、クライアントに許可するスコープを決定し、発行されるアクセストークンにその情報を付与します。リソースサーバー(APIを提供する側)は、受け取ったアクセストークンに含まれるスコープを確認することで、クライアントからのリクエストが許可された操作範囲内であるかを検証します。
OAuth 2.0におけるスコープの役割
OAuth 2.0は、認可(Authorization)のためのフレームワークです。ユーザーに代わって、特定のアプリケーション(クライアント)に保護リソースへのアクセス権限を限定的に委任することを目的としています。スコープは、この「限定的なアクセス権限」の範囲を具体的に指定するために使われます。
例:
* read:data
: データの読み取りを許可
* write:data
: データの書き込みを許可
* delete:user
: ユーザーの削除を許可
クライアントは認可リクエスト時に必要なスコープを認可サーバーに伝えます。認可サーバーは、ユーザーの同意を得た上で、要求されたスコープの中から許可するものを選択し、アクセストークンに含めて発行します。
OpenID Connectにおけるスコープの役割
OpenID Connect(OIDC)は、OAuth 2.0をベースにした認証(Authentication)レイヤーです。ユーザーのID検証や、ユーザーに関する基本的な属性情報(プロファイル情報)の取得を目的としています。OIDCにおけるスコープは、OAuth 2.0の役割に加え、主に認証結果として取得できるユーザー属性(Claim)の種類を指定するためにも使用されます。
OIDCにはいくつかの標準スコープが定義されています。
openid
: OIDCのリクエストであることを示し、IDトークン発行を要求します。これがなければOIDCフローは開始されません。profile
: ユーザーの基本的なプロファイル情報(名前、Picture、Zoneinfoなど)の取得を要求します。email
: ユーザーのメールアドレスや検証状況の取得を要求します。address
: ユーザーの住所情報の取得を要求します。phone
: ユーザーの電話番号や検証状況の取得を要求します。
これらの標準スコープに加えて、OAuth 2.0と同様にカスタムスコープを定義し、アプリケーション固有の認可情報を扱うことも可能です。
標準スコープとカスタムスコープ
- 標準スコープ: OAuth 2.0やOpenID Connectの仕様で定義されている、あるいは広く普及しているスコープです。OIDCの
openid
,profile
,email
などがこれにあたります。OpenID Connect Discoveryエンドポイントなどでサポートされている標準スコープを確認できます。 - カスタムスコープ: サービス提供者が独自に定義するスコープです。APIのエンドポイントごと、リソースの種類ごと、操作の種類ごとなど、アプリケーションの要件に合わせて柔軟に設計できます。例えば、ECサイトであれば
read:order
,write:product
,manage:cart
のようなカスタムスコープが考えられます。
実践的なスコープ設計
安全かつ効率的なシステムを構築するためには、慎重なスコープ設計が求められます。以下の点を考慮してスコープを設計することをお勧めします。
1. 最小権限の原則に基づいた設計
セキュリティの基本原則の一つに「最小権限の原則(Principle of Least Privilege)」があります。これは、ユーザーやシステムには、そのタスクを遂行するために必要最低限の権限のみを与えるべきであるという考え方です。スコープ設計においてもこの原則を適用します。
- 必要以上に広範なスコープを定義しない: 例えば、
read:all
のように全てのデータを読み取れるスコープではなく、read:users
,read:orders
のようにリソースごとに細分化します。 - 必要以上の操作権限を組み合わせない: 例えば、
read_write:data
のようなスコープは避け、read:data
とwrite:data
のように操作ごとに分けます。 - クライアントの役割に応じたスコープセットを検討する: Webアプリケーション、モバイルアプリケーション、バッチ処理など、クライアントの種類によって必要な権限は異なります。それぞれのユースケースで必要となる最小限のスコープセットを定義します。
2. APIエンドポイントやリソースに応じたスコープ定義
マイクロサービスアーキテクチャなど、APIが多数存在する環境では、APIエンドポイントやリソースごとにアクセス制御を行うことが一般的です。スコープをこれらの単位に対応させて定義すると、リソースサーバーでの検証が容易になります。
例:/users
エンドポイントへのGETリクエストにはread:users
スコープ、/orders
エンドポイントへのPOSTリクエストにはwrite:orders
スコープが必要、といった設計です。
スコープ命名規則として、{操作}:{リソース名}
のような形式を採用すると、分かりやすく管理しやすくなります。例: get:users
, post:users
, get:orders/{id}
, put:orders/{id}
, delete:orders/{id}
など。あるいは、特定のサービスや機能に関連付けたスコープ名も有効です。例: payment:process
, shipping:update_status
.
3. クライアントの種類とスコープの関連性
前述の通り、クライアントの種類によって必要な権限は異なります。
- ユーザー操作を伴う対話型クライアント(Webアプリ、モバイルアプリ): ユーザーが明示的に許可する範囲のスコープを要求します。ユーザーがデータ閲覧だけを行う場合は
read:...
系のスコープのみ、データ登録も行う場合はwrite:...
系のスコープも要求するなど、ユーザーの操作権限に応じます。 - 非対話型クライアント(バッチ処理、サーバー間通信): 特定のタスクを実行するために必要な最小限の固定的なスコープを付与します。例えば、日次バッチでレポート生成のためにデータを読み取るクライアントには
read:reports_data
スコープのみを付与します。
認可サーバーは、クライアントの種類(Client Type)や登録されている情報に基づいて、要求されたスコープがそのクライアントに付与可能かを判断することも重要です。
4. スコープ命名規則の重要性
一貫性のある分かりやすいスコープ命名規則を採用することで、開発者や運用者がスコープの意味を正確に理解しやすくなります。
- 小文字とコロン
:
、スラッシュ/
、ハイフン-
などを使用する。 {操作}:{リソース}
や{サービス}:{機能}
など、構造化された命名にする。- スコープ名だけで何ができるか、何にアクセスできるかがある程度推測できるようにする。
5. スコープ設計時の考慮事項
- 粒度: スコープの粒度をどれだけ細かくするかは設計上のトレードオフです。細かすぎると管理が複雑になりますが、最小権限を厳密に適用できます。粗すぎると管理は楽ですが、過剰な権限付与のリスクが高まります。システムやデータの機密性、開発・運用の体制などを考慮して適切な粒度を決定します。
- 拡張性: 将来的に機能が追加されたり、新しいリソースが登場したりする場合に、既存のスコープ定義を大きく変更することなく対応できるような命名規則や構造を検討します。
- セキュリティリスク: 誤ったスコープ設計や設定は、セキュリティホールに直結します。特に機密性の高いデータや操作に関わるスコープは、慎重に設計・管理する必要があります。
アプリケーションでのスコープの使い方(実装例)
実際にクライアントアプリケーション、認可サーバー、リソースサーバーがどのようにスコープを扱うかを見ていきましょう。
クライアント側:認可リクエストでのスコープ指定
クライアントアプリケーションは、ユーザーに代わって保護リソースへのアクセスを要求する際、認可サーバーへのリクエストに希望するスコープを含めます。これは通常、認可エンドポイントへのリダイレクトURLのクエリパラメータとして渡されます。
GET /authorize?
response_type=code&
client_id=s6BhdRkqt3&
state=xyz&
redirect_uri=https%3A%2F%2Fclient.example.com%2Fcb&
scope=openid%20profile%20email%20read%3Aorders
(URLエンコードされているため、scope=openid profile email read:orders
をURLエンコードした結果です。)
ここで、scope
パラメータに複数のスコープをスペース区切りで指定しています。認可サーバーは、このリクエストを受け取り、ユーザーにこれらのスコープに対するアクセス許可を求める同意画面を表示します。
リソースサーバー側:アクセストークンに含まれるスコープの検証
リソースサーバー(APIを提供する側)は、クライアントからのリクエスト時に提供されたアクセストークンを受け取ります。このアクセストークンを認可サーバー(または専用のイントロスペクションエンドポイントなど)で検証し、トークンが有効であること、そしてリクエストされた操作を実行するために必要なスコープが含まれていることを確認します。
アクセストークンがJWT(JSON Web Token)形式である場合、スコープ情報はPayload部分にクレームとして含まれていることが多いです(例: "scope": "read:orders write:orders"
)。不透明なトークン(opaque token)の場合は、認可サーバーのイントロスペクションエンドポイントに問い合わせてスコープ情報を取得します。
リソースサーバーでは、APIエンドポイントごとに要求されるスコープを定義しておき、リクエストが来るたびにアクセストークンのスコープと照合します。
Python + FastAPI でのリソースサーバー側のスコープ検証の簡略例:
from fastapi import FastAPI, Depends, HTTPException, Security
from fastapi.security import OAuth2BearerToken
# ダミーのトークン検証関数 (実際には認可サーバーと連携)
# この例では、トークン文字列からスコープ情報を取得すると仮定
def get_token_scopes(token: str) -> set[str]:
# 実際のアプリケーションでは、ここで認可サーバーの
# トークンイントロスペクションエンドポイントを呼び出すなどして
# トークンの有効性確認とスコープ情報の取得を行います。
# ここは簡略化のため、特定のトークン文字列にスコープを紐付けます。
if token == "valid_token_with_read_orders":
return {"read:orders"}
elif token == "valid_token_with_write_orders":
return {"write:orders"}
elif token == "valid_token_with_all_orders":
return {"read:orders", "write:orders"}
else:
# 無効なトークンとして扱う
return set()
# カスタムOAuth2BearerTokenクラスを作成し、スコープ検証を組み込む
class ScopedOAuth2BearerToken(OAuth2BearerToken):
def __init__(self, auto_error: bool = True):
super().__init__(auto_error=auto_error)
# __call__ メソッドをオーバーライドしてスコープ検証を追加
async def __call__(self, request) -> str:
token = await super().__call__(request) # まずは標準のトークン検証
# ここでスコープ検証を行うロジックを追加
required_scopes: set[str] = getattr(request.scope["route"], "required_scopes", set())
if required_scopes:
provided_scopes = get_token_scopes(token)
if not provided_scopes.issuperset(required_scopes):
raise HTTPException(
status_code=403, # Forbidden
detail="Insufficient scope",
headers={"WWW-Authenticate": self.scheme_name},
)
return token # 検証OKならトークンを返す
# カスタム依存性注入可能なオブジェクトを作成
oauth2_scheme = ScopedOAuth2BearerToken()
# エンドポイントに関数を追加してスコープを定義
def require_scopes(*scopes: str):
def decorator(func):
# 依存性注入のリストにoauth2_schemeを追加
if not hasattr(func, "__dependencies__"):
func.__dependencies__ = []
# ScopedOAuth2BearerTokenのインスタンスを依存性として追加
func.__dependencies__.append(Depends(oauth2_scheme))
# 必要なスコープを関数オブジェクトに付与(ScopedOAuth2BearerToken.__call__ で参照される)
# FastAPIの内部構造を利用しているため、プロダクションコードではライブラリ等の利用を推奨
# 例として、依存関係の解決時にrouteオブジェクトにアクセスできることを利用
func.required_scopes = set(scopes)
return func
return decorator
app = FastAPI()
# スコープ read:orders が必要
@app.get("/orders")
@require_scopes("read:orders")
async def read_orders(token: str = Depends(oauth2_scheme)):
# ここに orders を読み取るロジック
return {"message": "Orders data", "scopes": get_token_scopes(token)}
# スコープ write:orders が必要
@app.post("/orders")
@require_scopes("write:orders")
async def create_order(token: str = Depends(oauth2_scheme)):
# ここに order を作成するロジック
return {"message": "Order created", "scopes": get_token_scopes(token)}
# スコープ read:users が必要 (この例では有効なトークンがないためアクセス不可)
@app.get("/users")
@require_scopes("read:users")
async def read_users(token: str = Depends(oauth2_scheme)):
return {"message": "Users data", "scopes": get_token_scopes(token)}
# スコープが不要なエンドポイント
@app.get("/")
async def public_endpoint():
return {"message": "Public access"}
上記の例では、@require_scopes("...")
デコレーターを使って、各APIエンドポイントに必要とされるスコープを指定しています。ScopedOAuth2BearerToken
クラスがトークンを受け取り、定義されたスコープが含まれているかを確認し、含まれていなければ403 Forbiddenエラーを返します。実際には、トークン検証やスコープ抽出の部分は、OAuth/OIDCライブラリや認可サーバーとの連携によって実装されます。
認可サーバー側:ユーザーの同意画面とスコープ付与ロジック
認可サーバーは、クライアントから要求されたスコープをユーザーに提示し、同意を得るための画面(同意画面、Consent Screen)を表示します。ユーザーはこの画面で、クライアントに許可するスコープを選択または確認します。
認可サーバーは、以下の要素に基づいて最終的に発行するアクセストークンに含めるスコープを決定します。
- クライアントが要求したスコープ
- ユーザーが同意したスコープ
- 認可サーバーのポリシー(例: 特定のクライアントタイプには特定のスコープしか許可しない、ユーザーのロールや属性に基づいて許可するスコープを制限するなど)
- ユーザー自身に紐づく権限情報(ユーザーがどのデータにアクセスする権限を持っているかなど、より細かいレベルでの制御を行う場合)
例えば、クライアントがread:orders
とwrite:orders
を要求し、ユーザーがread:orders
のみに同意した場合、発行されるアクセストークンにはread:orders
スコープのみが含まれます。
スコープ管理の注意点とベストプラクティス
スコープの変更管理
スコープ定義はシステムのAPIと共に進化していく可能性があります。新しい機能の追加に伴い新しいスコープが必要になったり、既存のスコープの定義を変更したりすることがあります。
- 後方互換性: 既存のクライアントアプリケーションに影響を与えないよう、既存のスコープを削除したり、意味を大きく変更したりする場合は慎重に行います。新しいスコープを追加する方が影響は少ないです。
- 文書化: 定義されているスコープとその意味、用途、必要な権限レベルなどを明確に文書化し、クライアント開発者や関係者が参照できるようにします。
動的スコープ vs 静的スコープ
- 静的スコープ: 認可サーバーにクライアントを登録する際に、そのクライアントが要求できるスコープを事前にリストとして登録しておく方法です。セキュリティが高まりますが、クライアントごとに許可するスコープが変わるような柔軟な運用は難しくなります。
- 動的スコープ: クライアントが認可リクエスト時に任意のスコープを要求できる方法です。柔軟性は高いですが、認可サーバー側での検証(要求されたスコープが有効なものであるか、そのクライアントに許可可能かなど)がより重要になります。
ほとんどのOAuth/OIDC実装では、事前に定義されたスコープの中からクライアントが選択して要求する、という半静的な運用が一般的です。
セキュリティリスク
- 過剰なスコープ付与: クライアントに必要以上のスコープを付与してしまうと、そのクライアントに脆弱性があった場合に被害が拡大するリスクがあります。最小権限の原則を徹底することが重要です。
- スコープの誤った検証: リソースサーバーがアクセストークンのスコープを正しく検証しないと、スコープによって制限されるはずの操作が無制限に実行されてしまう可能性があります。スコープ検証ロジックは慎重に実装し、テストを実施します。
- 機密性の高い情報のスコープ名での漏洩: スコープ名自体に機密性の高い情報を含めないようにします。
デバッグ時のスコープ確認方法
開発中や運用中にアクセストークンに含まれるスコープを確認したい場合があります。
- JWTの場合: JWTデコーダー(オンラインツールなど)でトークンのPayloadを確認します。ただし、本番環境のトークンを安易に外部ツールに入力するのは避けてください。
- 不透明なトークンの場合: 認可サーバーが提供するイントロスペクションエンドポイントにトークンを送信して詳細情報を取得します。
まとめ
OAuth 2.0およびOpenID Connectにおけるスコープは、API連携を安全に行うための重要な要素です。スコープを適切に設計し、最小権限の原則に基づいた権限管理を実践することで、システム全体のセキュリティレベルを向上させることができます。
本記事では、スコープの基本的な概念から、APIエンドポイントやリソースに応じた具体的な設計方法、そしてクライアント・リソースサーバーにおける実装のポイントまでを解説しました。API連携機能を開発する際には、ぜひスコープ設計に時間をかけ、各システムコンポーネントでスコープが正しく扱われるように注意してください。
権限設計は、一度行えば終わりというものではなく、システムの進化に合わせて見直しや改善を継続的に行う必要があります。本記事で解説した内容が、皆様のシステム開発における安全な権限管理の一助となれば幸いです。