drupal-simple-oauth について
Drupal simple_oauth で OAuth2 認証を実装する開発者向けのリファレンススキルです。トークンのスコープ、ユーザーのロールと権限の関係に加え、クライアント設定、モジュールの動作、トラブルシューティングを説明します。
主な機能
- 01トークンとユーザー権限の両方を確認
- 02スコープとロールの対応規則
- 03OAuth クライアントとスコープの設定
- 04JSON:API のフィールドアクセス動作
- 05トークン認可のトラブルシューティング
活用シーン
- 01トークンが API にアクセスできない原因の調査
- 02モバイルアプリ用 OAuth クライアントの設定
- 03ロールに対応する OAuth スコープの設定
- 04JSON:API のフィールドアクセス制御の確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ grasmash/ drupal-claude-skills --skill drupal-simple-oauthインストール後、Claude Code で/drupal-simple-oauthを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/bearer-csrf-bypass.mdconsumer-ttls-and-social-grant.mddebugging-tokens.md詳細
Drupal Simple OAuth のパターン
Drupal で OAuth2 認証に simple_oauth モジュールを使用するための包括的なパターンです。API 認証、モバイルアプリのトークン、OAuth2 の実装に取り組む際に使用します。
バージョン情報
- simple_oauth 6.1.1(consumers 8.x-1.24)に対して検証済みです。以下の行番号はこのリリースを参照しています
- スコーププロバイダー:動的(ロールベースの粒度)
- 現在の Drupal:10.x/11.x 互換
OAuth トークンの重要な概念
TokenAuthUser:認証の中核となるラッパー
OAuth トークンでリクエストが認証されると、Drupal はユーザーを TokenAuthUser デコレーターでラップし、トークンとユーザーの両方の権限を適用します。
場所: <webroot>/modules/contrib/simple_oauth/src/Authentication/TokenAuthUser.php(プロジェクトに応じて <webroot> は web/ または docroot/ です)
権限チェックのロジック(95 行目)
public function hasPermission($permission) {
// User #1 has all permissions.
if ((int) $this->id() === 1) {
return TRUE;
}
return $this->token->hasPermission($permission) && $this->subject->hasPermission($permission);
}
**重要なルール:**トークンとユーザーの両方がその権限を持っている必要があります(AND 条件)。
ロールの積集合のロジック(107 行目)
public function getRoles($exclude_locked_roles = FALSE) {
$default_roles = [];
if (!$exclude_locked_roles) {
$default_roles[] = $this->isAuthenticated() ? self::AUTHENTICATED_ROLE : self::ANONYMOUS_ROLE;
}
$token_roles = array_unique(array_merge($this->token->getRoles($exclude_locked_roles), $default_roles));
$user_roles = $this->subject->getRoles($exclude_locked_roles);
return array_intersect($token_roles, $user_roles);
}
**重要なルール:**トークンとユーザーの両方に存在するロールのみが付与されます(array_intersect)。
スコープとロールの一致要件
OAuth トークンで権限を付与するには、次の条件を満たす必要があります。
- ユーザーがその権限を持つロールを持っていること
- トークンリクエストに、そのロールと一致するスコープが含まれていること
- その名前の OAuth2 スコープエンティティが存在すること
いずれかの条件を満たさない場合、権限は拒否されます。
よくある落とし穴
落とし穴 1:スコープとロールの不一致
問題:
// User has: administrator role
// Token requested with: scope=api_consumer
// Result: Only 'authenticated' role granted (intersection)
// Permissions from administrator: DENIED
失敗する理由:
$token_roles = ['authenticated', 'api_consumer']; // From scope
$user_roles = ['authenticated', 'administrator']; // From user
$granted = array_intersect($token_roles, $user_roles); // ['authenticated']
**解決策:**正しいスコープを指定してトークンをリクエストします。
formData.append('scope', 'administrator');
落とし穴 2:スコープエンティティが存在しない
問題:
// Mobile app requests: scope=subscriber
// But no "subscriber" OAuth2 scope entity exists
// Result: Token has NO scopes, NO roles, NO permissions
**解決策:**OAuth2 スコープエンティティを作成するか、既存のスコープ名を使用します。
既存のスコープを確認する:
ddev drush config:get simple_oauth.settings
# Or list scope entities (config entities named simple_oauth.oauth2_scope.<id>)
ddev drush sqlq "SELECT name FROM config WHERE name LIKE 'simple_oauth.oauth2_scope.%' ORDER BY name"
落とし穴 3:認証済みロールの権限
**問題:**認証済みロールの権限が常に付与されると思い込むことです。
**実際の動作:**トークンのスコープの積集合に認証済みロールが含まれている場合に限り、権限が付与されます。
src/Plugin/ScopeGranularity/Role.php(95 行目)より:
// Scopes automatically grant authenticated role
return $exclude_locked_roles ? [$role] : [AccountInterface::AUTHENTICATED_ROLE, $role];
これは課題 #3451692 で修正されました(6.0.x に含まれています)。
他のモジュールとの連携
field_permissions モジュール
動作の仕組み:
// field_permissions.module (line 34)
function field_permissions_entity_field_access($operation, FieldDefinitionInterface $field_definition, $account, FieldItemListInterface $items = NULL) {
// ...
$access_field = \Drupal::service('field_permissions.permissions_service')
->getFieldAccess($operation, $items, $account, $field_definition);
// ...
}
// CustomAccess.php (line 36)
public function hasFieldAccess($operation, EntityInterface $entity, AccountInterface $account) {
// Calls $account->hasPermission()
// If $account is TokenAuthUser, uses the AND logic!
return $account->hasPermission($operation . ' ' . $field_name);
}
**結果:**スコープがロールと一致していれば、field_permissions は simple_oauth と正しく連携します。
JSON:API モジュール
JSON:API は field_permissions を含む、すべてのフィールドレベルのアクセスチェックに従います。OAuth トークンを使用する場合:
- JSON:API が
entity_field_accessフックを呼び出します - field_permissions が
$account->hasPermission()をチェックします $accountが TokenAuthUser の場合、トークンとユーザーの両方がその権限を持っている必要があります- どちらか一方でも満たさない場合、フィールドは JSON:API のレスポンスから除外されます
特別な設定は不要です。スコープが正しければ、自動的に動作します。
OAuth クライアントの設定
モバイルアプリのクライアント設定
モバイルアプリまたは分離型アプリを OAuth クライアントとして設定する場合、トークンリクエストは次のパターンに従います。
const formData = new FormData();
formData.append('client_id', clientId);
formData.append('client_secret', clientSecret);
formData.append('scope', scope); // MUST match user's role!
formData.append('grant_type', 'password');
formData.append('username', username);
formData.append('password', password);
アプリの設定で client_id と client_secret を安全に保管してください(環境変数やセキュアな設定コンテキストなど)。
OAuth クライアントの作成
# simple_oauth ships no Drush command for creating clients (its only command is
# simple-oauth:generate-keys). Clients are `consumer` content entities:
# Via UI: /admin/config/services/consumer/add
# Or via the entity API
ddev drush php:eval '\Drupal::entityTypeManager()->getStorage("consumer")->create([
"label" => "Mobile App",
"client_id" => "mobile_app",
"secret" => "your-secret",
"confidential" => TRUE,
"grant_types" => ["password", "refresh_token"],
"scopes" => ["authenticated"],
])->save();'
Consumer の TTL とソーシャルログインのオーディエンス
- Consumer トークンの TTL は設定ではなくコンテンツエンティティのデータです。
cimではデプロイされません。client_idで Consumer を読み込むhook_update_N()で変更してください。 - **ソーシャル/Google トークンのグラントでは、
audを許可リストと照合する必要があります。**モバイルアプリはそれぞれのクライアント ID でトークンを発行するため、期待するaudを 1 つだけ指定すると、すべてのアプリでログインが拒否されます。
詳細とコード:references/consumer-ttls-and-social-grant.md。
スコープエンティティの管理
スコープエンティティの作成
# Via config: config/install/simple_oauth.oauth2_scope.subscriber.yml
langcode: en
status: true
id: subscriber
name: subscriber
description: 'Subscriber role access'
grant_types:
refresh_token:
status: true
description: ''
password: # provided by the simple_oauth_password_grant submodule
status: true
description: ''
umbrella: false
parent: _none
granularity_id: role
granularity_configuration:
role: subscriber
動的スコーププロバイダーの設定
# simple_oauth.settings.yml
scope_provider: 'dynamic'
動的スコーププロバイダーでは、スコープがロールに直接対応します。
スコープの一覧表示
# Via SQL (scopes are config entities, stored in the config table)
ddev drush sqlq "SELECT name FROM config WHERE name LIKE 'simple_oauth.oauth2_scope.%' ORDER BY name"
# Via config
ddev drush config:get simple_oauth.oauth2_scope.subscriber
ベストプラクティス
1. スコープをユーザーロールに一致させる
推奨:
// Check user's roles, request matching scope
if (userHasRole('administrator')) {
scope = 'administrator';
} else if (userHasRole('premium_user')) {
scope = 'premium_user';
}
非推奨:
// Hardcode scope that might not match user
scope = 'subscriber'; // What if user has different role?
2. すべての API ロールにスコープエンティティを作成する
API アクセスが必要なすべてのロールに、対応するスコープエンティティがあることを確認します。
3. 複数のスコープをリクエストする(必要な場合)
OAuth2 は、スペース区切りのスコープに対応しています。
formData.append('scope', 'premium_user api_consumer');
トークンには、ユーザーのロールに一致するすべてのスコープが含まれます。
4. 具体的な権限を使用する
広範な権限ではなく、フィールド固有の権限を使用します。
// Good: Granular field control
'view field_premium_content'
'edit field_premium_content'
// Less secure: Too broad
'administer nodes'
5. 管理者以外のユーザーでテストする
管理者ユーザー(uid=1)は、すべての権限チェックをバイパスします。
if ((int) $this->id() === 1) {
return TRUE; // Admin bypass!
}
OAuth のテストは、必ず一般ユーザーで行ってください。
トラブルシューティングのチェックリスト
OAuth の権限で問題が発生した場合:
- OAuth2 スコープエンティティは存在しますか?
ddev drush config:get simple_oauth.oauth2_scope.SCOPE_NAME
- ユーザーは必要なロールを持っていますか?
ddev drush user:role:list [email protected]
- ロールには必要な権限がありますか?
ddev drush role:perm:list ROLE_NAME | grep PERMISSION
- トークンリクエストに正しいスコープが含まれていますか?
- モバイルアプリのコードまたは API クライアントの設定を確認してください
- スコープはトークンリクエスト内で、ユーザーのロールに一致していますか?
- これは最もよくある問題です!
- uid=1 でテストしていますか?(いいえ、一般ユーザーを使用してください)
- simple_oauth のスコーププロバイダーは「dynamic」に設定されていますか?
ddev drush config:get simple_oauth.settings scope_provider
参考資料
| ファイル | 次の場合に参照 |
|---|---|
| references/debugging-tokens.md | トークンに権限が付与されず、上記のトラブルシューティングのチェックリストでも原因がわからない場合:スコープエンティティ、ユーザーロール、ロール権限を段階的に確認する方法、テストトークンを発行してトークン/ユーザー/共通ロールを出力する PHP スクリプト、および実際の API リクエストを実行する curl スクリプト |
| references/bearer-csrf-bypass.md | クライアントもセッション Cookie を送信するため、有効な Bearer トークンを含むリクエストで 403 CSRF エラーが発生する場合(React Native、webviews):session_configuration デコレーター、そのサービス定義、およびテスト方法 |
| references/consumer-ttls-and-social-grant.md | コンシューマーのアクセストークン/リフレッシュトークンの TTL を変更する場合(コンテンツエンティティのため、cim ではデプロイされません)、または aud を許可リストと照合する必要があるソーシャル/Google トークン付与を作成する場合 |
関連ドキュメント
- Drupal.org Issue #3451692:「ロール単位の粒度を持つ動的スコープでは、認証済みの権限が継承されない」(6.0.x で修正済み)
よく使うコマンド
# List OAuth clients
ddev drush sqlq "SELECT label, client_id FROM consumer_field_data"
# List OAuth scopes
ddev drush sqlq "SELECT name FROM config WHERE name LIKE 'simple_oauth.oauth2_scope.%' ORDER BY name"
# List each client's default scopes (multi-value consumer base field `scopes`)
ddev drush php:eval 'foreach (\Drupal::entityTypeManager()->getStorage("consumer")->loadMultiple() as $c) { print $c->label() . ": " . implode(", ", array_column($c->get("scopes")->getValue(), "scope_id")) . PHP_EOL; }'
# Check user roles
ddev drush user:role:list [email protected]
# Add role to user
ddev drush user:role:add ROLE_NAME username
# Check role permissions
ddev drush role:perm:list ROLE_NAME
# View OAuth settings
ddev drush config:get simple_oauth.settings
# Test token endpoint
curl -X POST "https://yoursite.ddev.site/oauth/token" \
-d "grant_type=password" \
-d "client_id=CLIENT_ID" \
-d "client_secret=SECRET" \
-d "[email protected]" \
-d "password=pass123" \
-d "scope=SCOPE_NAME"
主要ファイルの一覧
TokenAuthUser.php- AND 権限ロジックを備えたコア認証ラッパーRole.php- 動的スコープからロールへのマッピング(95 行目:認証済みロールの付与)field_permissions.module- フィールドアクセスフック(34 行目)CustomAccess.php- フィールド権限タイプ(36 行目:hasPermission 呼び出し)src/Session/BearerSessionConfiguration.php- Bearer トークン用 CSRF バイパスデコレーター(カスタムモジュール)