mcpField
D
drupal-simple-oauth

drupal-simple-oauth

github.com/grasmashセキュリティ82137 回インストール4 週間前に更新
リポジトリ

drupal-simple-oauth について

Drupal simple_oauth で OAuth2 認証を実装する開発者向けのリファレンススキルです。トークンのスコープ、ユーザーのロールと権限の関係に加え、クライアント設定、モジュールの動作、トラブルシューティングを説明します。

主な機能

  1. 01トークンとユーザー権限の両方を確認
  2. 02スコープとロールの対応規則
  3. 03OAuth クライアントとスコープの設定
  4. 04JSON:API のフィールドアクセス動作
  5. 05トークン認可のトラブルシューティング

活用シーン

  1. 01トークンが API にアクセスできない原因の調査
  2. 02モバイルアプリ用 OAuth クライアントの設定
  3. 03ロールに対応する OAuth スコープの設定
  4. 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.md
consumer-ttls-and-social-grant.md
debugging-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 トークンで権限を付与するには、次の条件を満たす必要があります。

  1. ユーザーがその権限を持つロールを持っていること
  2. トークンリクエストに、そのロールと一致するスコープが含まれていること
  3. その名前の 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 トークンを使用する場合:

  1. JSON:API が entity_field_access フックを呼び出します
  2. field_permissions が $account->hasPermission() をチェックします
  3. $account が TokenAuthUser の場合、トークンとユーザーの両方がその権限を持っている必要があります
  4. どちらか一方でも満たさない場合、フィールドは 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 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 バイパスデコレーター(カスタムモジュール)

コメント