Writing a Custom Local Authenticator for WSO2 Identity Server 🖥️

Photo by Erik Mclean on Unsplash

Writing a Custom Local Authenticator for WSO2 Identity Server 🖥️

The WSO2 Identity Server is mainly comprised of two frameworks, authentication framework and provisioning framework. One of the most important components in the authentication framework is the local authenticators. The local authenticators are used to authenticate users based on various user claims. The most common local authenticator that is being used in the WSO2 Identity Server is the basic authenticator with username and password. However, developers can create their custom local authenticators for specific use cases.

In this article, we will be looking at how to create a custom local authenticator for WSO2 Identity Server. As of writing this article, WSO2 Identity Server 7.0 is the latest release of WSO2 Identity Server. However, in this article, I will be using WSO2 Identity Server 6.1. Furthermore, I will be using IntelliJ IDEA as my IDE, but feel free to use any IDE of your choice if you are following my steps and want to get hands-on experience.

In this article, we will be creating a local authenticator that takes user’s telephone number and password as user credentials.

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

Open IntelliJ IDEA and create a new project with the following configurations.

Since we are creating the custom local authenticator as an OSGi service, we don’t need the Main.java file. Therefore, we can delete that file from our project.

Next, we need to update the pom.xml file with the necessary dependencies. To do that, you can replace the contents of your pom.xml file with the below pom.xml configurations. However, make sure to update the artifactId and groupId to match the initial artifactId and groupId of your project.

<?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.local.authenticator</groupId>
    <artifactId>org.wso2.custom.local.authenticator</artifactId>
    <version>1.0-SNAPSHOT</version>
    <name>Custom Local Authenticator</name>
    <packaging>bundle</packaging>

    <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <!-- Maven Artifact Versions -->
        <maven.compiler.plugin.version>2.0</maven.compiler.plugin.version>
        <maven.bundle.plugin.version>3.2.0</maven.bundle.plugin.version>
        <!-- Apache Versions -->
        <commons.logging.version>1.2</commons.logging.version>
        <!-- OSGi -->
        <equinox.osgi.services.version>3.5.100.v20160504-1419</equinox.osgi.services.version>
        <osgi.framework.imp.pkg.version.range>[1.7.0, 2.0.0)</osgi.framework.imp.pkg.version.range>
        <osgi.service.component.imp.pkg.version.range>[1.2.0, 2.0.0)</osgi.service.component.imp.pkg.version.range>
        <commons-logging.osgi.version.range>[1.2,2.0)</commons-logging.osgi.version.range>
        <!-- WSO2 -->
        <carbon.kernel.version>4.9.0</carbon.kernel.version>
        <carbon.kernel.package.import.version.range>[4.6.0, 5.0.0)</carbon.kernel.package.import.version.range>
        <carbon.identity.framework.version>5.25.90</carbon.identity.framework.version>
        <carbon.identity.framework.package.import.version.range>[5.25.0, 6.0.0)</carbon.identity.framework.package.import.version.range>
        <carbon.user.api.imp.pkg.version.range>[1.0.1, 2.0.0)</carbon.user.api.imp.pkg.version.range>
        <axiom.imp.pkg.version>[1.2.11, 1.3.0)</axiom.imp.pkg.version>
        <commons-lang.wso2.version>2.6.0.wso2v1</commons-lang.wso2.version>
        <commons-lang.wso2.osgi.version.range>[2.6.0,3.0.0)</commons-lang.wso2.osgi.version.range>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.wso2.carbon</groupId>
            <artifactId>org.wso2.carbon.utils</artifactId>
            <version>${carbon.kernel.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>
        <dependency>
            <groupId>org.wso2.eclipse.osgi</groupId>
            <artifactId>org.eclipse.osgi.services</artifactId>
            <version>${equinox.osgi.services.version}</version>
        </dependency>
        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>${commons.logging.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>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>${maven.compiler.plugin.version}</version>
                <configuration>
                    <encoding>${project.build.sourceEncoding}</encoding>
                    <source>${maven.compiler.source}</source>
                    <target>${maven.compiler.target}</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.felix</groupId>
                <artifactId>maven-bundle-plugin</artifactId>
                <version>${maven.bundle.plugin.version}</version>
                <extensions>true</extensions>
                <configuration>
                    <instructions>
                        <Bundle-SymbolicName>${project.artifactId}</Bundle-SymbolicName>
                        <Bundle-Name>${project.artifactId}</Bundle-Name>
                        <Private-Package>
                            org.wso2.custom.local.authenticator.internal.*
                        </Private-Package>
                        <Export-Package>
                            !org.wso2.custom.local.authenticator.internal,
                            org.wso2.custom.local.authenticator.*
                        </Export-Package>
                        <Import-Package>
                            org.osgi.framework.*;version="${osgi.framework.imp.pkg.version.range}",
                            org.osgi.service.component.*;version="${osgi.service.component.imp.pkg.version.range}",
                            org.apache.commons.logging.*; version="${commons-logging.osgi.version.range}",
                            org.wso2.carbon.user.core.*; version="${carbon.kernel.package.import.version.range}",
                            org.wso2.carbon.utils.*; version="${carbon.kernel.package.import.version.range}",
                            *;resolution:=optional
                        </Import-Package>
                        <DynamicImport-Package>*</DynamicImport-Package>
                    </instructions>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

In the pom.xml file, you can observe the following XML snippet.

<plugin>
  <groupId>org.apache.felix</groupId>
  <artifactId>maven-bundle-plugin</artifactId>
  <version>${maven.bundle.plugin.version}</version>
  <extensions>true</extensions>
  <configuration>
    <instructions>
      <Bundle-SymbolicName>${project.artifactId}</Bundle-SymbolicName>
      <Bundle-Name>${project.artifactId}</Bundle-Name>
      <Private-Package> 
        org.wso2.custom.local.authenticator.internal.*
      </Private-Package>
      <Export-Package> 
        !org.wso2.custom.local.authenticator.internal, 
        org.wso2.custom.local.authenticator.*
      </Export-Package>
      <Import-Package> 
        org.osgi.framework.*;version="${osgi.framework.imp.pkg.version.range}", 
        org.osgi.service.component.*;version="${osgi.service.component.imp.pkg.version.range}", 
        org.apache.commons.logging.*; version="${commons-logging.osgi.version.range}", 
        org.wso2.carbon.user.core.*; version="${carbon.kernel.package.import.version.range}", 
        org.wso2.carbon.utils.*; version="${carbon.kernel.package.import.version.range}", *;resolution:=optional 
      </Import-Package>
      <DynamicImport-Package>*</DynamicImport-Package>
    </instructions>
  </configuration>
</plugin>

Since I have already explained why we need this configuration in a previous article, I am not going to repeat myself here. But if you want to check that article you can check it from here.

Furthermore, if you want to know how to write OSGi services, you can check my article on OSGi services from here.

Step 2: Write Custom Local Authenticator

To create a custom local authenticator we need to extend our component with AbstractApplicationAuthenticator and implement the interface LocalApplicationAuthenticator Afterwards, you need to override the following methods.

  • canHandle()

  • initiateAuthenticationRequest()

  • processAuthenticationResponse()

  • getContextIdentifier()

  • getName()

  • getFriendlyName()

Furthermore, to use the WSO2 Realm service, you need to make the OSGi service component as well. To create the OSGi service component, create a new package named internal The code snippets of the custom local authenticator and OSGi service component are given below.

package org.wso2.custom.local.authenticator;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.identity.application.authentication.framework.AbstractApplicationAuthenticator;
import org.wso2.carbon.identity.application.authentication.framework.LocalApplicationAuthenticator;
import org.wso2.carbon.identity.application.authentication.framework.config.ConfigurationFacade;
import org.wso2.carbon.identity.application.authentication.framework.context.AuthenticationContext;
import org.wso2.carbon.identity.application.authentication.framework.exception.AuthenticationFailedException;
import org.wso2.carbon.identity.application.authentication.framework.exception.InvalidCredentialsException;
import org.wso2.carbon.identity.application.authentication.framework.model.AuthenticatedUser;
import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkUtils;
import org.wso2.carbon.identity.application.common.model.User;
import org.wso2.carbon.identity.base.IdentityRuntimeException;
import org.wso2.carbon.identity.core.util.IdentityTenantUtil;
import org.wso2.carbon.user.api.UserRealm;
import org.wso2.carbon.user.core.UniqueIDUserStoreManager;
import org.wso2.carbon.user.core.UserCoreConstants;
import org.wso2.carbon.user.core.common.AuthenticationResult;
import org.wso2.custom.local.authenticator.internal.SampleLocalAuthenticatorServiceComponent;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Optional;

/**
 * This is the sample local authenticator which will be used to authenticate the user based on the registered mobile
 * phone number.
 */
public class SampleLocalAuthenticator extends AbstractApplicationAuthenticator implements
        LocalApplicationAuthenticator {

    private static final Log log = LogFactory.getLog(SampleLocalAuthenticator.class);
    private static final String MOBILE_CLAIM_URL = "http://wso2.org/claims/telephone";
    private static final String USERNAME = "username";
    private static final String PASSWORD = "password";

    @Override
    public boolean canHandle(HttpServletRequest httpServletRequest) {

        String userName = httpServletRequest.getParameter(USERNAME);
        String password = httpServletRequest.getParameter(PASSWORD);
        return userName != null && password != null;
    }

    @Override
    protected void initiateAuthenticationRequest(HttpServletRequest request,
                                                 HttpServletResponse response,
                                                 AuthenticationContext context)
            throws AuthenticationFailedException {

        String loginPage = ConfigurationFacade.getInstance().getAuthenticationEndpointURL();
        // This is the default WSO2 IS login page. If you can create your custom login page you can use that instead.
        String queryParams =
                FrameworkUtils.getQueryStringWithFrameworkContextId(context.getQueryParams(),
                        context.getCallerSessionKey(),
                        context.getContextIdentifier());

        try {
            String retryParam = "";

            if (context.isRetrying()) {
                retryParam = "&authFailure=true&authFailureMsg=login.fail.message";
            }

            response.sendRedirect(response.encodeRedirectURL(loginPage + ("?" + queryParams)) +
                    "&authenticators=BasicAuthenticator:" + "LOCAL" + retryParam);
        } catch (IOException e) {
            throw new AuthenticationFailedException(e.getMessage(), e);
        }
    }

    @Override
    protected void processAuthenticationResponse(HttpServletRequest httpServletRequest, HttpServletResponse
            httpServletResponse, AuthenticationContext authenticationContext) throws AuthenticationFailedException {


        String username = httpServletRequest.getParameter(USERNAME);
        String password = httpServletRequest.getParameter(PASSWORD);

        Optional<org.wso2.carbon.user.core.common.User> user = Optional.empty();

        boolean isAuthenticated = false;

        // Check the authentication
        try {
            int tenantId = IdentityTenantUtil.getTenantIdOfUser(username);
            UserRealm userRealm = SampleLocalAuthenticatorServiceComponent.getRealmService()
                    .getTenantUserRealm(tenantId);
            if (userRealm != null) {
                UniqueIDUserStoreManager userStoreManager = (UniqueIDUserStoreManager) userRealm.getUserStoreManager();

                // This custom local authenticator is using the telephone number as the username.
                // Therefore the login identifier claim is http://wso2.org/claims/telephone.
                AuthenticationResult authenticationResult = userStoreManager.
                        authenticateWithID(MOBILE_CLAIM_URL, username, password, UserCoreConstants.DEFAULT_PROFILE);
                if (AuthenticationResult.AuthenticationStatus.SUCCESS == authenticationResult
                        .getAuthenticationStatus()) {
                    user = authenticationResult.getAuthenticatedUser();
                    isAuthenticated = true;
                }
            } else {
                throw new AuthenticationFailedException("Cannot find the user realm for the given tenant: " + tenantId,
                        User.getUserFromUserName(username));
            }
        } catch (IdentityRuntimeException e) {
            if (log.isDebugEnabled()) {
                log.debug("BasicAuthentication failed while trying to get the tenant ID of the user " + username, e);
            }
            throw new AuthenticationFailedException(e.getMessage(), e);
        } catch (org.wso2.carbon.user.api.UserStoreException e) {
            if (log.isDebugEnabled()) {
                log.debug("BasicAuthentication failed while trying to authenticate the user " + username, e);
            }
            throw new AuthenticationFailedException(e.getMessage(), e);
        }

        // If the authentication fails, throws the invalid client credential exception.
        if (!isAuthenticated) {
            if (log.isDebugEnabled()) {
                log.debug("User authentication failed due to invalid credentials");
            }
            throw new InvalidCredentialsException("User authentication failed due to invalid credentials",
                    User.getUserFromUserName(username));
        }

        // When the user is successfully authenticated, add the user to the authentication context to be used later in
        // the process.
        if (user != null) {
            username = user.get().getUsername();
        }
        authenticationContext.setSubject(AuthenticatedUser.createLocalAuthenticatedUserFromSubjectIdentifier(username));
    }

    @Override
    public String getContextIdentifier(HttpServletRequest httpServletRequest) {

        return httpServletRequest.getParameter("sessionDataKey");
    }

    @Override
    public String getName() {

        return "SampleLocalAuthenticator";
    }

    @Override
    public String getFriendlyName() {

        return "sample-local-authenticator";
    }
}
package org.wso2.custom.local.authenticator.internal;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
import org.wso2.custom.local.authenticator.SampleLocalAuthenticator;
import org.wso2.carbon.identity.application.authentication.framework.ApplicationAuthenticator;
import org.wso2.carbon.user.core.service.RealmService;

@Component(
        name = "org.wso2.carbon.custom.local.authenticator",
        immediate = true)
public class SampleLocalAuthenticatorServiceComponent {

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

    private static RealmService realmService;

    @Activate
    protected void activate(ComponentContext ctxt) {

        try {
            SampleLocalAuthenticator sampleLocalAuthenticator = new SampleLocalAuthenticator();
            ctxt.getBundleContext().registerService(ApplicationAuthenticator.class.getName(),
                    sampleLocalAuthenticator, null);
            if (log.isDebugEnabled()) {
                log.info("SampleLocalAuthenticator bundle is activated");
            }
        } catch (Throwable e) {
            log.error("SampleLocalAuthenticator bundle activation Failed", e);
        }
    }

    @Deactivate
    protected void deactivate(ComponentContext ctxt) {

        if (log.isDebugEnabled()) {
            log.info("SampleLocalAuthenticator bundle is deactivated");
        }
    }

    public static RealmService getRealmService() {

        return realmService;
    }

    @Reference(name = "realm.service",
            service = org.wso2.carbon.user.core.service.RealmService.class,
            cardinality = ReferenceCardinality.MANDATORY,
            policy = ReferencePolicy.DYNAMIC,
            unbind = "unsetRealmService")
    protected void setRealmService(RealmService realmService) {

        log.debug("Setting the Realm Service");
        SampleLocalAuthenticatorServiceComponent.realmService = realmService;
    }

    protected void unsetRealmService(RealmService realmService) {

        log.debug("UnSetting the Realm Service");
        SampleLocalAuthenticatorServiceComponent.realmService = null;
    }
}

Step 3: Build and test the custom local authenticator

Now, we can build our custom local authenticator with the following command.

mvn clean install -DskipTests

After that, you can find the org.wso2.custom.local.authenticator-1.0-SNAPSHOT.jar file in the target directory of the project.

Download a fresh pack of WSO2 Identity Server 6.1 from here and go to <IS_HOME>/repository/components/dropins and paste the org.wso2.custom.local.authenticator-1.0-SNAPSHOT.jar file there.

Now we can start the WSO2 Identity Server by using the following commands.

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

  • Windows → wso2server.bat -DosgiConsole

With -DosgiConsole flag, you are activating the inbuilt OSGi console of the WSO2 Identity Server. With ss <component_name>command you can check whether your custom component is in theACTIVE state or not. You can check the other important OSGi Console commands from this article.

If the component is not in the ACTIVE state, you can use b <component_id> and diag <component_id> to resolve issues with the component.

Next, create a sample service provider named playground_2 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": ["authorization_code","password"], "redirect_uris": ["http://localhost:8080/playground2/oauth2client"],"ext_param_client_id":"provided_client_id0001","ext_param_client_secret":"provided_client_secret0001" }' "https://localhost:9443/api/identity/oauth2/dcr/v1.1/register"

If you want to check more about the DCR API of the WSO2 Identity Server, you can read my article on DCR from here.

Now, type https://localhost:9443/carbon and go to the WSO2 Management Console. Use the default username admin and default password admin to log in. Next, go to, Service Providers → List → playground_2 → Edit.

Select the sample-local-authenticator from the Local & Outbound Authentication Configuration and click Update.

Next, update the admin user’s user profile by going to, Users and Roles → List → Users → Select admin user → User Profile → default. Then update the telephone number and other required claims.

Finally, type the following URL in the browser and you will notice that after providing your telephone number and password you can log in.

https://localhost:9443/oauth2/authorize?response_type=code&client_id=provided_client_id0001&redirect_uri=http://localhost:8080/playground2/oauth2client&scope=openid

So this is it! This is how you can create a custom local authenticator for the WSO2 Identity Server. You can find the implemented code from the below repository link.

https://github.com/nipunaupeksha/wso2-custom-components-medium/tree/main/org.wso2.custom.local.authenticator