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.
Subscribe to my newsletter
Read articles from Nipuna Upeksha directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by