Customizing OAuth 2.0 Scope Handling
RFC 6749, The OAuth 2.0 Authorization Framework, describes access token scopes as a set of case-sensitive strings defined by the authorization server. Clients can request scopes, and resource owners can authorize them.
The default scopes implementation in AM treats scopes as per RFC 7662, while the legacy /oauth2/tokeninfo
endpoint populates the scopes with profile attribute values. For example, if one of the scopes is mail
, AM sets mail
to the resource owner's email address in the token information returned.
You can change the scope implementation behavior by writing your own scope validator plugin. This section shows how to write a custom OAuth 2.0 scope validator plugin for use in an OAuth 2.0 provider (authorization server) configuration.
Tip
The default scope validator calls the script that lets AM modify the key pairs contained inside an access token before issuing it. If you intend to use this functionality, you must incorporate this call into your custom scope validator implementation.
About the Scope Validator Plugin Sample
A scope validator plugin implements the org.forgerock.oauth2.core.ScopeValidator
interface. As described in the API specification, the ScopeValidator
interface has several methods that your plugin overrides.
This plugin, taken from the openam-scope-sample example, sets whether read
and write
permissions were granted:
/* * Copyright 2014-2022 ForgeRock AS. All Rights Reserved * * Use of this code requires a commercial software license with ForgeRock AS. * or with one of its affiliates. All use shall be exclusively subject * to such license between the licensee and ForgeRock AS. */ package org.forgerock.openam.examples; import org.forgerock.oauth2.core.AccessToken; import org.forgerock.oauth2.core.ClientRegistration; import org.forgerock.oauth2.core.OAuth2Request; import org.forgerock.oauth2.core.ScopeValidator; import org.forgerock.oauth2.core.Token; import org.forgerock.oauth2.core.UserInfoClaims; import org.forgerock.oauth2.core.exceptions.InvalidClientException; import org.forgerock.oauth2.core.exceptions.ServerException; import org.forgerock.oauth2.core.exceptions.UnauthorizedClientException; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; /** * Custom scope validators implement the * {@link org.forgerock.oauth2.core.ScopeValidator} interface. * * <p> * This example sets read and write permissions according to the scopes set. * </p> * * <ul> * * <li> * The {@code validateAuthorizationScope} method * adds default scopes, or any allowed scopes provided. * </li> * * <li> * The {@code validateAccessTokenScope} method * adds default scopes, or any allowed scopes provided. * </li> * * <li> * The {@code validateRefreshTokenScope} method * adds the scopes from the access token, * or any requested scopes provided that are also in the access token scopes. * </li> * * <li> * The {@code getUserInfo} method * populates scope values and sets the resource owner ID to return. * </li> * * <li> * The {@code evaluateScope} method * populates scope values to return. * </li> * * <li> * The {@code additionalDataToReturnFromAuthorizeEndpoint} method * returns no additional data (an empty Map). * </li> * * <li> * The {@code additionalDataToReturnFromTokenEndpoint} method * adds no additional data. * </li> * * </ul> */ public class CustomScopeValidator implements ScopeValidator { @Override public Set<String> validateAuthorizationScope( ClientRegistration clientRegistration, Set<String> scope, OAuth2Request oAuth2Request) { if (scope == null || scope.isEmpty()) { return clientRegistration.getDefaultScopes(); } Set<String> scopes = new HashSet<String>( clientRegistration.getAllowedScopes()); scopes.retainAll(scope); return scopes; } @Override public Set<String> validateAccessTokenScope( ClientRegistration clientRegistration, Set<String> scope, OAuth2Request request) { if (scope == null || scope.isEmpty()) { return clientRegistration.getDefaultScopes(); } Set<String> scopes = new HashSet<String>( clientRegistration.getAllowedScopes()); scopes.retainAll(scope); return scopes; } @Override public Set<String> validateRefreshTokenScope( ClientRegistration clientRegistration, Set<String> requestedScope, Set<String> tokenScope, OAuth2Request request) { if (requestedScope == null || requestedScope.isEmpty()) { return tokenScope; } Set<String> scopes = new HashSet<String>(tokenScope); scopes.retainAll(requestedScope); return scopes; } /** * Set read and write permissions according to scope. * * @param token The access token presented for validation. * @return The map of read and write permissions, * with permissions set to {@code true} or {@code false}, * as appropriate. */ private Map<String,Object> mapScopes(AccessToken token) { Set<String> scopes = token.getScope(); Map<String, Object> map = new HashMap<String, Object>(); final String[] permissions = {"read", "write"}; for (String scope : permissions) { if (scopes.contains(scope)) { map.put(scope, true); } else { map.put(scope, false); } } return map; } @Override public UserInfoClaims getUserInfo( AccessToken token, OAuth2Request request) throws UnauthorizedClientException { Map<String, Object> response = mapScopes(token); response.put("sub", token.getResourceOwnerId()); UserInfoClaims userInfoClaims = new UserInfoClaims(response, null); return userInfoClaims; } @Override public Map<String, Object> evaluateScope(AccessToken token) { return mapScopes(token); } @Override public Map<String, String> additionalDataToReturnFromAuthorizeEndpoint( Map<String, Token> tokens, OAuth2Request request) { return new HashMap<String, String>(); // No special handling } @Override public void additionalDataToReturnFromTokenEndpoint( AccessToken token, OAuth2Request request) throws ServerException, InvalidClientException { // No special handling } }
For information on downloading and building AM sample source code, see How do I access and build the sample code provided for AM (All versions)? in the Knowledge Base.
Get a local clone so that you can try the sample on your system.
pom.xml
Apache Maven project file for the module
This file specifies how to build the sample scope validator plugin, and also specifies its dependencies on AM components.
src/main/java/org/forgerock/openam/examples/CustomScopeValidator.java
Core class for the sample OAuth 2.0 scope validator plugin
See "About the Scope Validator Plugin Sample" for a listing.
After you successfully build the project, you find the openam-scope-sample-7.1.jar
in the /path/to/openam-samples-external/openam-scope-sample/target
directory of the project.
Configuring an Instance to Use the Plugin
After building your plugin .jar file, copy the .jar file under WEB-INF/lib/
where you deployed AM.
Restart AM or the container in which it runs.
In the AM console, you can either configure a specific OAuth 2.0 provider to use your plugin, or configure your plugin as the default for new OAuth 2.0 providers. In either case, you need the class name of your plugin. The class name for the sample plugin is org.forgerock.openam.examples.CustomScopeValidator
.
To configure a specific OAuth 2.0 provider to use your plugin, navigate to Realms > Realm Name > Services, click OAuth2 Provider, and enter the class name of your scopes plugin to the Scope Implementation Class field.
To configure your plugin as the default for new OAuth 2.0 providers, add the class name of your scopes plugin. Navigate to Configure > Global Services, click OAuth2 Provider, and set Scope Implementation Class.
Trying the Sample Plugin
In order to try the sample plugin, make sure you have configured an OAuth 2.0 provider to use the sample plugin. Also, set up an OAuth 2.0 client of the provider that takes scopes read
and write
.
Next try the provider as shown in the following example:
$curl \ --request POST \ --data "grant_type=client_credentials \ &client_id=myClientID&client_secret=password&scope=read" \ "https://openam.example.com:8443/openam/oauth2/realms/root/realms/alpha/access_token"
{ "scope": "read", "expires_in": 59, "token_type": "Bearer", "access_token": "c8860442-daba-4af0-a1d9-b607c03e5a0b" }
$curl https://openam.example.com:8443/openam/oauth2/realms/root/realms/alpha/tokeninfo\ ?access_token=0d492486-11a7-4175-b116-2fc1cbff6d78
{ "scope": [ "read" ], "grant_type": "client_credentials", "realm": "/alpha", "write": false, "read": true, "token_type": "Bearer", "expires_in": 24, "access_token": "c8860442-daba-4af0-a1d9-b607c03e5a0b" }
As seen in this example, the requested scope read
is authorized, but the write
scope has not been authorized.