drupal-simple-oauth
OAuth2 authentication patterns for Drupal using simple_oauth module. Covers TokenAuthUser permission logic, scope/role matching, mobile app token flows, field_permissions integration, CSRF bypass, and debugging token issues.
What this skill does
# Drupal Simple OAuth Patterns
Comprehensive patterns for working with the simple_oauth module for OAuth2 authentication in Drupal. Use when working with API authentication, mobile app tokens, or OAuth2 implementation.
## Version Information
- simple_oauth: 6.0.9
- Scope provider: dynamic (role-based granularity)
- Current Drupal: 10.x/11.x compatible
## Critical OAuth Token Concepts
### TokenAuthUser: The Core Authentication Wrapper
When a request is authenticated with an OAuth token, Drupal wraps the user in a `TokenAuthUser` decorator that enforces BOTH token AND user permissions.
**Location:** `/docroot/modules/contrib/simple_oauth/src/Authentication/TokenAuthUser.php`
#### Permission Check Logic (Line 95)
```php
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);
}
```
**Critical Rule:** BOTH the token AND the user must have the permission (AND condition).
#### Role Intersection Logic (Line 109)
```php
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);
}
```
**Critical Rule:** Only roles that exist in BOTH the token AND the user are granted (`array_intersect`).
### The Scope/Role Matching Requirement
For an OAuth token to grant a permission:
1. The user MUST have a role with that permission
2. The token request MUST include a scope matching that role
3. An OAuth2 scope entity MUST exist with that name
**If any condition fails, permission is DENIED.**
## Common Pitfalls
### Pitfall 1: Scope/Role Mismatch
**Problem:**
```php
// User has: administrator role
// Token requested with: scope=api_consumer
// Result: Only 'authenticated' role granted (intersection)
// Permissions from administrator: DENIED
```
**Why it fails:**
```php
$token_roles = ['authenticated', 'api_consumer']; // From scope
$user_roles = ['authenticated', 'administrator']; // From user
$granted = array_intersect($token_roles, $user_roles); // ['authenticated']
```
**Solution:** Request token with correct scope:
```javascript
formData.append('scope', 'administrator');
```
### Pitfall 2: Non-existent Scope Entity
**Problem:**
```javascript
// Mobile app requests: scope=subscriber
// But no "subscriber" OAuth2 scope entity exists
// Result: Token has NO scopes, NO roles, NO permissions
```
**Solution:** Create the OAuth2 scope entity or use existing scope name.
**Check existing scopes:**
```bash
ddev drush config:get simple_oauth.settings
# Or query scope entities
ddev drush sqlq "SELECT id FROM consumer_scopes"
```
### Pitfall 3: Authenticated Role Permissions
**Problem:** Assuming authenticated role permissions are always granted.
**Reality:** Only if the token includes the authenticated role in its scope intersection.
**From Role.php (line 94):**
```php
// Scopes automatically grant authenticated role
return $exclude_locked_roles ? [$role] : [AccountInterface::AUTHENTICATED_ROLE, $role];
```
This was fixed in issue #3451692 (included in 6.0.x).
## Debugging OAuth Permission Issues
### Step 1: Verify Scope Entity Exists
```bash
# List all OAuth2 scopes
ddev drush sqlq "SELECT id, description FROM consumer_scopes"
# Example scopes you might have:
# - authenticated
# - api_consumer
# - premium_user
# - administrator
```
### Step 2: Check User Roles
```bash
ddev drush user:role:list [email protected]
```
### Step 3: Verify Role Permissions
```bash
# Check if role has the permission
ddev drush role:perm:list api_consumer | grep "view field_premium_content"
```
### Step 4: Test Token Creation
```php
// Create test script: test_oauth_token.php
use Drupal\simple_oauth\Entity\Oauth2Token;
$username = 'test_user';
$scope = 'premium_user'; // Match user's role!
// Get user
$user = user_load_by_name($username);
$consumer = \Drupal::entityTypeManager()
->getStorage('consumer')
->loadByProperties(['label' => 'Mobile App']);
$consumer = reset($consumer);
// Create token
$token = Oauth2Token::create([
'auth_user_id' => $user->id(),
'client' => $consumer->id(),
'bundle' => 'access_token',
'scopes' => $scope,
'value' => bin2hex(random_bytes(32)),
'expire' => time() + 3600,
'status' => TRUE,
]);
$token->save();
// Wrap user with token context
$token_user = new \Drupal\simple_oauth\Authentication\TokenAuthUser($token);
// Test permissions
$permission = 'view field_premium_content';
$token_has = $token->hasPermission($permission);
$user_has = $user->hasPermission($permission);
$token_user_has = $token_user->hasPermission($permission);
print "Token roles: " . implode(', ', $token->getRoles()) . "\n";
print "User roles: " . implode(', ', $user->getRoles()) . "\n";
print "Intersected roles: " . implode(', ', $token_user->getRoles()) . "\n";
print "Token has permission: " . ($token_has ? 'YES' : 'NO') . "\n";
print "User has permission: " . ($user_has ? 'YES' : 'NO') . "\n";
print "TokenAuthUser has permission: " . ($token_user_has ? 'YES' : 'NO') . "\n";
```
Run with: `ddev drush php:script test_oauth_token.php`
### Step 5: Test API Request
```bash
#!/bin/bash
# Get OAuth token
TOKEN_RESPONSE=$(curl -s -X POST "https://yoursite.ddev.site/oauth/token" \
-d "grant_type=password" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET" \
-d "[email protected]" \
-d "password=password123" \
-d "scope=premium_user")
ACCESS_TOKEN=$(echo "$TOKEN_RESPONSE" | python3 -c "import sys, json; print(json.load(sys.stdin).get('access_token', ''))")
# Test API request
curl -s -X GET "https://yoursite.ddev.site/jsonapi/node/article/2" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/vnd.api+json" | python3 -m json.tool
```
## Integration with Other Modules
### field_permissions Module
**How it works:**
```php
// 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);
}
```
**Result:** field_permissions works correctly with simple_oauth when scopes match roles.
### JSON:API Module
JSON:API respects all field-level access checks, including field_permissions. When using OAuth tokens:
1. JSON:API calls `entity_field_access` hooks
2. field_permissions checks `$account->hasPermission()`
3. If `$account` is TokenAuthUser, both token AND user must have permission
4. If either fails, field is excluded from JSON:API response
**No special configuration needed** - it works automatically when scopes are correct.
## OAuth Client Configuration
### Mobile App Client Configuration
When configuring a mobile or decoupled app as an OAuth client, the token request follows this pattern:
```javascript
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);
```
Store `client_id` and `client_secret` securely in your apRelated in General
modeling-omnistudio-epc-catalog
IncludedSalesforce Industries CME EPC product-modeling skill for Product2-based catalog creation. Use when creating EPC products, configuring product attributes, building offer bundles with Product Child Items, or reviewing EPC DataPack JSON metadata for product catalog changes. TRIGGER when: user creates or updates Product2 EPC records, AttributeAssignment payloads, AttributeMetadata/AttributeDefaultValues, Offer bundles, or ProductChildItem relationships. DO NOT TRIGGER when: designing OmniScripts/FlexCards/Integration Procedures (use building-omnistudio-omniscript, building-omnistudio-flexcard, or building-omnistudio-integration-procedure), implementing Apex business logic (use generating-apex), or troubleshooting deployment pipelines (use deploying-metadata).
relationship-science-coach
IncludedUse this skill for direct, practical adult relationship coaching: couples conflict, repair, trust, marriage, dating, flirting, attachment patterns, emotional connection, sex, desire differences, eroticism, kink negotiation, affection, love languages, breakups, and long-term passion. Draw on Gottman, EFT and Hold Me Tight, attachment science, modern sex research, Perel, Nagoski, Kerner, Schnarch, Love and Stosny, and flexible love-language tools. Be concrete and low-hedge. Redirect only for imminent danger, abuse, coercive control, minors, non-consent, self-harm, stalking, or medical/legal/psychiatric decisions.
building-sf-integrations
IncludedSalesforce integration architecture and runtime plumbing with 120-point scoring. Use this skill to set up Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, and Change Data Capture. TRIGGER when: user sets up Named Credentials, External Services, REST/SOAP callouts, Platform Events, CDC, or touches .namedCredential-meta.xml files. DO NOT TRIGGER when: Connected App/OAuth config (use configuring-connected-apps), Apex-only logic (use generating-apex), or data import/export (use handling-sf-data).
venue-templates
IncludedAccess comprehensive LaTeX templates, formatting requirements, and submission guidelines for major scientific publication venues (Nature, Science, PLOS, IEEE, ACM), academic conferences (NeurIPS, ICML, CVPR, CHI), research posters, and grant proposals (NSF, NIH, DOE, DARPA). This skill should be used when preparing manuscripts for journal submission, conference papers, research posters, or grant proposals and need venue-specific formatting requirements and templates.
let-fate-decide
IncludedDraws the 12 Houses of the Zodiac Tarot spread to inject entropy into planning when prompts are vague, ambiguous, or casually delegated. Interprets the spread to guide next steps. Use when the user says 'let fate decide', 'YOLO', 'whatever', 'idk', or other nonchalant phrases, makes Yu-Gi-Oh references, or when you are about to arbitrarily pick between multiple reasonable approaches. Prefer over ask-questions-if-underspecified when the user's tone is casual or playful rather than precision-seeking.
net-ops
IncludedCross-platform network troubleshooting (Windows, macOS, Linux) via local or remote shell. Use for: DNS broken, can't resolve hostnames, nslookup/dig works but apps fail, NRPT, WFP, scutil, /etc/resolver, systemd-resolved, /etc/resolv.conf, NetworkManager, VPN DNS leak residue (ProtonVPN/Mullvad/WireGuard/AnyConnect), AV/firewall blocking DNS or DoH, Tailscale DNS interaction, intermittent connectivity, remote diagnostics over SSH.