Writing a Custom Grant Type for WSO2 Identity Server 🔖

Nipuna UpekshaNipuna Upeksha
6 min read

When we talk about OAuth2 and OpenID Connect(OIDC), we cannot talk about how those two operate by providing access to the client applications without talking about grant types.

In simple terms, grant type is a mechanism to provide access to protected sources. And with WSO2 Identity Server you can use multiple grant types like authorization code, implicit, and password grant types. You can refer to my previous articles to learn more about OAuth2, OIDC, and grant types.

Apart from the aforementioned grant types, the WSO2 Identity Server allows users to implement custom grant types to fulfill specific use cases.

In this article, we will be looking at how to implement a mobile number-based grant type for WSO2 Identity Server.

Although the latest release of WSO2 Identity Server is version 7.0 as of writing this article, I will be using WSO2 Identity Server 6.1 for our implementation. Furthermore, I will be using IntelliJ IDEA as the IDE, but you are free to use any IDE of your choice if you are following my steps to implement a custom grant type for WSO2 Identity Server.

Step 1: Create a new Maven project and add the necessary dependencies

Create a new Maven project using the below configurations for groupId and artifactId.

We don’t need the Main.java file for our implementation. Therefore, you can delete that file. For the contents of the pom.xml file, you can replace it with the below-given pom.xml file. But make sure to update the groupId and artifactId properly.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.wso2.custom.grant.type</groupId>
    <artifactId>org.wso2.custom.grant.type</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <oltu.version>1.0.2</oltu.version>
        <carbon.identity.framework.version>5.25.92</carbon.identity.framework.version>
        <carbon.user.core.version>4.9.0</carbon.user.core.version>
        <maven.compiler.plugin.version>2.0</maven.compiler.plugin.version>
        <commons.logging.version>1.2</commons.logging.version>
        <carbon.identity.auth.oauth2>6.11.21</carbon.identity.auth.oauth2>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.apache.oltu.oauth2</groupId>
            <artifactId>org.apache.oltu.oauth2.client</artifactId>
            <version>${oltu.version}</version>
        </dependency>
        <dependency>
            <groupId>org.wso2.carbon.identity.inbound.auth.oauth2</groupId>
            <artifactId>org.wso2.carbon.identity.oauth</artifactId>
            <version>${carbon.identity.auth.oauth2}</version>
        </dependency>
        <!--<dependency>
            <groupId>org.ops4j.pax.logging</groupId>
            <artifactId>pax-logging-api</artifactId>
        </dependency>-->
        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>${commons.logging.version}</version>
        </dependency>
        <dependency>
            <groupId>org.wso2.carbon</groupId>
            <artifactId>org.wso2.carbon.user.core</artifactId>
            <version>${carbon.user.core.version}</version>
        </dependency>
        <dependency>
            <groupId>org.wso2.carbon.identity.framework</groupId>
            <artifactId>org.wso2.carbon.identity.application.authentication.framework</artifactId>
            <version>${carbon.identity.framework.version}</version>
        </dependency>
    </dependencies>

    <repositories>
        <repository>
            <id>wso2-nexus</id>
            <name>WSO2 internal Repository</name>
            <url>https://maven.wso2.org/nexus/content/groups/wso2-public/</url>
            <releases>
                <enabled>true</enabled>
                <updatePolicy>daily</updatePolicy>
                <checksumPolicy>ignore</checksumPolicy>
            </releases>
        </repository>
        <repository>
            <id>wso2.releases</id>
            <name>WSO2 internal Repository</name>
            <url>https://maven.wso2.org/nexus/content/repositories/releases/</url>
            <releases>
                <enabled>true</enabled>
                <updatePolicy>daily</updatePolicy>
                <checksumPolicy>ignore</checksumPolicy>
            </releases>
        </repository>
        <repository>
            <id>wso2.snapshots</id>
            <name>WSO2 Snapshot Repository</name>
            <url>https://maven.wso2.org/nexus/content/repositories/snapshots/</url>
            <snapshots>
                <enabled>true</enabled>
                <updatePolicy>daily</updatePolicy>
            </snapshots>
            <releases>
                <enabled>false</enabled>
            </releases>
        </repository>
    </repositories>

    <build>
        <sourceDirectory>src/main/java</sourceDirectory>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>${maven.compiler.plugin.version}</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

Although we can use the configurations to make our grant type an OSGi service, it is not necessary here. That’s the reason you don’t see the OSGi dependencies here like in the previous custom components we have discussed. You can check out the previous custom component implementations from here.

Step 2: Implement the Custom Grant Type

When creating a new OAuth2 grant type for the WSO2 Identity Server, you need to write a handler and a validator for your grant type.

Grant type handler specifies how the validation must be done and how the token should be issued. This can be done in two ways. Either you can implement the AuthorizationGrantHandler interface or you can extend the AbstractAuthorizationGrantHandler class.

package org.wso2.custom.grant.type;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.identity.application.authentication.framework.model.AuthenticatedUser;
import org.wso2.carbon.identity.oauth2.IdentityOAuth2Exception;
import org.wso2.carbon.identity.oauth2.ResponseHeader;
import org.wso2.carbon.identity.oauth2.dto.OAuth2AccessTokenRespDTO;
import org.wso2.carbon.identity.oauth2.model.RequestParameter;
import org.wso2.carbon.identity.oauth2.token.OAuthTokenReqMessageContext;
import org.wso2.carbon.identity.oauth2.token.handlers.grant.AbstractAuthorizationGrantHandler;
import org.wso2.carbon.utils.multitenancy.MultitenantUtils;

import java.util.UUID;

public class MobileGrant extends AbstractAuthorizationGrantHandler  {

    private static Log log = LogFactory.getLog(MobileGrant.class);


    public static final String MOBILE_GRANT_PARAM = "mobileNumber";

    @Override
    public boolean validateGrant(OAuthTokenReqMessageContext oAuthTokenReqMessageContext)  throws IdentityOAuth2Exception {

        log.info("Mobile Grant handler is hit");

        boolean authStatus = false;

        // extract request parameters
        RequestParameter[] parameters = oAuthTokenReqMessageContext.getOauth2AccessTokenReqDTO().getRequestParameters();

        String mobileNumber = null;

        // find out mobile number
        for(RequestParameter parameter : parameters){
            if(MOBILE_GRANT_PARAM.equals(parameter.getKey())){
                if(parameter.getValue() != null && parameter.getValue().length > 0){
                    mobileNumber = parameter.getValue()[0];
                }
            }
        }

        if(mobileNumber != null) {
            //validate mobile number
            authStatus =  isValidMobileNumber(mobileNumber);

            if(authStatus) {
                // if valid set authorized mobile number as grant user
                String tenantAwareUsername = MultitenantUtils.getTenantAwareUsername(mobileNumber);
                /*
                    Please use AuthenticatedUser.createFederateAuthenticatedUserFromSubjectIdentifier() if a federated
                    user is involved with this custom grant.
                 */
                AuthenticatedUser mobileUser = AuthenticatedUser.createLocalAuthenticatedUserFromSubjectIdentifier(
                        tenantAwareUsername);
                // Set the federated IdP name if a federated user is involved with this custom grant.
                // mobileUser.setFederatedIdPName(FrameworkConstants.LOCAL_IDP_NAME);

                oAuthTokenReqMessageContext.setAuthorizedUser(mobileUser);
                oAuthTokenReqMessageContext.setScope(oAuthTokenReqMessageContext.getOauth2AccessTokenReqDTO().getScope());
            } else{
                ResponseHeader responseHeader = new ResponseHeader();
                responseHeader.setKey("SampleHeader-999");
                responseHeader.setValue("Provided Mobile Number is Invalid.");
                oAuthTokenReqMessageContext.addProperty("RESPONSE_HEADERS", new ResponseHeader[]{responseHeader});
            }

        }

        return authStatus;
    }

    @Override
    public OAuth2AccessTokenRespDTO issue(OAuthTokenReqMessageContext tokReqMsgCtx) throws IdentityOAuth2Exception {

        OAuth2AccessTokenRespDTO tokenRespDTO = new OAuth2AccessTokenRespDTO();
        tokenRespDTO.setExpiresIn(tokReqMsgCtx.getAccessTokenIssuedTime() + 10000);
        tokenRespDTO.setAccessToken(UUID.randomUUID().toString());
        tokenRespDTO.setRefreshToken(UUID.randomUUID().toString());
        tokenRespDTO.setTokenType("mobile");
        return tokenRespDTO;
    }

    public boolean authorizeAccessDelegation(OAuthTokenReqMessageContext tokReqMsgCtx)
            throws IdentityOAuth2Exception {

        // if we need to just ignore the end user's extended verification

        return true;

        // if we need to verify with the end user's access delegation by calling callback chain.
        // However, you need to register a callback for this. Default call back just return true.


//        OAuthCallback authzCallback = new OAuthCallback(
//                tokReqMsgCtx.getAuthorizedUser(),
//                tokReqMsgCtx.getOauth2AccessTokenReqDTO().getClientId(),
//                OAuthCallback.OAuthCallbackType.ACCESS_DELEGATION_TOKEN);
//        authzCallback.setRequestedScope(tokReqMsgCtx.getScope());
//        authzCallback.setCarbonGrantType(org.wso2.carbon.identity.oauth.common.GrantType.valueOf(tokReqMsgCtx.
//                                                            getOauth2AccessTokenReqDTO().getGrantType()));
//        callbackManager.handleCallback(authzCallback);
//        tokReqMsgCtx.setValidityPeriod(authzCallback.getValidityPeriod());
//        return authzCallback.isAuthorized();

    }


    public boolean validateScope(OAuthTokenReqMessageContext tokReqMsgCtx)
            throws IdentityOAuth2Exception {


        // if we need to just ignore the scope verification

        return true;

        // if we need to verify with the scope n by calling callback chain.
        // However, you need to register a callback for this. Default call back just return true.
        // you can find more details on writing custom scope validator from here
        // http://xacmlinfo.org/2014/10/24/authorization-for-apis-with-xacml-and-oauth-2-0/

//        OAuthCallback scopeValidationCallback = new OAuthCallback(
//                tokReqMsgCtx.getAuthorizedUser().toString(),
//                tokReqMsgCtx.getOauth2AccessTokenReqDTO().getClientId(),
//                OAuthCallback.OAuthCallbackType.SCOPE_VALIDATION_TOKEN);
//        scopeValidationCallback.setRequestedScope(tokReqMsgCtx.getScope());
//        scopeValidationCallback.setCarbonGrantType(org.wso2.carbon.identity.oauth.common.GrantType.valueOf(tokReqMsgCtx.
//                                                            getOauth2AccessTokenReqDTO().getGrantType()));
//
//        callbackManager.handleCallback(scopeValidationCallback);
//        tokReqMsgCtx.setValidityPeriod(scopeValidationCallback.getValidityPeriod());
//        tokReqMsgCtx.setScope(scopeValidationCallback.getApprovedScope());
//        return scopeValidationCallback.isValidScope();
    }



    /**
     * TODO
     *
     * You need to implement how to validate the mobile number
     *
     * @param mobileNumber
     * @return
     */
    private boolean isValidMobileNumber(String mobileNumber){

        // just demo validation

        if(mobileNumber.startsWith("033")){
            return true;
        }

        return false;
    }

    @Override
    public boolean isOfTypeApplicationUser() throws IdentityOAuth2Exception {
        return true;
    }

}

As you can see in the code, I am returning true for every mobile number starting with 033 to simplify the use case. However, if you want to check the mobile claim properly you can implement that logic in the code.

Grant type validator verifies and validates the token request and checks whether all the required parameters are sent with the request. This can be implemented by extending the AbstractValidator class.

package org.wso2.custom.grant.type;

import org.apache.oltu.oauth2.common.validators.AbstractValidator;

import javax.servlet.http.HttpServletRequest;


public class MobileGrantValidator  extends AbstractValidator<HttpServletRequest> {


    public MobileGrantValidator() {

        // mobile number must be in the request parameter
        requiredParams.add(MobileGrant.MOBILE_GRANT_PARAM);
    }
}

Step 3: Build and Test

To build the custom component, execute the following command in the terminal.

mvn clean install -DskipTests

After that, copy the org.wso2.custom.grant.type-1.0-SNAPSHOT.jar from the target directory and paste it inside the <IS_HOME>/repository/components/lib directory of a fresh IS 6.1 pack downloaded from here.

Also, add the following configuration in the <IS_HOME>/repository/conf/deployment.toml file.

[[oauth.custom_grant_type]]
name="mobile_grant"
grant_handler="org.wso2.custom.grant.type.MobileGrant"
grant_validator="org.wso2.custom.grant.type.MobileGrantValidator"

[oauth.custom_grant_type.properties]
IdTokenAllowed=true

Next, go to the <IS_HOME>/bin directory and run the WSO2 Identity Server.

  • Linux/Mac Users → ./wso2server.sh -DosgiConsole

  • Windows → wso2server.bat -DosgiConsole

Use ss <component_name> command to check whether the custom grant type is in ACTIVE state. This is possible due to the fact WSO2 Identity Server converts the artifacts in lib directory to OSGi services during runtime and places them in the dropins directory.

If it is not in ACTIVE state use b <component_id> and diag <component_id> command to resolve the issues.

Next, create a new service provider using the DCR API of WSO2 IS.

curl -k -X POST -H "Authorization: Basic YWRtaW46YWRtaW4=" -H "Content-Type: application/json" -d '{  "client_name": "playground_2", "grant_types": ["password","mobile_grant"], "redirect_uris":["http://localhost:8080/playground2"], "ext_param_client_id":"provided_client_id0001","ext_param_client_secret":"provided_client_secret0001" }' "https://localhost:9443/api/identity/oauth2/dcr/v1.1/register"

Now, if you go to the management console with https://localhost:9443/carbon you can see the created playground_2 service provider under the Service Providers section (use the default username admin and password admin to login).

Go to the Inbound Authentication Configurations → OAuth/OpenID Connect Configurations to see whether you have selected the mobile_grant grant type.

Since we have the activated themobile_grant grant type with playground_2 application, we can get an access token using that. To get an access token use the following cURL command.

curl --user provided_client_id0001:provided_client_secret0001 -k -d "grant_type=mobile_grant&mobileNumber=0333444" -H "Content-Type: application/x-www-form-urlencoded" https://localhost:9443/oauth2/token

So this is it! This is how you can write a custom grant type for the WSO2 Identity Server. You can find more information on WSO2 grant types from the official WSO2 documentation.

You can find the implemented Custom Grant Type from the below link.

https://github.com/nipunaupeksha/wso2-custom-components-medium/tree/main/org.wso2.custom.grant.type

0
Subscribe to my newsletter

Read articles from Nipuna Upeksha directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Nipuna Upeksha
Nipuna Upeksha