External Provider Scopes

When connecting to a user provider, the script should provide a getScopes() callback.

Background:
A user logs in → the ProviderClass fetches the user from the external service, sets the profile, and commits the login action. Different users in the store can have different scopes. Currently, all scopes requested by the client are accepted as long as they are allowed by the client; there is no filtering based on the user repository yet. The ProviderClass should be able to add additional scopes.

What happens if a user requests scopes A, B, and D, but the client is only allowed to request A, B, and C, and the user storage returns scopes B and C for that user?

Proposal:

The final granted scopes should be the intersection of:

  1. Scopes the client is allowed to request

  2. Scopes the user requested

  3. Scopes your user storage allows for that user

Step-by-step for your example:

  • User requested: A, B, D

  • Client allowed: A, B, C

  • User storage: B, C

Now compute intersections:

  1. Request ∩ ClientAllowed
    → A, B

  2. (Request ∩ ClientAllowed) ∩ UserStorage
    → B

Final granted scopes: B

Because:

  • D is not allowed for the client → rejected

  • A is not allowed by user storage → rejected

  • C was never requested → not included

  • B is requested, allowed, and granted → included

Should we allow setting a scope if the user did not request it?
For example: a user store may need to handle different permissions for applications during login. If we only set scopes that the application explicitly requests, then the application would have to request all possible scopes in order to receive the ones defined for the user — because we cannot assign a scope that was not requested.
If the user does not request any scopes, we could instead assign the ones returned by the user store.

Follow-up question: Should this behavior be the default (no requested scopes = set it, requested scopes = filtering) , or should it be explicitly set by the UserProvider using something like overwriteScopes(:scopes)?

ref:

But perhaps this is not the right tool for managing permissions, since scopes are meant to define what an application can access on a profile.

In other words:

  • Scopes → “What can this app access on this user’s account?”

  • User permissions / roles → “What is this user allowed to do within the system?”

So while mapping user permissions to scopes for convenience, relying solely on scopes to manage internal permissions can be risky and confusing.

What we have here is what’s described in Auth0 Request custom API access:

In this Example read:appointments is set by the requesting client.

Here is my proposal:

The Client asks for:

  • openid
  • email
  • profile
  • user:delete

allowed scopes for the request in the uitsmijter client-config are:

  • openid
  • email
  • adress

What Uitsmijter sees is:

  • openid
  • email

Because adress was not requesed and user:delete was not allowed.

The external user provider sends additional scopes:

  • user:list
  • user:add
  • admin:all

allowed scopes for the provoder in the uitsmijter client-config are:

  • user:*
  • can:

What the resulting scopes are:

  • openid (client reques)
  • email (client reques)
  • user:list (provider)
  • user:add (provider)

omitted is admin:all, because it is not in the provoders allowd list in the client configuration.

We have to split the scopes array into two, because otherwise the user can request scopes that are not allowed and only set by the provider. But there is a security issue here: what if is for example user:**allowd in both lists: The client asks for user:delete and the procider does not set this, but it is allowed by the client config. In this case, user:delete is also in the final list.

We could implement, that when a provider is setting scopes it has to set all (overwrite), but in this case we have to pass the scopes into the provider script as a parameter in the credentials object that currenty only contains username and password.

We could make it more secure by naming the “allowedProviderScopes” just “scopes”, like it is now. and name the clients allow list: “autoAcceptScopes”.

This will be a hugh breaking change for the client configuration.
this is open for comments.

The implementation of external provider scopes in Uitsmijter addresses the fundamental challenge of balancing flexibility with security when JavaScript authentication providers need to dynamically assign permissions based on user context. Rather than implementing as proposed, the final design separates concerns through a two-tier filtering architecture that maintains backward compatibility while enabling powerful new authorization patterns.

At its core, the system recognizes that scopes serve two distinct purposes: controlling what OAuth clients can request, and controlling what authentication providers can dynamically grant based on user attributes like roles or group memberships. The scopes field continues to govern client-requested permissions, when an application requests openid, email, and admin:delete, only those scopes explicitly allowed in the client configuration will pass through. This prevents malicious or misconfigured clients from requesting excessive permissions.

The new allowedProviderScopes field addresses the second concern raised in my original discussion: enabling providers to enrich user tokens with context-specific permissions without compromising security. Consider a scenario where your JavaScript provider queries LDAP and discovers a user belongs to the “finance” group. The provider can return scopes like invoice:read, invoice:write, and payment:approve, but these will only appear in the final JWT if they match patterns defined in allowedProviderScopes, such as invoice:* or payment:*. This prevents a compromised provider script from granting arbitrary administrative access.

The wildcard pattern matching system provides the flexibility initially sought while maintaining security boundaries. Rather than enumerating every possible permission variation, clients can configure user:* to match user:read, user:write, and user:list, while still explicitly blocking sensitive operations by omitting patterns like admin:*. This addresses the concern about provider override capability mentioned in the discussion, but does so with explicit allowlisting rather than implicit trust.

Importantly, the implementation follows a secure-by-default philosophy. If allowedProviderScopes is absent or empty, no provider-supplied scopes are added to tokens, ensuring that existing deployments continue functioning identically after upgrade. The final scope list merges both filtered sets rather than intersecting them, which better supports the separation of client permissions and user-specific entitlements. For instance, a web application might always need openid and email scopes for basic functionality, while a manager user additionally receives team:manage and reports:view based on their role. Merging these creates a complete permission set rather than reducing it to an empty intersection.

This design resolves the breaking change concerns raised in my original discussion by treating the two scope sources as complementary rather than competitive, allowing OAuth clients to define their baseline permission needs while providers dynamically enhance tokens based on authenticated user context.