15 Customizing the OAuth Service

This chapter discusses how to customize the Oracle Access Management OAuth server using the plug-in framework. This chapter includes the following topics:

15.1 Introduction

The Oracle Access Management OAuth server utilizes a plug-in framework that lets you customize and extend functionality in the following areas using Java interfaces.

  • Client Management - This plug-in delegates client authentication, authorization, and profile management to an external module.

  • Resource-Server Profile Management - This plug-in delegates resource server authentication, authorization, and profile management to an external module.

  • Token Attributes - This plug-in allows administrators to add custom claims to generated access tokens.

  • Authorization and Consent Service - This plug-in handles authorization duties during user consent-based authorization.

  • Adaptive Access Security - This plug-in integrates adaptive access security applications, such as Oracle Adaptive Access Manager, with the OAuth service.

15.2 Creating a Custom Client Management Plug-in

The client management plug-in delegates the following client functions to an external module:

  • client authentication

  • client authorization (the evaluation of privileges)

  • client profile management (both registration and reading)

An administrator must configure the OAuth client profile before the OAuth service can process an authorization request from the OAuth client. When the request is sent, the client plug-in evaluates which grant types, scopes, and so on the client can access.

You or another OAuth developer can write a custom plug-in implementation to address custom requirements, for example creating a custom client authentication mechanism, or storing OAuth client profiles in an external repository instead of the default client MBean repository, and so on. The Client Management Plug-in consists of the following three Java interfaces:

  • Client Authenticator Interface - Verifies an OAuth client credential and redirect URI.

  • Client Privilege Interface - Evaluates if an OAuth client has the privileges necessary to use certain OAuth grant types and scopes, as well as the user consent required for the requested scopes. The OAuth Service framework sends the requested parameters along with the client IP address to the plug-in during client privilege checking.

  • Dynamic Client Registration Interface - Writes OAuth client profiles to the client repository and retrieves the profiles as needed. This interface has two parts, the client profile reader and the client profile writer. This interface is consumed by the OAuth service runtime.

15.2.1 The Default Client Management Plug-in Implementation

The default implementation consists of the Client Profile Reader, the Client Authenticator, and the Client Privilege interfaces. The IDM OAuth Service invokes the default plug-in implementation during runtime, and the plug-in utilizes the OAuth MBean repository server to read, authenticate, and evaluate client privileges. The OAuth MBean Console and the WLST command-line read and write OAuth client profiles from the OAuth MBean repository using the MBean API.

15.2.2 The Client Runtime Flow

  • The client application sends a client_id parameter as part of an authorization request and a client_id+client_secret Base64 value as an authorization header in its token endpoint requests.

  • The plug-in validates the client ID and secret values with the client repository.

  • During validation, the plug-in compares the client ID with the client definitions stored in oauth.xml (or in LDAP and potentially in other repositories). The secret is validated with CSF.

  • If the client_id value in the authorization request is invalid, the authorization endpoint responds with an invalid_client error. The token endpoint validates the Base64 authorization header value.

  • If the client credentials are valid, then they are embedded in the response along with the authorization code and access tokens. For example, an issued JWT authorization code and access token includes this claim:

    "oracle.oauth.client_origin_id":"2a180cbc780742698cf51d9a19f80ff6"
    
  • The plug-in checks if the client is requesting its configured scopes. If not, the plug-in rejects the request and sends an error response.

  • The plug-in checks if the client is requesting its configured grant flow. If not, the plug-in rejects the request and sends an error response.

The following example shows the client_id and client_secret attributes used in an authorization code request:

GET /authorize?response_type=code
&client_id=s6BhdRkqt3&state=xyz     
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Freturn
&scope=user_read

The following example shows the client ID and secret sent in a Basic authorization header:

POST /token HTTP/1.1      
Host: server.example.com      
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW      
Content-Type: application/x-www-form-urlencoded;charset=UTF-8       
grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA      
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb

15.2.3 Deployment Notes

Refer to the following notes when deploying a custom plug-in.

  • To deploy the plug-in, copy the JAR file to the following location:

     $ORACLE_HOME/user_projects/domains/base_domain/config/fmwconfig/oic/plugins/
    
  • If any third-party libraries were used to develop the custom plug-in, they need to be available in the container (WebLogic or WebSphere) classpath.

  • Restart the managed server after deploying the JAR files.

Use the OAM console to create your new client security plug-in.

  1. Log in to the OAM console.

  2. From the Launch Pad, choose OAuth Service > Specific Domain > OAuth Plug-ins > Client Plug-ins.

  3. Create the new plug-in. Refer to the following screen capture for details.

  4. From the Launch Pad, choose OAuth Service > Specific Domain > OAuth Service Profiles > OAuth Service Profile.

    In the Plug-ins section, assign the client plug-in to the service profile by choosing it from the menu.

Plug-in configuration screen

15.2.4 Sample Code

package mypackage;
 
import java.util.ArrayList;
import oracle.security.idaas.oauth.client.ClientAuthenticationResponse;
import oracle.security.idaas.oauth.client.ClientAuthorizationResponse;
import oracle.security.idaas.oauth.client.ClientProfile;
import oracle.security.idaas.oauth.client.ClientRequest;
import oracle.security.idaas.oauth.client.ClientScopeProfile;
import oracle.security.idaas.oauth.client.ClientSecurityManager;
import oracle.security.idaas.oauth.client.ClientSecurityManagerException;
import oracle.security.idaas.oauth.client.ClientWritableProfile;
import oracle.security.idaas.oauth.common.appinfra.AppAuthnRequest;
import oracle.security.idaas.oauth.common.provider.exception.OAuthMisconfigurationException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.atomic.AtomicLong;
 
import oracle.security.idaas.oauth.client.ClientScopeWritableProfile;
import oracle.security.idaas.oauth.client.impl.ClientAuthenticationResponseImpl;
import oracle.security.idaas.oauth.client.impl.ClientAuthorizationResponseImpl;
import oracle.security.idaas.oauth.client.impl.ClientScopeWritableProfileImpl;
import oracle.security.idaas.oauth.client.impl.ClientWritableProfileImpl;
import oracle.security.idaas.oauth.common.Validator;
import oracle.security.idaas.oauth.common.appinfra.AppWritableProfile;
import oracle.security.idaas.oauth.common.appinfra.impl.AppWritableProfileImpl;
 
public class CustomClientSecurityPlugin implements ClientSecurityManager{
 
 
    protected static AtomicLong sequenceNumberGenerator = new AtomicLong(0);
    protected ReadWriteLock lock = new ReentrantReadWriteLock();
 
    public CustomClientSecurityPlugin() {
        super();
    }
 
    public void init(Map<String, Object> config)
            throws OAuthMisconfigurationException {
        System.out.println("CustomClientSecurityPlugin::init()");
    }
    
 
    @Override
    public ClientProfile readClientProfile(ClientRequest clientRequest) throws ClientSecurityManagerException {
        System.out.println("CustomClientSecurityPlugin::readClientProfile()");
        ClientProfile clientProfile = getClientProfile( clientRequest.getAppId() );      
        if ( clientProfile != null && clientRequest.getAppId().equals( clientProfile.getAppId()) )
          return clientProfile;
        else {
          System.out.println("CustomClientSecurityPlugin::readClientProfile():  No Client Profile found.");
          return null;
        }
    }
    
    
    public boolean isValidConfidentialSecret(AppAuthnRequest appAuthnRequest) {
        System.out.println("CustomClientSecurityPlugin::isValidConfidentialSecret()");
        boolean result = false;
        ClientProfile clientProfile = getClientProfile(appAuthnRequest.getAppId());
        if ( clientProfile != null && compareChars(appAuthnRequest.getSecret(), clientProfile.getAppSecret().getSecret()) )
          result = true;
        
        return result;
    }
 
    public boolean isValidRedirectURI(ClientRequest clientRequest) {
        System.out.println("CustomClientSecurityPlugin::isValidRedirectURI()");
        boolean result = false;
        return result;
 
    }
 
 
    @Override
    public boolean isUserConsentRequired(ClientRequest clientRequest) {
        System.out.println("CustomClientSecurityPlugin::isValidRedirectURI()");
        boolean result = false;
        ClientProfile clientProfile = getClientProfile(clientRequest.getAppId());
        if( clientProfile != null )
          result = clientProfile.getClientScopeProfile().isUserConsentRequired();
        return result;
    }
 
    @Override
    public void destroy() {
    }
 
    @Override
    public Collection<ClientProfile> readClientProfiles(ClientRequest clientRequest) throws ClientSecurityManagerException {
        System.out.println("CustomClientSecurityPlugin::readClientProfiles()");
        Collection<ClientProfile> clientProfiles = new HashSet<ClientProfile>();
        return clientProfiles;
    }
 
    @Override
    public ClientProfile write(ClientWritableProfile clientWritableProfile) throws ClientSecurityManagerException {
        throw new UnsupportedOperationException();
    }
 
    @Override
    public ClientProfile update(ClientWritableProfile clientWritableProfile) throws ClientSecurityManagerException {
        throw new UnsupportedOperationException();
    }
 
    @Override
    public void delete(ClientWritableProfile clientWritableProfile) throws ClientSecurityManagerException {
        throw new UnsupportedOperationException();
    }
 
    /**
     * This methods does perform client authentication against XML repository
     * This method returns only authentication result (true/false) to the IDM OAuth framework in R2 PS2.
     * In future, this method may send error message if authentication is failed and other additional attributes
     * which may be used for client authorization and authorization plugin
     * @param clientRequest contains requested client id, secret, ip address and requested map
     * @return ClientAuthenticationResponse contains authentication result, error message and additional attributes
     * @throws ClientSecurityManagerException
     */
    @Override
    public ClientAuthenticationResponse authenticate(ClientRequest clientRequest) throws ClientSecurityManagerException {
        ClientAuthenticationResponse clientAuthenticationResponse = new ClientAuthenticationResponseImpl();
        clientAuthenticationResponse.setResult(isValidConfidentialSecret(clientRequest));
        return clientAuthenticationResponse;
    }
 
    @Override
    public ClientAuthorizationResponse isAuthorized(ClientRequest clientRequest) throws ClientSecurityManagerException {
        ClientAuthorizationResponse clientAuthorizationResponse = new ClientAuthorizationResponseImpl();
     
        if (allowToUseScopes(clientRequest) &&  allowToUseGrantTypes(clientRequest)) {
            clientAuthorizationResponse.setResult(true);
        } else {
            clientAuthorizationResponse.setResult(false);
        }
 
        return clientAuthorizationResponse;
 
    }
 
    private ClientProfile getClientProfile(String appid) {
      List<ClientProfile> CProfiles = getClientProfiles();
      for (ClientProfile profile: CProfiles) {
        if (appid.equals( profile.getAppId()))
          return profile;
      }
      return null;
    }
       
     private List<ClientProfile> getClientProfiles() {
       List<ClientProfile> CProfiles = new ArrayList<ClientProfile>();
       
       ClientWritableProfile clientWritableProfile = new ClientWritableProfileImpl();
       List<String> grantTypes = new ArrayList<String>();
       grantTypes.add( Validator.OAUTH_STD_GRANT_TYPE_CLIENT_CRED );
       grantTypes.add( Validator.OAUTH_STD_GRANT_TYPE_RESOURCE_OWNER_PW_CRED );
       grantTypes.add( Validator.OAUTH_GRANT_TYPE_JWT_BEARER );
       grantTypes.add( Validator.OAUTH_GRANT_TYPE_SAML2_BEARER );
       
       AppWritableProfileImpl appWritableProfileImpl = new AppWritableProfileImpl();
       AppWritableProfile.AppWritableSecret appWritableSecret = appWritableProfileImpl.new AppWritableSecretImpl();
       appWritableSecret.setSecret("welcome1".toCharArray());
 
       ClientScopeWritableProfile clientScopeWritableProfile = new ClientScopeWritableProfileImpl();
       clientScopeWritableProfile.setAnyScopeAllowed(false);
       clientScopeWritableProfile.setUserConsentRequired(false);
       clientScopeWritableProfile.setAllowedResourceServers(Collections.<String>emptySet());
       clientScopeWritableProfile.addAllowedScope("DocResourceServiceProfile.ALL");
       clientScopeWritableProfile.addAllowedScope("MessagingResourceServiceProfile.ALL");
       
       clientWritableProfile.setAllowedGrantTypes(grantTypes);
       clientWritableProfile.setAppId( "f35eed9e0cb3471bbd5a6a19919c7a78");      
       clientWritableProfile.setAppSecret(appWritableSecret);
       clientWritableProfile.setClientScopeProfile(clientScopeWritableProfile);
       clientWritableProfile.setClientType(ClientProfile.ClientType.CONFIDENTIAL_CLIENT);
       clientWritableProfile.setSequenceNumber( (new Long(sequenceNumberGenerator.get()).longValue() ));
       
       CProfiles.add(  clientWritableProfile );
       
       clientWritableProfile = new ClientWritableProfileImpl(); 
       clientScopeWritableProfile = new ClientScopeWritableProfileImpl();
       clientScopeWritableProfile.setAnyScopeAllowed(false);
       clientScopeWritableProfile.setUserConsentRequired(false);
       clientScopeWritableProfile.setAllowedResourceServers(Collections.<String>emptySet());
       clientScopeWritableProfile.addAllowedScope("DocResourceServer.ALL");
 
       clientWritableProfile.setAllowedGrantTypes(grantTypes);
       clientWritableProfile.setAppId( "a0709401479645c2923142e04dbd483f");      
       clientWritableProfile.setAppSecret(appWritableSecret);
       clientWritableProfile.setClientScopeProfile(clientScopeWritableProfile);
       clientWritableProfile.setClientType(ClientProfile.ClientType.CONFIDENTIAL_CLIENT);
       clientWritableProfile.setSequenceNumber( (new Long(sequenceNumberGenerator.get()).longValue() ));
       
       CProfiles.add(  clientWritableProfile );
       return CProfiles;
       
     }
     
    private  boolean compareChars(char source[], char destion[]) {
        return new String(source).equals(new String(destion));
    }
    
    private boolean allowToUseScopes(ClientRequest clientRequest)
    throws ClientSecurityManagerException {
        final String sourceMethod = "allowToUseScopes";
        boolean result = false;
        
        ClientProfile clientProfile = getClientProfile( clientRequest.getAppId() );
        if (clientProfile == null)
          return result;
        
        ClientScopeProfile clientScopeProfile = clientProfile.getClientScopeProfile();
        if (clientScopeProfile != null && !clientRequest.getScopes().isEmpty()) {
            if (clientScopeProfile.isAnyScopeAllowed()) {
                result = true;
            } else if (clientScopeProfile.getAllowedScopes().containsAll(clientRequest.getScopes())) {
                result = true;
            } else {
                //TODO:  verify resource server level scopes 
            }
        } else {
  
            //some case scope is not in a part of request
            //for example create UT and CT creation (Identity domain)
            return true;
        }
        return result;
    }
 
    private boolean allowToUseGrantTypes(ClientRequest clientRequest)
    throws ClientSecurityManagerException {
        final String sourceMethod = "allowToUseGrantTypes";  
        boolean result = false;
        
        ClientProfile clientProfile = getClientProfile( clientRequest.getAppId() );      
        if( clientProfile == null )
          return result;
        
        Collection<String> allowedGrantTypes = clientProfile.getAllowedGrantTypes();
        if (allowedGrantTypes != null) {
            if (!clientRequest.getGrantTypes().isEmpty() && allowedGrantTypes.containsAll(clientRequest.getGrantTypes())) {
                result = true;
            }
        }
        return result;  
  
    }
}

15.3 Creating a Custom Resource Server Profile-Management Plug-in

The resource server profile-management plug-in delegates the following functions to an external module:

  • resource server profile management

The plug-in reads and writes OAuth resource server profiles from the resource server repository. This plug-in is used to read and write OAuth resource server profiles from the resource server repository. This plug-in consists of two Java interfaces. One interface is a resource profile reader, and the other is a resource profile-writer interface. This plug-in is consumed by the OAuth runtime server.

15.3.1 The Default Resource Server Profile-Management Plug-in Implementation

The default plug-in implements the Resource Server Profile Reader interface. The plug-in reads profiles from the OAuth MBean repository. An OAuth administrator can use either the OAM console or the WLST command-line to read and write OAuth Resource Server profiles. (Both the console and the command line use the MBean API to interact with the OAuth MBean repository.)

15.3.2 Resource Server Usage and Validation

  • Resource servers are identified through the usage of scopes in the authorization request. The client application sends the scope parameter as part of an authorization request.

  • The scope parameter value is compared with the resource and scope value stored in the repository. The definitions are stored in oauth.xml.

  • If the scope parameter value is invalid, an invalid_scope error response is sent to the client application.

  • If the scope parameter value is valid, then it gets embedded in the response along with the authorization code and access tokens. For example, an issued JWT authorization code and access token includes this claim:

    "oracle.oauth.scope":"resumes.position.pmts"
    
  • When the client application accesses the resource with an access token, the resource server can either validate locally, or it can check with the OAuth Service whether an access token for a given scope is valid before allowing access. The resource server sends a resource server ID and a resource server secret as an authorization header Base64 value. It also sends these parameters as POST body of the validate access token call:

    • grant_type. For example:

      grant_type=oracle-idm:/oauth/grant-type/resource-access-token/jwt
      
    • oracle_token_action. For example:

      oracle_token_action=validate
      
    • scope. For example:

      scope=resumes.position.pmts
      
    • assertion. For example:

      assertion=accessToken-value
      
  • Finally, the OAuth service validates the access token and sends a JSON response that indicates successful or invalid scope usage.

15.3.3 Development and Deployment Notes

Refer to the following notes when deploying a custom plug-in.

  • To deploy the plug-in, copy the JAR file to the following location:

     $ORACLE_HOME/user_projects/domains/base_domain/config/fmwconfig/oic/plugins/
    
  • If any third-party libraries were used to develop the custom plug-in, they need to be available in the container (WebLogic or WebSphere) classpath.

  • Restart the managed server after deploying the JAR files.

Use the OAM console to create your new client security plug-in.

  1. Log in to the OAM console.

  2. From the Launch Pad, choose OAuth Service > Specific Domain > OAuth Plug-ins > Resource Server Profile Plug-ins.

  3. Create the new plug-in.

    Refer to the screen capture for details.

  4. From the Launch Pad, choose OAuth Service > Specific Domain > OAuth Service Profiles > OAuth Service Profile.

    In the Plug-ins section, assign the resource server profile plug-in to the service profile by choosing it from the menu.

    Make sure the Allow clients access to all resource servers option (under Custom Resource Servers) is enabled.

Plug-in configuration screen

Note:

The password field is shown for demonstration purposes. Use other literals instead. If you use password or secret literals, the system stores their value in the deployment's credential storage where third-party plug-in code cannot retrieve them.

Refer to the following notes when developing a custom plug-in.

  • The following JAR files are needed for plug-in development and compilation:

    • oauth_common.jar

    • oic_common.jar

    • ms_oauth.jar

  • Pay attention to collection usage with regards to scopes. The framework uses List and HashSet types, so the implementation should not do any casting. Otherwise get ClassCast exceptions.

  • The scope description retrieval expects the locale to be set in the plug-in implementation, otherwise a null pointer exception occurs. You can modify this by changing the if condition as follows.

@Override
public ScopeDescriptionProfile getScopeDescription(Locale locale) {
   if (this.scopeDescriptionProfiles != null) {
      for (ScopeDescriptionProfile scopeDescriptionProfile : scopeDescriptionProfiles) {
         if (scopeDescriptionProfile.getLocale().equals(locale)) {
             return scopeDescriptionProfile;
}

15.3.4 Sample Code

package com.db;
 
import oracle.security.idaas.oauth.common.provider.exception.OAuthMisconfigurationException;
import oracle.security.idaas.oauth.resourceserver.*;
 
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
 
/**
 * Custom resource plug-in sample. This plug-in demonstrates the development of a custom plug-in
 * for resource server profile service. In this specific sample, resource server profiles are 
 * defined in a database. So the logic typically revolves around CRUD operations, and in this      
 * specific sample various retrieval methods are implemented, which get used by the OAuth server 
 * runtime.
 */

public class ACMEResourceSvrPlugin implements ResourceServerProfileService {
    //plugin config
    Map<String, Object> pluginConfig = new HashMap<String, Object>();
 
    // database util gets used for CRUD operations
    DBUtil dbUtil = new DBUtil();
 
    @Override
    public void init(Map<String, Object> pluginAttrs) throws OAuthMisconfigurationException {
        //initialize plugin config as set
        pluginConfig = pluginAttrs; //
 
    }
 
    @Override
    public void destroy() {
        // destroy plugin config
        pluginConfig.clear();
    }
 
    @Override
    public ResourceServerProfile readResourceServerProfile(ResourceServerProfileRequest resourceServerProfileRequest)
            throws ResourceServerProfileNotFoundException {
        // get resource server profile based on resource server name
        return dbUtil.getResSvrByName(resourceServerProfileRequest.getAppName(),
                pluginConfig);
    }
 
    @Override
    public Collection<ResourceServerProfile> readResourceServerProfiles(ResourceServerProfileRequest resourceServerProfileRequest)
            throws ResourceServerProfileNotFoundException {
        // get all resource server profiles
        return dbUtil.getAllResSvrs(pluginConfig);
    }
 
    @Override
    public Collection<ResourceServerProfile> readResourceServerProfileByRequestedScope(ResourceServerProfileRequest resourceServerProfileRequest)
            throws ResourceServerProfileServiceException {
        // get all resource server profiles based on requested scopes
        return dbUtil.searchWithScopesList(resourceServerProfileRequest.getRequestedScopes(),
                pluginConfig);
    }
 
    @Override
    public ResourceServerProfile write(ResourceServerWritableProfile resourceServerWritableProfile)
            throws ResourceServerProfileNameAlreadyUsedException, ResourceServerProfileIdAlreadyUsedException, ResourceServerProfileServiceException {
        //not implemented
        throw new UnsupportedOperationException();
    }
 
    @Override
    public ResourceServerProfile update(ResourceServerWritableProfile resourceServerWritableProfile)
            throws ResourceServerProfileNotFoundException, ResourceServerProfileServiceException {
        // not implemented
        throw new UnsupportedOperationException();
    }
 
    @Override
    public void delete(ResourceServerProfileRequest resourceServerProfileRequest)
            throws ResourceServerProfileNotFoundException, ResourceServerProfileServiceException {
        // not implemented
        throw new UnsupportedOperationException();
    }
}

DBUtil.java Code Sample

package com.db;
 
import oracle.security.idaas.oauth.common.appinfra.AppWritableProfile;
import oracle.security.idaas.oauth.common.appinfra.impl.AppWritableProfileImpl;
import oracle.security.idaas.oauth.resourceserver.*;
import oracle.security.idaas.oauth.resourceserver.impl.*;
 
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.*;
 
/**
 * Custom resource plug-in sample. This is a utility class which interacts with the database
 * for doing CRUD operations. This specific sample shows how various retrieval methods are 
 * implemented.
 */
public class DBUtil {
    private final static String CONN_URL = "connUrl";
    private final static String CONN_USER = "user";
    private final static String CONN_USER_PWD = "password";
 
    /**
     * Get resource profile by resource server profile name
     * @param resName
     * @param pluginConfig
     * @return
     * @throws ResourceServerProfileNotFoundException
     */
    public ResourceServerProfile getResSvrByName(String resName,
                                                  Map<String, Object> pluginConfig)
            throws ResourceServerProfileNotFoundException {
        Connection dbcon;
        ResourceServerWritableProfile resProfile = null;
        try {
            dbcon = DriverManager.getConnection((String) pluginConfig.get(CONN_URL),
                    (String) pluginConfig.get(CONN_USER),
                    (String) pluginConfig.get(CONN_USER_PWD));
            Statement stmt = dbcon.createStatement();
            System.out.println("searchWithScopesList: BEFORE LIST");
            String resQuery = "select NAME ," +
                    "      AUTHZ_PLUGIN_REF ," +
                    "      SCOPE_PREFIX ," +
                    "      REFRESH_TOKEN_EXPIRY_TIME ," +
                    "      ACCESS_TOKEN_OVERRIDDEN ," +
                    "      DOMAIN_UUID ," +
                    "      AUD_CLAIM ," +
                    "      ALLOW_TOKEN_ATTR_RETRIEVAL ," +
                    "      LAST_UPDATE_TIME ," +
                    "      ID ," +
                    "      DESCR ," +
                    "      ACCESS_TOKEN_EXPIRY_TIME ," +
                    "      OFFLINE_SCOPE_NAME," +
                    "      OVERRIDE_TOKEN_SETTINGS" +
                    " from oauth_resource_svr where name = '" + resName + "'" ;
            ResultSet rslt = stmt.executeQuery(resQuery);
 
            if (rslt.next()) {
                resProfile = new ResourceServerWritableProfileImpl();
                System.out.println("searchWithScopesList: RES: " + rslt.getString("id") + "\t" + rslt.getString("name"));
                String resId = rslt.getString("id");
                populateResSvrPrimaryData(rslt, resProfile);
                //get scopes data  and populate resource profile
                populateScopesData(resId, stmt, resProfile);
 
                //get static and dynamic attributes data and populate them
                populateTokenCustomAttributes(resId, stmt, resProfile);
 
                //get resource attributes data
                populateResSvrAttributes(resId, stmt, resProfile);
 
            }
            dbcon.close();
 
 
        } catch (Exception ex) {
            System.out.println(ex);
        }
 
        if (resProfile == null) {
            throw new ResourceServerProfileNotFoundException("Invalid resource name: " + resName );
        }
        return resProfile;
    }
 
    /**
     * Get all resource server profiles.
     * @param pluginConfig
     * @return
     * @throws ResourceServerProfileNotFoundException
     */
    public Collection<ResourceServerProfile> getAllResSvrs(Map<String, Object> pluginConfig)
            throws ResourceServerProfileNotFoundException {
        Connection dbcon;
        List<ResourceServerProfile> listResources =
                new ArrayList<ResourceServerProfile>();
        try {
            dbcon = DriverManager.getConnection((String) pluginConfig.get(CONN_URL),
                    (String) pluginConfig.get(CONN_USER),
                    (String) pluginConfig.get(CONN_USER_PWD));
            Statement stmt = dbcon.createStatement();
            System.out.println("searchWithScopesList: BEFORE LIST");
            String resQuery = "select NAME ," +
                    "      AUTHZ_PLUGIN_REF ," +
                    "      SCOPE_PREFIX ," +
                    "      REFRESH_TOKEN_EXPIRY_TIME ," +
                    "      ACCESS_TOKEN_OVERRIDDEN ," +
                    "      DOMAIN_UUID ," +
                    "      AUD_CLAIM ," +
                    "      ALLOW_TOKEN_ATTR_RETRIEVAL ," +
                    "      LAST_UPDATE_TIME ," +
                    "      ID ," +
                    "      DESCR ," +
                    "      ACCESS_TOKEN_EXPIRY_TIME ," +
                    "      OFFLINE_SCOPE_NAME," +
                    "      OVERRIDE_TOKEN_SETTINGS" +
                    " from oauth_resource_svr " ;
            ResultSet rslt = stmt.executeQuery(resQuery);
            while (rslt.next()) {
                ResourceServerWritableProfile testProfile = new ResourceServerWritableProfileImpl();
                System.out.println("searchWithScopesList: RES: " + rslt.getString("id") + "\t" + rslt.getString("name"));
                String resId = rslt.getString("id");
                populateResSvrPrimaryData(rslt, testProfile);
                //get scopes data  and populate resource profile
                populateScopesData(resId, stmt, testProfile);
 
                //get static and dynamic attributes data  and populate them
                populateTokenCustomAttributes(resId, stmt, testProfile);
 
                //get resource attributes data
                populateResSvrAttributes(resId, stmt, testProfile);
                listResources.add(testProfile);
            }
            dbcon.close();
 
 
        } catch (Exception ex) {
            System.out.println(ex);
        }
 
        if (listResources.isEmpty()) {
            throw new ResourceServerProfileNotFoundException("Not able to retrieve any resources, may be resource table has no data");
        }
        return listResources;
    }
 
    /**
     * Get resource server profiles based on requested scopes.
     * @param scopeList
     * @param pluginConfig
     * @return
     * @throws ResourceServerProfileNotFoundException
     */
    public Collection<ResourceServerProfile> searchWithScopesList(Collection<String> scopeList,
                                     Map<String, Object> pluginConfig) throws ResourceServerProfileNotFoundException {
        Connection dbcon;
        List<ResourceServerProfile> listResources =
                new ArrayList<ResourceServerProfile>();
        try {
            dbcon = DriverManager.getConnection((String) pluginConfig.get(CONN_URL),
                    (String) pluginConfig.get(CONN_USER),
                    (String) pluginConfig.get(CONN_USER_PWD));
            Statement stmt = dbcon.createStatement();
            System.out.println("searchWithScopesList: BEFORE LIST");
            String scopeQueryPat = "select id from oauth_resource_svr_scope where name in (%s)";
            String scopeQuery = String.format(scopeQueryPat, prepareInQueryPart(scopeList));
            System.out.println("searchWithScopesList: scopeQuery :" + scopeQuery);
            String resQuery = "select NAME ," +
                    "      AUTHZ_PLUGIN_REF ," +
                    "      SCOPE_PREFIX ," +
                    "      REFRESH_TOKEN_EXPIRY_TIME ," +
                    "      ACCESS_TOKEN_OVERRIDDEN ," +
                    "      DOMAIN_UUID ," +
                    "      AUD_CLAIM ," +
                    "      ALLOW_TOKEN_ATTR_RETRIEVAL ," +
                    "      LAST_UPDATE_TIME ," +
                    "      ID ," +
                    "      DESCR ," +
                    "      ACCESS_TOKEN_EXPIRY_TIME ," +
                    "      OFFLINE_SCOPE_NAME," +
                    "      OVERRIDE_TOKEN_SETTINGS" +
                    " from oauth_resource_svr where id in (" + scopeQuery + ")" ;
            ResultSet rslt = stmt.executeQuery(resQuery);
            while (rslt.next()) {
                ResourceServerWritableProfile testProfile = new ResourceServerWritableProfileImpl();
                System.out.println("searchWithScopesList: RES: " + rslt.getString("id") + "\t" + rslt.getString("name"));
                String resId = rslt.getString("id");
                populateResSvrPrimaryData(rslt, testProfile);
                //get scopes data  and populate resource profile
                populateScopesData(resId, stmt, testProfile);
 
                //get static and dynamic attributes data  and populate them
                populateTokenCustomAttributes(resId, stmt, testProfile);
 
                //get resource attributes data
                populateResSvrAttributes(resId, stmt, testProfile);
                listResources.add(testProfile);
            }
            dbcon.close();
 
 
        } catch (Exception ex) {
            System.out.println(ex);
        }
        if (listResources.isEmpty()) {
            throw new ResourceServerProfileNotFoundException("Invalid scopes: " + scopeList );
        }
        return listResources;
    }
 
    /**
     * Populates the resource server profile writable instance with data retrieved from the 
     * database
     * @param rslt
     * @param resourceServerWritableProfile
     */
    private static void populateResSvrPrimaryData(ResultSet rslt,
                                                  ResourceServerWritableProfile resourceServerWritableProfile) {
        try {
            resourceServerWritableProfile.setAppId(rslt.getString("ID"));
            resourceServerWritableProfile.setAppName(rslt.getString("NAME"));
            resourceServerWritableProfile.setIdentityDomainUUID(rslt.getString("DOMAIN_UUID"));
            resourceServerWritableProfile.setAudienceClaimValue(rslt.getString("AUD_CLAIM"));
            resourceServerWritableProfile.setAuthzUserConsentPluginRef(rslt.getString("AUTHZ_PLUGIN_REF"));
            resourceServerWritableProfile.setOfflineScopeName(rslt.getString("OFFLINE_SCOPE_NAME"));
            resourceServerWritableProfile.setScopeNamespacePrefix(rslt.getString("SCOPE_PREFIX"));
 
            if ("Y".equalsIgnoreCase(rslt.getString("OVERRIDE_TOKEN_SETTINGS"))) {
                Long rtExp =  rslt.getLong("REFRESH_TOKEN_EXPIRY_TIME");
                if (rtExp != null ) {
                    System.out.println("RES: REFRESH_TOKEN_EXPIRY_TIME " + rslt.getLong("REFRESH_TOKEN_EXPIRY_TIME"));
 
                    OAuthRefreshableTokenWritableProfile oAuthRefreshableTokenWritableProfile
                        = new OAuthRefreshableTokenWritableProfileImpl();
                    oAuthRefreshableTokenWritableProfile.setRefreshTokenExpiresIn(rtExp);
                    resourceServerWritableProfile.setOverriddenAccessTokenProfile(oAuthRefreshableTokenWritableProfile);
                }
 
                Long atExp = rslt.getLong("ACCESS_TOKEN_EXPIRY_TIME");
                if ( atExp != null ) {
                    OAuthTokenWritableProfile oAuthTokenWritableProfile = new OAuthTokenWritableProfileImpl();
                    oAuthTokenWritableProfile.setExpiresIn(atExp);
                    resourceServerWritableProfile.setOverriddenAuthzCodeTokenProfile(oAuthTokenWritableProfile);
                }
            }
 
            AppWritableProfile.AllowedTokenAttributesRetrievalWritableProfile allowedTokenAttributesRetrievalWritableProfile =
                new AppWritableProfileImpl().new AllowedTokenAttributesRetrievalWritableProfileImpl();
            if ("Y".equalsIgnoreCase(rslt.getString("ALLOW_TOKEN_ATTR_RETRIEVAL"))) {
                allowedTokenAttributesRetrievalWritableProfile.setAllTokenAttributesRetrievalAllowed(true);
            } else {
                allowedTokenAttributesRetrievalWritableProfile.setAllTokenAttributesRetrievalAllowed(false);
            }
            resourceServerWritableProfile.setAllowedTokenAttributesRetrieval(allowedTokenAttributesRetrievalWritableProfile);
        } catch (Exception e) {
            System.out.println("ACMEResourceSvrPlugin: DBUtil: populateResSvrPrimaryData: " + e) ;
        }
    }
 
    /**
     * Populates resource server profile writable instance with scopes data.
     * @param resId
     * @param stmt
     * @param resourceServerWritableProfile
     */
    private static void populateScopesData(String resId,
                                           Statement stmt,
                                           ResourceServerWritableProfile resourceServerWritableProfile)   {
        try {
            String scopeQuery1 = "select name, " +
                "descr, " +
                "user_consent_required " +
                "from OAUTH_RESOURCE_SVR_SCOPE where id='" + resId + "'";
            ResultSet scopeRslt = stmt.executeQuery(scopeQuery1);
            while (scopeRslt.next()) {
                 System.out.println("RES SCOPES: " +scopeRslt.getString("name") + "\t" + scopeRslt.getString("descr"));
                 ScopeWritableProfile scopeProfile = new ScopeWritableProfileImpl();
                 //set scope value
                 scopeProfile.setName(scopeRslt.getString("name"));
                //set user consent required flag
                if ("Y".equalsIgnoreCase(scopeRslt.getString("user_consent_required"))) {
                    scopeProfile.setUserConsentRequired(true);
                } else {
                    scopeProfile.setUserConsentRequired(false);
                }
 
                // set scope description
                if (scopeRslt.getString("descr") != null) {
                    ScopeWritableProfile.ScopeDescriptionWritableProfile scopeDescriptionWritableProfile = new ScopeWritableProfileImpl.ScopeDescriptionWritableProfileImpl();
                    scopeDescriptionWritableProfile.setDescription(scopeRslt.getString("descr"));
                    scopeDescriptionWritableProfile.setLocale(Locale.ENGLISH);
                    scopeProfile.addScopeDescription(scopeDescriptionWritableProfile);
                }
                resourceServerWritableProfile.addScopeProfile(scopeProfile);
            }
       } catch (Exception e) {
 
            System.out.println("ACMEResourceSvrPlugin: DBUtil: populateScopesData: " + e) ;
        }
    }
 
    /**
     * Populates resource server profile writable instance with access token's custom attributes 
     * data.
     * @param resId
     * @param stmt
     * @param resourceServerWritableProfile
     */
    private static void populateTokenCustomAttributes(String resId,
                                                      Statement stmt,
                                                      ResourceServerWritableProfile resourceServerWritableProfile) {
        try {
            TokenAttributeWritableProfile tokenAttributeWritableProfile
                    = new TokenAttributeWritableProfileImpl();
            String stQuery = "select name, " +
                    "value " +
                    "from OAUTH_RESOURCE_SVR_AT_STATTRS where id='" + resId + "'";
            ResultSet stRslt = stmt.executeQuery(stQuery);
            while (stRslt.next()) {
                System.out.println("RES STATIC ATTRS: " +  stRslt.getString("name") + "\t" + stRslt.getString("value"));
                tokenAttributeWritableProfile.addTokenStaticAttribute(stRslt.getString("name"),
                        stRslt.getString("value"));
 
            }
 
            //get dynamic attributes data
            String dyQuery = "select name " +
                    "from OAUTH_RESOURCE_SVR_AT_DYNATTRS where id='" + resId + "'";
            ResultSet dyRslt = stmt.executeQuery(dyQuery);
            while (dyRslt.next()) {
                System.out.println("RES DYN ATTRS: " +dyRslt.getString("name"));
                tokenAttributeWritableProfile.addTokenDynamicAttribute(dyRslt.getString("name"));
            }
            resourceServerWritableProfile.setTokenAttributeProfile(tokenAttributeWritableProfile);
        } catch (Exception e) {
 
            System.out.println("ACMEResourceSvrPlugin: DBUtil: populateTokenCustomAttributes: " + e) ;
        }
    }
 
    /**
     * Populates resource server profile writable instance with additional attributes data.
     * @param resId
     * @param stmt
     * @param resourceServerWritableProfile
     */
    private static void populateResSvrAttributes(String resId,
                                                      Statement stmt,
                                                      ResourceServerWritableProfile resourceServerWritableProfile) {
        try {
            String attrsQuery = "select name, " +
                    "value " +
                    "from OAUTH_RESOURCE_SVR_ATTRS where id='" + resId + "'";
            ResultSet attrsRslt = stmt.executeQuery(attrsQuery);
            Map<String, String> attrs = new HashMap<String, String>();
            while (attrsRslt.next()) {
 
                System.out.println("RES ATTRS: " +attrsRslt.getString("name") + "\t" + attrsRslt.getString("value"));
                attrs.put(attrsRslt.getString("name"), attrsRslt.getString("value"));
            }
 
            if (!attrs.isEmpty()) {
                resourceServerWritableProfile.setAttributes(attrs);
            }
        } catch (Exception e) {
 
            System.out.println("ACMEResourceSvrPlugin: DBUtil: populateResSvromAttributes: " + e) ;
        }
 
    }
 
    private static String prepareInQueryPart(Collection<String> listStr) {
        StringBuilder builder = new StringBuilder();
        Iterator itr = listStr.iterator();
        for (int i = 0; i < listStr.size();) {
            builder.append("'");
            //builder.append(listStr.get(i));
            builder.append((String)itr.next()) ;
            builder.append("'") ;
            if (++i < listStr.size()) {
                builder.append(",");
            }
        }
        System.out.println("ACMEResourceSvrPlugin: prepareInQueryPart: " + builder.toString()) ;
        return builder.toString();
    }
}

15.4 Creating a Custom Token Attributes Plug-in

OAuth server provides ways for adding custom attributes to access tokens based on server configuration. There are two types of attributes:

  • Static attributes - These are defined as attribute name and value pairs. The value is fixed at the time of attribute definition. For example: name1=value1.

  • Dynamic (user) attributes - These attributes are limited to user profile specific attributes. The OAuth server needs to designate the source of the user profile attributes. The user profile service may be used for attribute name and attribute value retrieval.

Static and dynamic attributes can be defined for two entities in the IDM OAuth token server: (1) OAuth Service Profile (2) Resource Server

  • Static attribute names should not conflict with dynamic ones. Because dynamic attribute names are known to the system, name collision will be prevented. However in a corner case where a static attribute is defined first and a related dynamic attribute with the same name is later introduced into the system, the static attribute should be removed first.

  • If the same attributes are defined in both of these entities, then the Resource Server based attributes will be taken into consideration for population into an access token.

  • Because dynamic attributes are related to users, a user consent page will show (if configured) to request the user's consent. The user acknowledges that the configured attributes will be shared with clients/resources for information purposes.

Custom attributes appear as-is as claims in the access token. The OAuth server issues a JWT-based access token that contains standard JWT claims along with OAuth server-specific ones. Here is a sample set:

  • Standard

    "exp":1357596398000,
    "iat":1357589198000,
    "aud":"oam_server1",
    "iss":"OAuthServiceProfile",
    "prn":null,
    "jti":"340c8324-e49f-43cb-ba95-837eb419e068",
    
    
  • OAuth server specific

    Note:

    The following may vary slightly based on 2-legged/3-legged and web/mobile scenarios.
    "oracle.oauth.user_origin_id":"john101",
    "oracle.oauth.user_origin_id_type":"LDAP_UID",
    "oracle:idm:claims:client:macaddress":"1C:AB:A7:A5:F0:DC",
    "oracle.oauth.scope":"brokerage",
    "oracle.oauth.client_origin_id":"oauthssoapp1id",
    "oracle.oauth.grant_type":"oracle-idm:/oauth/grant-type/resource-access-token/jwt"
    

The above claims are inherently available as part of an access token generated by the OAuth server. Because the custom attributes appear as claims in JWT-based access tokens, the following naming restrictions should be followed:

  • Avoid JWT standard claim names

  • Avoid the Oracle prefix (as shown above)

  • Avoid reserved words

When defining attributes, keep in mind the following:

  • The OAuth service profile may have 0..n static and dynamic attributes.

  • The resource server may have 0..n static and dynamic attributes.

  • Each scope may have 0..n static and dynamic attributes.

  • The static and dynamic type indicator may be needed for runtime usage.

15.4.1 Deployment Notes

Refer to the following notes when deploying a custom plug-in.

  • To deploy the plug-in, copy the JAR file to the following location:

     $ORACLE_HOME/user_projects/domains/base_domain/config/fmwconfig/oic/plugins/
    
  • If any third-party libraries were used to develop the custom plug-in, they need to be available in the container (WebLogic or WebSphere) classpath.

  • Restart the managed server after deploying the JAR files.

Use the OAM console to create your new client security plug-in.

  1. Log in to the OAM console.

  2. From the Launch Pad, choose OAuth Service > Specific Domain > OAuth Plug-ins > Custom Token Attributes Plug-ins.

  3. Create the new plug-in. Refer to the following screen capture for details.

  4. From the Launch Pad, choose OAuth Service > Specific Domain > OAuth Service Profiles > OAuth Service Profile.

    In the Plug-ins section, assign the custom token attributes plug-in to the service profile by choosing it from the menu.

Plug-in configuration screen

15.4.2 Sample Code

package mypackage;
 
import java.util.Map;
import oracle.security.idaas.rest.provider.plugin.HandlerRequest;
import oracle.security.idaas.rest.provider.plugin.HandlerResult;
import oracle.security.idaas.rest.provider.plugin.SecurityHandler;
 
public class CustomTokenAttrsPlugin implements SecurityHandler {
    public CustomTokenAttrsPlugin() {
        super();
        System.out.println("CustomTokenAttrsPlugin: in the constructor");
 
    }
    
    public void init(Map<String, String> config) {
        System.out.println("CustomTokenAttrsPlugin: in the constructor init: " + config.toString());
    }
 
    // get token claims from the config and add them to handler result
    public void processSecurityEvent(HandlerRequest req,
                                     HandlerResult result) { 
 
        String svcDynAttr1 = "thirdparty-svc-username";
        String svcStaticAttr1 = "thirdparty-svcs-st1";
        String rsrcDynAttr1 = "thridparty-rs-empnumber";
        String rsrcStaticAttr1 = "thridparty-rs-st1";
        
        result.addTokenClaim(svcStaticAttr1, "my-svc-st1-val");
        result.addTokenClaim(svcDynAttr1, "my-svc-margchou");
        result.addTokenClaim(rsrcStaticAttr1, "my-rs-st1-val");
        result.addTokenClaim(rsrcDynAttr1, "my-rs-123456");
        
        result.setResultStatus(HandlerResult.ResultStatus.ALLOW);       
 
    }
    
    public void destroy() {
        final String METHOD = "destroy()";
    } 
}

15.5 Creating a Custom Authorization and Consent Service Plug-in

The Authorization and Consent Service plug-in handles general authorization during user consent-based authorization. This plug-in can also influence claims in a generated token. This plug-in has two parts: the resource authorization service, and the user consent service.

The Resource Authorization Service

The IDM OAuth Service framework invokes the Resource Authorization Security Handler plug-in prior to processing OAuth access token requests, authorization code requests, and OAuth access token validation requests. The service then returns an authorization decision (that is, allowed or denied).

When invoking the Resource Authorization Server plug-in, the OAuth Service sends the plug-in the following information:

  • Requested scopesGrant typeClient IP addressUser and client authentication statusClient profile and resource server profile

The Resource Server Authorization plug-in uses the requested data and evaluates an authorization decision. In addition, the Resource Authorization Service plug-in may add a permission payload claim into the access token if an authorization decision is allowed.

User Consent Service

The User Consent Service stores, retrieves, and revokes user consent based on user ID, client ID, scope name, OAuth service profile, and identity domain. The User Consent Service is invoked by the OAuth Server framework when processing the authorization code and access token requests for scopes that require consent.

Note:

The OAuth administrator can define the resource authorization and user consent plug-in when configuring the OAuth Service Profile. The administrator can also override this plug-in reference by configuring the Resource Server Profile.

This plug-in is invoked during access token creation and authorization code creation for requested scopes. This plug-in cannot be invoked for identity client creation and user token creation (JWT User/Client Assertion).

15.5.1 The Default Resource Authorization and User Consent Services Implementations

There are two different implementations for the Authorization and Consent plug-in: the default authorization with OAuth database consent repository, and the REST callback authorization plug-in (for RAS).

The Default Authorization and Use Consent Plug-in Implementation

The default plug-in implements both the Authorization and user Consent services. User consent is managed using the OAuth database repository. When processing requests for access token creation and validation, the authorization implementation checks if the user has already given consent for the required scopes.

The REST Callback Resource Authorization Plug-in Implementation

This REST callback plug-in implements the Authorization Service interface only. It is used when an external authorization service is deployed for making the authorization decisions. Because user consent is not implemented in this plug-in, 2-legged OAuth is the intended use case. The invocation sequence is as follows:

OAuth Service Framework > REST Callback Resource Authorization Plug-in > (remote) RAS REST Services

This plug-in enables OAuth developers to write a REST implementation for Custom Resource Authorization Service and deploy it on a remote server. An OAuth administrator can define a REST authorization endpoint in the REST Callback Authorization Plug-in configuration using the OAM Console or WLST commands.