GraphQL OAuth Authentication Fails Due to Incomplete User Entity in Access Checks

Created on 17 March 2025, about 1 month ago

Problem/Motivation

In a website with the modules graphql, simple_oauth and graphql_oauth enabled, and an OAuth client with grant type Authorization Code, when sending authenticated GraphQL requests, the $account object does not contain full user entity data. This results in permission checks failing, as the system is unable to verify user roles and permissions correctly. The issue occurs because \Drupal::currentUser() returns an AccountProxy instead of a fully loaded `User` entity.

Proposed resolution

Ensure that the user entity is fully loaded before performing access checks in QueryAccessCheck. This can be achieved by explicitly loading the user entity using \Drupal\user\Entity\User::load($account->id()) when required.

Remaining tasks

N/A

User interface changes

N/A

API changes

N/A

Data model changes

N/A

🐛 Bug report
Status

Active

Version

4.0

Component

Code

Created by

🇧🇷Brazil tregismoreira

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Merge Requests

Comments & Activities

  • Issue created by @tregismoreira
  • Pipeline finished with Success
    about 1 month ago
    Total: 354s
    #450649
  • 🇳🇱Netherlands kingdutch

    Access should be checked on the account proxy of the current user. Loading the user and checking directly on the user would create a security issue of escalated permissions since it ignores the token's restrictions.

    Although the Access Policy API now somewhat changes this and reloading the user wouldn't actually change the permissions (since User::hasPermission delegates to the Access Policy API as of 10.3 ), there are still implementations (such as Simple OAuth) which decorate the user object and change permission management that way (which was one of the few options available pre-10.3).

    The hasPermission function of TokenAuthUser (which itself contains a fully loaded user object) looks as follows

      public function hasPermission($permission) {
        // When the 'auth_user_id' isn't available on the token (which can happen
        // with the 'client credentials' grant type):
        // has permission checks are then only performed on the scopes.
        if ($this->token->get('auth_user_id')->isEmpty()) {
          return $this->token->hasPermission($permission);
        }
        // User #1 has all permissions.
        if ((int) $this->id() === 1) {
          return TRUE;
        }
    
        return $this->token->hasPermission($permission) && $this->subject->hasPermission($permission);
      }
    

    In case of a system token (client credentials) only the permissions actually on the token will be used. In case of a user token (authorization code) then the token will only be allowed to do what both the user and token have access to. This ensures that 1) The external application using a token can not do things it wasn't allowed to do (e.g. delete a user) even if the user can take those actions 2) The user can not take actions they otherwise wouldn't be able to take (e.g. an application can offer user deletion to sufficiently permissioned users, but the user must still be allowed to do this through the normal UI).

    The issue that you're running into is likely that the scopes being provided do not grant sufficient permissions. There's currently no way in simple_oauth to grant a base-set of permissions and the choice to make scopes point to a single permission was a conscious design decision implemented in #3283821: Support individual permission and role reference in the new scope data model . The original job of scopes was described as follows (from an internal Open Social document before our contribution to Simple OAuth).

    OAuth scopes exist not to indicate what a user may do, for this we have an extensive permission system built within Open Social. Instead OAuth scopes define what an application may do, either standalone or on behalf of a user. It's possible that an application may have permission from a user to take an action on their behalf but still encounter an access denied error because the user themselves does not have the permission to perform that action.
    It's therefor important to realise that when talking about OAuth scopes the scopes indicate what type of trust the user (either acting on their behalf, or the site manager giving access to the platform) places in the application.

    My suggestion would be to map out your scenario and the permissions you need. It may either be solvable by changing your scope hierarchy, implementing a set of base permissions in Simple OAuth (anyone with a token regardless of scopes gets this), or by allowing multiple permissions to be assigned to a scope. (Those options are presented in order of author preference).

Production build 0.71.5 2024