Problem/Motivation

When creating a new consumer with the client credentials grant, the form requires you to also input a User reference with description:

When no specific user is authenticated Drupal will use this user as the author of all the actions made.

This wording made me think the user account is only relevant when the user creates new content and that's the author that would be assigned. But the user is also used in more ways than this. The TokenAuthUser user object essentially wraps the specified user. When ->getRoles() is invoked, it returns the intersection of the user's roles and the scope's roles. This makes sense now that I think about it, because the purpose of the scope is essentially to define what the application/consumer has access to as a subset of what the actual user account has access to.*

*Note that this scoping system doesn't work that way for permissions, confusingly. E.g., the Permission scope seems additive to the permissions of the referenced User account. This is also confusing. Why does the permission scope do this but the Role scope doesn't?

I think this relationship should be made clearer on the configuration form. I initially assumed that I could create a client credentials consumer that acted as its own distinct user and the role and permission scopes would assign that user access to things on the site, but that's not how this works.

Steps to reproduce

Proposed resolution

Update the form description for the User field on client credentials to something like this:

When no specific user is authenticated Drupal will masquerade as this user.

Also figure out a way to clarify the additive nature of the Permission scope vs the intersection nature of the Role scope.

Remaining tasks

User interface changes

API changes

Data model changes

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

bkosborne created an issue. See original summary.

bkosborne’s picture

Issue summary: View changes
vidit.anjaria’s picture

Issue tags: +ai::outside, +2026Sprint14
vidit.anjaria’s picture

Issue tags: +AI Initiative Sprint
vidit.anjaria’s picture

Issue tags: +2026Sprint15

alex ua made their first commit to this issue’s fork.

alex ua’s picture

Status: Active » Needs review
vidit.anjaria’s picture

Issue tags: -2026Sprint15 +2026Sprint16
cobadger’s picture

Status: Needs review » Needs work
nikro’s picture

Assigned: Unassigned » nikro

Picking this up 🫡

vidit.anjaria’s picture

Issue tags: -2026Sprint16 +2026Sprint17
nikro’s picture

Assigned: nikro » Unassigned
Status: Needs work » Needs review

Thanks @cobadger for the detailed review, and @alex ua for the original patch. I have pushed changes on top of the existing branch addressing all four threads.

  • Super user in the test base. Fixed in AuthorizedRequestBase rather than only in ClientCredentialsTest, as suggested. The first account created in an empty user table becomes uid 1, and SuperUserAccessPolicy then short-circuits every access check. Worth noting that RefreshTokenTest and OpenIdConnectTest pass unchanged once they get an ordinary account, so they were not relying on the bypass either. The base fix therefore hardens all three with no behaviour change, and makes the per-subclass override unnecessary.
  • Deterministic uid 1 for SuperUserTest. It now loads uid 1 explicitly instead of depending on creation order, which also removes the ambiguity around the kernel reboot in testSuperUser(). The existing assertEquals('1', ...) stays as an intentional guard.
  • Assertion placement. Moved out of testSaveConsumerWithNewSecret() into its own testClientCredentialsFieldDescriptions(), with a fixture holding only what the descriptions need.
  • The second half of the issue. The role guidance went on the scopes field rather than growing the user_id description further, as suggested.

On that last point, I confirmed the asymmetry locally before wording it. With a Role granularity scope pointing at a role the selected user does not hold, the same token gives:

route requiring _permission: 'access administration pages'  ->  200
route requiring _role: 'repro_editor'                       ->  403
getRoles() reports only ["authenticated"], hasRole() is FALSE

So permission checks pass while role membership checks quietly fail, with nothing to explain why. Role::getPermissions() contributes the role's permissions unconditionally, while TokenAuthUser::getRoles() intersects with the user's real roles.

To be explicit about scope: this MR now resolves both halves of the report, the User field wording and the permission-versus-role asymmetry. Full test suite passes locally (90/90) and PHPCS is clean; the PHPStan finding in Oauth2ClientCredentialsTokenCreateForm.php is pre-existing on 6.1.x and untouched here.

Cross-referencing #3589432: Access denied in JSONAPI for OAuth users, which documents the same asymmetry on the scope form. Different audience, same facts, so the wording in both should stay consistent.

nikro’s picture

Assigned: Unassigned » nikro

Picking this up - will actually check mid-week.

vidit.anjaria’s picture

Issue tags: -2026Sprint17 +2026Sprint18
pfrilling’s picture

Assigned: nikro » pfrilling

Working on this today.

pfrilling’s picture

Assigned: pfrilling » Unassigned
Status: Needs review » Reviewed & tested by the community

The changes all look good to me.

I made a quick wording change to the Scopes field description to address Alex's point 4 — the previous text conflated the unconditional permission grant with the separate role-membership check. The pipeline is green after a rebase.

Cross linking the related #3589432: Access denied in JSONAPI for OAuth users, which adds scope-form descriptions, a README section, and additional tests for the behavior documented here.

Drafted with help of an LLM

  • bojan_dev committed ab424420 on 6.1.x authored by alex ua
    fix: #3590930 Clarify the purpose of the User assignment in the Client...
bojan_dev’s picture

Status: Reviewed & tested by the community » Fixed

Nice work all!

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.