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. 04验证 JSON: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 中使用 simple_oauth 模块实现 OAuth2 身份验证的全面模式。适用于 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];

此问题已在 issue #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() 中修改 TTL。
  • **社交登录/Google 令牌授权必须将 aud 与允许列表进行比对。**移动应用使用各自的客户端 ID 生成令牌,因此只检查一个预期的 aud 会导致所有应用登录都失败。

详情和代码: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 脚本生成测试令牌并打印令牌、用户和交集角色;使用 curl 脚本发送实际 API 请求
references/bearer-csrf-bypass.md请求携带有效的 Bearer 令牌,却因客户端同时发送会话 Cookie 而出现 403 CSRF 错误(React Native、WebView):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 绕过装饰器(自定义模块)

评论

支持 Markdown