From the: OAuth Docs

Scope is a mechanism in OAuth 2.0 to limit an application's access to a user's account. An application can request one or more scopes, this information is then presented to the user in the consent screen, and the access token issued to the application will be limited to the scopes granted.

In farmOS 1.x we only have the user_access and farm_info scopes available to OAuth Clients. Due to how the oauth2_server module works, it requires additional code to check & validate OAuth scopes before returning protected info. As a first pass we simply implemented the user_access scope which logs in the Authorized user, thus limiting an access_token to the permissions given to the user that authorized a client with that token. The farm_info scope was provided authorize OAuth Clients with only access to info included in the /farm.json endpoint.

The simple_oauth module is different in that it implements OAuth Scopes as Drupal Roles. This will require a different approach for implementing granular access control via OAuth. The notable difference is that when users authorize an OAuth Client (modeled as consumer entities which are configured with Drupal roles), the client is granted the roles configured with that OAuth Client. In other words, when requests are authenticated with the access_token generated from this authorization, the OAuth Client may have different roles (and thus different permissions) than the user that authorized the client. This is an important consideration because the client may be provided with a higher level of access than the authorizing user. (See this issue for more info. It also includes a patch which changes this functionality to not grant more roles than a user has access to: https://www.drupal.org/project/simple_oauth/issues/3077125)

Depending on how we design permissions & roles in farmOS 2.x this isn't necessarily a problem. There may in fact be use cases where a user should be able to authorize a 3rd party client with different permissions than they have. After chatting with @mstenta about this, it seems that we will likely a need a separate permission for authorizing each individual OAuth client eg: "authorize [consumer client_id] via oauth".

Another notable change from oauth2_server is that simple_oauth (by default) grants all Scopes assigned to the OAuth Client. Typically only the scopes that are requested during the authorization flow are granted to the client. This is an important feature for the farmOS-Aggregator which allows users to select which Scopes they would like the client to have (note the patch in the above issue provides this functionality too). On the other hand, other uses cases may require that the OAuth Client indeed has a set level or permissions in order for an integration to work. It seems that we will need to provide some way of configuring clients/consumers to behave one way or another.

Comments

paul121 created an issue. See original summary.

paul121’s picture

Issue summary: View changes
paul121’s picture

Some highlights from chat that helped us arrive at the above summary...

@mstenta

Thinking... what if we had a layer between roles and permissions
For defining "permission sets"
Lo and beyond "there's a module for that" (https://www.drupal.org/project/permission_set)
And what if simple_oauth could use those instead of or in addition to roles
It seems that the linkage between "user" and "consumer" is an important distinction here too
Consumers are essentially a different kind of user
eg: they can access the sites data completely automatically and without any intervention from the actual "user" who gave them access
So really they need their own permissions
But it raises the question: should a user be able to grant access to permissions that they themselves don't have access to?
No. They shouldn't.
So it seems there needs to be a set of permissions specifically for granting access to certain permissions :-)
Or: a set of permissions that allow a user to grant access to certain consumers
Eg: for each consumer, there is a user permission "can grant access to [consumer]"
(are there already permissions like this?)
Or perhaps there needs to be a distinction between "roles that apply to users" and "roles for oauth scope purposes"
Because it really just comes down to having too many granular roles...
But maybe that's not a bad thing

@mstenta

In my mind we have two things to balance: API UX and internal farmOS UX
With API UX, I mean: we want the ability to create granular bundles of permissions with human readable names (aka scopes). So having a lot of scopes to choose from is a good thing IMO.
With internal farmOS UX, I mean: farm managers/admins will need to assign permissions to workers. Do we want to require them to go through the same big list of granular bundles when they are adding a new user? That feels like it will be overwhelming/confusing/not helpful.

@mstenta

The "first party" vs "third party" distinction of OAuth is important here as well
For "first party" integrations (eg: Field Kit), it makes sense for scopes to == roles
For "third party" integrations (eg: Aggregator), I think we want more granular controls...
But there's a catch to "third party", that you highlighted:

Specifically which user ends up creating the content

Is it the user that approved the integration with the Aggregator? Is it a new user that represents the Aggregator itself? Is it some standard "API" user we provide with farmOS by default?
Right now, with Our Sci and OpenTEAM, we're doing the middle one: we create an "Our Sci" user on each farmOS site, and use that to authorize the aggregator
But: I don't like the UX of that right now... it means a) creating a new user, b) logging out, and then in as that user, c) going through the authorization process with the aggregator, d) logging out and then back in as your normal user
It would be better if you didn't need to do any of that.
But, we still want the third party authorization to be tied to a different user, I think
Right? Or maybe not in all cases...

@paul121

Right now, with Our Sci and OpenTEAM, we're doing the middle one: we create an "Our Sci" user on each farmOS site, and use that to authorize the aggregator

yeah I agree, this isn't ideal. This would be a great use case for the "Our Sci" Consumer to use the default User feature - that way all requests are associated with that users account. SO the oursci aggregator module could 1) provide an oursci user (maybe?) and 2) configure their Consumer to operate with that user
and in that case... maybe the farmOS Admin account on each farmOS server could be used to Authorize farmOS servers with the Aggregator? That would really abstract out the need for anyone to even know about the "oursci" user
what would really be ideal is the same client_id and client_secret, that way oursci could authorize with these farmOS servers through a "back door" basically. but... then it's the challenge of keeping that client_secret truly secret across 100s of servers. agh. or.... DNS haha :D

seems like we need different "types" of consumers, or at least a way to make them behave differently...

Third-party integration that provides recommendations in the form of logs, authored by the third-party, eg: an irrigation model that predicts when you should irrigate (third-party, authorized by a farmOS/Drupal user with permission to grant the third-party access, logs authored by a user that represents the third party)

This is similar to the oursci use case. Definitely a valid use case. The farmOS user should only be able to grant the third party permissions either 1) they themselves have access to OR 2) they can grant further access if they have the "Authorize [3rd party] oauth client" permission

User accessing their data through Field Kit (first party)

For this, definitely seems like all actions made through FK should be associated with that user's account
I think there would be valid 3rd party integrations where this would be the case, too? 3rd party acting on behalf of the user? *(errr maybe thats not correct wording, not acting on behalf of the user, just helping the user. eg: The 3rd party app provides special features for the user to create special irrigation logs, but should be shown as created by the user)

m.stenta’s picture

paul121’s picture

Going to try and break this down a bit further:

So it seems there needs to be a set of permissions specifically for granting access to certain permissions :-)
Or: a set of permissions that allow a user to grant access to certain consumers

See separate issue: https://www.drupal.org/project/farm/issues/3172315 Having granular permissions that enable users to authorize individual OAuth Clients (Consumers) with any grant type (I don't think we need separate permissions for each grant type) would be great. Not only would this solve the "set of permissions for granting certain permissions" issue, it would also allow general configuration of which users interact with 1st & 3rd party clients. For example, only users with "authorize farm_client consumers" would be able to use farmOS Field Kit.

We want the ability to create granular bundles of permissions with human readable names (aka scopes). So having a lot of scopes to choose from is a good thing IMO. ...But Farm managers/admins will need to assign permissions to workers. Do we want to require them to go through the same big list of granular bundles when they are adding a new user?

In the farm_access module we are using the User Role's third party settings feature to add additional data for "managed roles". I suggest we do the same for the farm_api module: Roles that have the user.role.*.third_party.farm_api.oauth_role flag set to TRUE designates that this role is primarily used as an OAuth Scope. This allows us to hide these specific roles from the UI that Farm managers/admins use when editing user permissions.

The "first party" vs "third party" distinction of OAuth is important here as well .... seems like we need different "types" of consumers, or at least a way to make them behave differently

Different Scope/Role use cases applicable to both 1st & 3rd party OAuth integrations:
- Scope to only grant the user's permissions (user_access)
- Scope to grant a hard coded set of permissions, potentially more/different than the user's permissions eg: harvest_info
- During Authorization, allow the user to choose which scopes a the client is granted.

I suggest we add additional settings to the Consumer entities that define how & if they handle these additional behaviors:
- consumer.grant_user_access: Always grant the authorizing user's roles when authorizing access for this consumer.
- consumer.limit_to_user_access: Limit granted roles to only include those assigned to the authorizing user.
- consumer.limit_to_requested_scopes: Only grant scopes included in the authorization request. While the consumer entity is configured with all of the possible roles an integration may use, this allows the user to select which scopes/roles they want to grant to a third party.

The patch provided in the simple_oauth issue referenced above limits the granted scopes to only the requested scopes, as well as only the roles granted to the authorizing user. This logic is largely implemented by calling this additional function when building roles for the OAuthToken entity. It would be pretty trivial to check the Consumer entity for the above flags and add logic to add/remove scopes accordingly:

  /**
   * Restricts the list of roles.
   *
   * We do so based on user input, client configuration and actual user roles.
   *
   * @param array $input_scope_ids
   *   A list of user requested scope IDs.
   * @param \Drupal\Core\Session\AccountInterface $user
   *   The user that was actually authenticated.
   * @param \Drupal\consumers\Entity\Consumer $consumer
   *   The consumer entity associated to the client for this request.
   *
   * @return array
   *   The list of scope IDs to be granted to the token / TokenUser.
   */
  public static function calculateAccessibleScopeIds(
    array $input_scope_ids,
    AccountInterface $user,
    Consumer $consumer
  ) {
    $user_roles = $user->getRoles();
    $client_roles = array_map(function ($item) {
      return $item['target_id'];
    }, $consumer->get('roles')->getValue());
    $intersection = array_intersect($user_roles, $input_scope_ids, $client_roles);
    return array_merge(
      $intersection,
      [$user->isAuthenticated() ? RoleInterface::AUTHENTICATED_ID : RoleInterface::ANONYMOUS_ID]
    );
  }
It seems that the linkage between "user" and "consumer" is an important distinction here too
Consumers are essentially a different kind of user
eg: they can access the sites data completely automatically and without any intervention from the actual "user" who gave them access.

and

But there's a catch to "third party", that you highlighted: Specifically which user ends up creating the content

Is it the user that approved the integration with the Aggregator? Is it a new user that represents the Aggregator itself? Is it some standard "API" user we provide with farmOS by default?

Again, I think this is behavior that should be "configurable" by the Consumer entity. By default the authenticated user is associated with the token, but consumers can have a "default user" that is used in case there was no authenticated user during authorization (normally this would only happen with the ClientCredentials grant, where no intervention from a User is required to grant access because the client id/client secret are truly secret values). So we need a way to configure a consumer to make a Token always reference the Consumer's default user. We could modify the AccessTokenRepository::getnewToken() method to check a consumer.force_default_user flag and set the $token->userIdentifier accordingly.

Some background on how a token is authenticated with simple_oauth:

- When a token is created, the authenticated user is saved with the token.
- BUT Consumers can be configured with a "default" user: "When no specific user is authenticated Drupal will use this user as the author of all the actions made."
- The SimpleOAuthAuthenticationProvider::authenticate() method checks for a valid token in the request and returns an AccountInterface if all is successful: return new TokenAuthUser($token);. This works because TokenAuthUser saves a reference to a user object; either 1) the Authenticated user from the token or 2) The Consumer's default user. TokenAuthUser implements the UserInterface (which implements AccountInterface), and delegates most of the interface methods to it's referenced user object.
- Once authenticated, a Token appears just like a User. When checking permissions, the hasPermission() method loads the token's scopes/roles and calls isPermissionInRoles($permission, $roles).

paul121’s picture

I've implemented the following consumer "config" options (that I outlined above) as additional fields on the consumer entity:

  • consumer.grant_user_access: Always grant the authorizing user's access
  • consumer.limit_user_access: Never grant the consumer more access than the authorizing user has
  • consumer.limit_requested_access Only grant the "scopes" that are requested - this allows the users to select which scopes to grant the consumer.

Combinations of the 3 above configurations should cover most of the 1st and 3rd party API integration use cases for farmOS. One option I haven't yet implemented is the consumer.force_default_user. This would force the Consumer to always use the optional "default user" that can be configured with a Consumer. This is a bit trickier to implement because it requires modifying how the simple_oauth module uses the TokenAuthUser to effectively authenticate tokens as a Drupal user.

There also is a slight security consideration here where anyone could authorize a "public" client at anytime via the Client Credentials grant without having a Drupal user account (more details here: https://www.drupal.org/project/simple_oauth/issues/3173947#comment-13846016)

This isn't a problem if there isn't a "default user" configured with the consumer, but it would be good to put some precautions in place. The oauth2_server module had the ability to configure which grants were available on a per-client level. This way a client could be configured to NEVER accept the Client Credentials grant, and only the Authorization Code or Password Credentials. This will be an easy option to implement since we can alter logic to check the supplied grant type in the validateClient() method of the ClientRepository service (that we're already decorating!)

Similarly... I'm curious if farmOS (via the farm_api module) should disable the Password Credentials grant for all Consumers that are configured as "third party". This would help enforce best-practices of the OAuth standard, specifically that Users should not be entering their credentials in 3rd party applications. It would be a *little* limiting, but this would leave the Authorization Code and Client Credentials grant available for third parties to use (and Client Credentials ONLY if the the 3rd party can maintain a true "client secret").

Lastly, we will want to put some thought into the user-facing UI of these features: (Perhaps we wait on this until we partner with an OpenTEAM parter to build a solid 3rd party integration?)

  1. We likely need to alter to the Authorization Form (used with the Authorization Code grant) to better-reflect what these Consumer "config" options mean. For example, if grant_user_acess is enabled, a message stating "This 1st/3rd client will be granted all the access your user account has." should be clearly displayed.
  2. A UI for farmOS Managers to review Consumer configuration: "farmOS Data Sharing" (Clearly distinguish 1st vs 3rd party clients)
  3. A UI for farmOS Manager to modify (perhaps just turn on/off? change available scopes?) consumers
  4. A UI to review & revoke API access (see which Consumers have recently connected, allow user to revoke tokens for clients)
m.stenta’s picture

Issue tags: +stable blocker
m.stenta’s picture

Issue tags: -stable blocker
m.stenta’s picture

Linking to this old v1 GitHub issue for more thoughts/context: https://github.com/farmOS/farmOS/issues/228

m.stenta’s picture

Status: Active » Closed (outdated)

I'm going to close this as "outdated". We are currently working on upgrading Simple OAuth from v5 to v6 in the 3.x branch of farmOS, which may change some of the considerations described above.