Showing posts with label custom handler. Show all posts
Showing posts with label custom handler. Show all posts

Wednesday, January 10, 2018

[WSO2] [APIM] Custom authentication handler for Okta based token validation

When invoking an API published in WSO2 API manager, the user has to send a valid access token (Oauth 2.0 token) in the Authorization header to verify the identity of the invoker. WSO2 API gateway calls the key validation service in the key manager component in order to check the validity of the token. If it is valid, the user can invoke the API and if not, the user will be served with a 401 (Unauthorized) response.
The key manager is usually a WSO2 component (APIM key manager, Identity Server as key manager). However, there can be some usecases where we need to integrate a third part key manager for authorizing API invocation requests. API manager provides an extension point for integrating third part identity providers as the key manager of APIM manager setup. You can follow [1] and integrate a third party key manager to WSO2 API manager deployment.

There can be situations, where the token was issued by a different key manager/Oauth identity provider which is not integrated to APIM and you just need to validate the token against the particular identity provider. In such cases, you can write a custom handler to replace the default Authentication handler which calls the default key validation service.

In this article, I will discuss on how to write a custom authentication handler to validate an access token (issued by Okta) against Okta.

This custom handler will replace the default authentication handler and validate the access token by calling the Okta introspection endpoint [3]. If the introspection endpoint identifies the token as valid, then the handler will authorize the request. If you want to control the API access based on the scopes, this handler can be modified to authorize the requests if and only if the given token was generated with a white listed scope (We have to maintained a set of white listed scopes in the properties file and validate the requests against it).

Following are the steps to configure the Okta based integration.

1. Download the source code here. Open the okta.properties file located at resources directory and provide the values for introspectionEndpoint, client_id and client_secret of the APP you have already created in Okta. You can provide 'NA' (client_secret=NA) if the id and secret values are not available for the app.

 Please enable debug logs if you want to check the requests being sent to the introspection endpoint.
  • open log4j.properties file located at <APIM_HOME>/repository/conf/ directory and add the following line:
     log4j.logger.org.wso2.carbon.authorization=DEBUG

2. Use Maven to build the project (mvn clean install), copy the authorization-1.0.0-SNAPSHOT.jar to <server-home>/repository/components/lib/ folder and restart the server.

 3. Create and publish your API using API publisher UI(Eg: let's assume API name is BlogPost, API version is 1.0.0 and user who published the API is admin)

4. Then go to the following directory
<API_MANAGER_HOME>/repository/deployment/server/synapse-configs/default/api/

5. Open the xml file with the following format

{API Provider}–{API Name}_v{Version}.xml(Eg: admin--BlogPost_v1.0.0.xml)

6. Replace
<handler class="org.wso2.carbon.apimgt.gateway.handlers.security.APIAuthenticationHandler"/>
with
<handler class="org.wso2.carbon.authorization.UserAuthorizationHandler"/>

7. Wait some time until he API get redeployed (Expects following message in <server-home>/repository/logs/wso2carbon.log file

[2017-02-08 11:51:54,988] INFO - API Initializing API: admin--BlogPost:v1.0.0
[2017-02-08 11:51:54,990] INFO - DependencyTracker API : admin--BlogPost:v1.0.0 was updated from the Synapse configuration successfully
[2017-02-08 11:51:54,990] INFO - APIDeployer API: admin-BlogPost:v1.0.0 has been updated from the file: /home/user/demo/setup/wso2am-2.1.0/repository/deployment/server/synapse-configs/default/api/admin--BlogPost_v1.0.0.xml
[2017-02-08 11:51:56,990] INFO - API Destroying API: admin--BlogPost:v1.0.0

8. Now the API is ready to be invoked with a token returned from Okta.

Sample request:

curl -X GET --header 'Accept: application/xml' --header 'Authorization: Bearer <complete token returned from okta>' 'https://10.200.7.41:8243/test/1/*' -k


We configured the custom handler through editing the API xml directly. This change will be overridden by the default handler if your republish the API. Also, your new apis will not be deployed with the custom handler. In order to make this change permanent, please modify the global API template (velocity_template) as follows:

1. Backup existing velocity_template.xml file located at
 <APIM_HOME>/repository/resources/api_templates/ directory and open it in an editor.

2. Replace the <handlers> element with following (this will replace the default handler with the custom handler we wrote):

<handlers xmlns="http://ws.apache.org/ns/synapse">
<handler class="org.wso2.carbon.authorization.UserAuthorizationHandler" />
#foreach($handler in $handlers)
#if(!($handler.className == "org.wso2.carbon.apimgt.gateway.handlers.security.APIAuthenticationHandler"))
<handler xmlns="http://ws.apache.org/ns/synapse" class="$handler.className">
#if($handler.hasProperties())
#set ($map = $handler.getProperties() )
#foreach($property in $map.entrySet())
<property name="$!property.key" value="$!property.value"/>
#end
#end
</handler>
#end
#end
</handlers>

3. Save the change and publish a test api to check whether the custom handler is properly placed as bellow in the api xml file located at <APIM_HOME>/repository/deployment/server/synapse-configs/default/api/ directory.


<handlers>
<handler class="org.wso2.carbon.authorization.UserAuthorizationHandler"/>
<handler class="org.wso2.carbon.apimgt.gateway.handlers.common.APIMgtLatencyStatsHandler"/>
<handler class="org.wso2.carbon.apimgt.gateway.handlers.security.CORSRequestHandler">
<property name="apiImplementationType" value="ENDPOINT"/>
</handler>
<handler class="org.wso2.carbon.apimgt.gateway.handlers.throttling.ThrottleHandler"/>
<handler class="org.wso2.carbon.apimgt.gateway.handlers.analytics.APIMgtUsageHandler"/>
<handler class="org.wso2.carbon.apimgt.gateway.handlers.analytics.APIMgtGoogleAnalyticsTrackingHandler">
<property name="configKey" value="gov:/apimgt/statistics/ga-config.xml"/>
</handler>
<handler class="org.wso2.carbon.apimgt.gateway.handlers.ext.APIManagerExtensionHandler"/>
</handlers>


Please note the following limitations as well


1. Since this approach replaces the APIM default key validation flow, you won't be able to create applications, generate access tokens and invoke apis through the WSO2 API store. You will have to pass a valid token retrieved from Okta to invoke the apis.
2. Handler will call the okta introspection endpoint for validating each request since there is no token caching mechanism.
3. API subscription level usage stats and some of the subscription level throttling policies won't work as those are handled by the default authentication handler.


Monday, June 19, 2017

[WSO2 identity server] Extending self registration to add users to custom roles

Self registration feature allows admin to add users to the organization without going through a manual registration process.
This can be easily configured in IS 5.3.0 by following the official documentation [1].

When the registration process completed, the new users are added to internal/selfsignup role. You can go to users and roles in management console and add user to preferred roles later (Figure 1).

Home --> Users and Roles --> List --> Roles , select Assign Users to add users to a specific role.

Screen Shot 2017-06-10 at 2.00.52 AM.png

 Figure 1: Assign users to a role



However, there can be situations where you want to add users directly to a specific role. This functionality can be achieved through a custom event handler.


There are two major steps:

  1. Writing a custom event handler
  2. Registering handler in identity-event.properties

  1. Writing the custom handler



Custom handler should extend the AbstractEventHandler class. The handler should be built as a OSGI bundle.



There are few methods to be overridden.

  1. getName() - this gives a unique name to the handler
  2. handleEvent() - this is where all the event handling logic is implemented
  3. getPriority() - this property sets the priority of the handler. Since this should get executed at the very end pos the add user process, we should provide a larger priority value such as 250.

Following is a sample handler class (In this sample we add users to Internal/Subscriber role).
package org.wso2.carbon.identity.customhandler.handler;

import org.wso2.carbon.context.PrivilegedCarbonContext;
import org.wso2.carbon.identity.base.IdentityRuntimeException;
import org.wso2.carbon.identity.core.handler.InitConfig;
import org.wso2.carbon.identity.core.util.IdentityTenantUtil;
import org.wso2.carbon.identity.event.IdentityEventConstants;
import org.wso2.carbon.identity.event.IdentityEventException;
import org.wso2.carbon.identity.event.event.Event;
import org.wso2.carbon.identity.event.handler.AbstractEventHandler;

import org.wso2.carbon.identity.customhandler.internal.CustomUserSelfRegistrationHandlerDataHolder;
import org.wso2.carbon.identity.recovery.IdentityRecoveryConstants;
import org.wso2.carbon.identity.recovery.IdentityRecoveryServerException;
import org.wso2.carbon.identity.recovery.util.Utils;
import org.wso2.carbon.user.api.UserStoreException;
import org.wso2.carbon.user.api.UserStoreManager;
import org.wso2.carbon.identity.core.bean.context.MessageContext;
import org.wso2.carbon.user.core.service.RealmService;

import java.util.List;

import static java.util.Arrays.asList;

public class CustomUserSelfRegistrationHandler extends AbstractEventHandler {

    //Role Constants
    public static final String SUBSCRIBER_ROLE = "Internal/Subscriber";
    public static final String SELF_SIGNUP_ROLE = "Internal/selfsignup";


    @Override public String getName() {
        return "customUserSelfRegistration";
    }

    public void handleEvent(Event event) throws IdentityEventException {

        String tenantDomain = (String) event.getEventProperties()
                .get(IdentityEventConstants.EventProperty.TENANT_DOMAIN);
        String userName = (String) event.getEventProperties().get(IdentityEventConstants.EventProperty.USER_NAME);

        //The handler should be called ss a post add user event.
        if (IdentityEventConstants.Event.POST_ADD_USER.equals(event.getEventName())) {
            try {
                addNewRole(tenantDomain, userName);
            } catch (IdentityRecoveryServerException e) {
                throw new IdentityEventException("Error while adding custom roles to the user", e);
            }
        }
    }

    private void addNewRole(String tenantDomain, String userName)
            throws org.wso2.carbon.identity.recovery.IdentityRecoveryServerException {
        try {
            //Realm service is used for user management tasks
            RealmService realmService = CustomUserSelfRegistrationHandlerDataHolder.getInstance().getRealmService();
            UserStoreManager userStoreManager;
            try {
                userStoreManager = realmService.getTenantUserRealm(IdentityTenantUtil.getTenantId(tenantDomain))
                        .getUserStoreManager();
            } catch (UserStoreException e) {
                throw Utils
                        .handleServerException(IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_UNEXPECTED, userName,
                                e);
            }
            //Start a tenant flow
            PrivilegedCarbonContext.startTenantFlow();
            PrivilegedCarbonContext carbonContext = PrivilegedCarbonContext.getThreadLocalCarbonContext();
            carbonContext.setTenantId(IdentityTenantUtil.getTenantId(tenantDomain));
            carbonContext.setTenantDomain(tenantDomain);
            try {
                //Since this handler is called as a post add user event, the user should exists in the userstore
                if (userStoreManager.isExistingUser(userName)) {
                    List<String> roleList = asList(userStoreManager.getRoleListOfUser(userName));
                    //User should have selfSignup role. Checking whether the user is in the new role
                    if (roleList.contains(SELF_SIGNUP_ROLE) && !roleList.contains(SUBSCRIBER_ROLE)) {
                        String[] userRoles = new String[] { SUBSCRIBER_ROLE };
                        userStoreManager.updateRoleListOfUser(userName, null, userRoles);
                    }
                }
            } catch (UserStoreException e) {
                throw Utils
                        .handleServerException(IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_UNEXPECTED, userName,
                                e);
            }
        } finally {
            Utils.clearArbitraryProperties();
            PrivilegedCarbonContext.endTenantFlow();
        }
    }

    @Override
    public void init(InitConfig configuration) throws IdentityRuntimeException {
        super.init(configuration);
    }

    @Override
    public int getPriority(MessageContext messageContext) {
        return 250;
    }

}

The complete sample code can be found at [git repo].

Please note that this is a basic sample and you can even improve it to read roles from a config file, create roles if not exist, etc.

2. Registering handler in identity-event.properties

1. Open the identity-event.properties file located at <IS_HOME>/repository/conf/identity/ directory.
2. Add your handler module as a new module as below:


module.name.10=suspension.notification
suspension.notification.subscription.1=POST_AUTHENTICATION
module.name.11=customUserSelfRegistration
customUserSelfRegistration.subscription.1=POST_ADD_USER
suspension.notification.enable=false

The module name is the name you provide in the getName() method of the handler.
3. Save the file and restart the server.

Finally, go to the end user dashboad and add a user to test the functionality. (https://localhost:9443/dashboard

[1] https://docs.wso2.com/display/IS530/Self+Sign+Up+and+Account+Confirmation