logo

Dynamic OTP page for WSO2 IS based on user claims

One Time Password (OTP) is something that we are heavily using these days to enhance the security of the applications. If you are using the WSO2 Identity server as a Single Sign-On (SSO) server, you have the functionality to send OTP and validate at the time of users logging in to the system. Also, you have the option to enable multiple channels to send the OTP. Mainly we can configure SMS and Email OTP and at the time of login in, users can decide which medium they want to use.

Now, what is the issue we are going to solve in this article? For example, let’s say in your user base phone number or email is not mandatory. Let’s say email is not mandatory in this case. Then if a user who doesn’t have an email address selects the Email OTP option, there will be an error. What we are going to do is dynamically populate the OTP options page so that users who don’t have an email address will not see the Email OTP option but will directly go to SMS OTP page.

We are going to write a custom authenticator (an extension point provided by WSO2 IS) to check the user information and dynamically populate the required data.

Prerequisites

  • Java development IDE (Eclipse, IntelliJ IDEA, etc.)
  • WSO2 Identity Server 5.3.0
  • Apache Tomcat 8.x

Developing the custom component

  • Create a new Maven project
  • Update the packaging to bundle so that the output of this project will be a OSGI Bundle.
<project ...>
    ...
    <packaging>bundle</packaging>
    ...
</project>
  • Update the dependancies as follows.
<dependencies>
    <dependency>
        <groupId>org.wso2.carbon</groupId>
        <artifactId>org.wso2.carbon.logging</artifactId>
        <version>4.4.11</version>
    </dependency>
    <dependency>
        <groupId>org.wso2.carbon.identity.framework</groupId>
        <artifactId>org.wso2.carbon.identity.application.authentication.framework</artifactId>
        <version>5.7.5</version>
    </dependency>
    <dependency>
        <groupId>org.wso2.carbon.identity.framework</groupId>
        <artifactId>org.wso2.carbon.identity.application.common</artifactId>
        <version>5.7.7</version>
    </dependency>
    <dependency>
        <groupId>com.googlecode.json-simple.wso2</groupId>
        <artifactId>json-simple</artifactId>
        <version>1.1.wso2v1</version>
    </dependency>
    <dependency>
        <groupId>org.wso2.carbon.identity.framework</groupId>
        <artifactId>org.wso2.carbon.identity.core</artifactId>
        <version>5.7.5</version>
    </dependency>
    <dependency>
        <groupId>org.wso2.carbon.identity.application.auth.basic</groupId>
        <artifactId>org.wso2.carbon.identity.application.authenticator.basicauth</artifactId>
        <version>5.3.0</version>
    </dependency>
    <dependency>
        <groupId>org.wso2.carbon.extension.identity.authenticator.outbound.smsotp</groupId>
        <artifactId>org.wso2.carbon.extension.identity.authenticator.smsotp.connector</artifactId>
        <version>2.0.12</version>
    </dependency>
    <dependency>
        <groupId>org.wso2.carbon.extension.identity.authenticator.outbound.emailotp</groupId>
        <artifactId>org.wso2.carbon.extension.identity.authenticator.emailotp.connector</artifactId>
        <version>2.0.10</version>
    </dependency>
</dependencies>
  • Create a Java package called com.wso2.poc. (Instead, you can use your own package name)
  • Create a Class called DynamicOTPAuthenticator in the newly created package.
  • Update the code as follows.
package com.wso2.poc.authenticators;

import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.extension.identity.helper.FederatedAuthenticatorUtil;
import org.wso2.carbon.identity.application.authentication.framework.AbstractApplicationAuthenticator;
import org.wso2.carbon.identity.application.authentication.framework.AuthenticatorFlowStatus;
import org.wso2.carbon.identity.application.authentication.framework.LocalApplicationAuthenticator;
import org.wso2.carbon.identity.application.authentication.framework.config.model.AuthenticatorConfig;
import org.wso2.carbon.identity.application.authentication.framework.config.model.SequenceConfig;
import org.wso2.carbon.identity.application.authentication.framework.config.model.StepConfig;
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.LogoutFailedException;
import org.wso2.carbon.identity.core.util.IdentityTenantUtil;
import org.wso2.carbon.user.api.UserRealm;
import org.wso2.carbon.user.api.UserStoreException;
import org.wso2.carbon.user.core.service.RealmService;
import org.wso2.carbon.utils.multitenancy.MultitenantUtils;

public class DynamicOTPAuthenticator extends AbstractApplicationAuthenticator implements LocalApplicationAuthenticator {

  private static final String AUTHENTICATOR_NAME = "DynamicOTPAuthenticator";
  private static final String AUTHENTICATOR_FRIENDLY_NAME = "Dynamic OTP Authenticator";
  private static final String DISABLE_SMS_OTP_CLAIM = "http://wso2.org/claims/identity/smsotp_disabled";
  private static final String DISABLE_EMAIL_OTP_CLAIM = "http://wso2.org/claims/identity/emailotp_disabled";
  private static final String USERNAME = "username";
  private static final String EMAILOTP = "EmailOTP";
  private static final String SMSOTP = "SMSOTP";

  private static final long serialVersionUID = -4329444011925985689L;
  private static Log log = LogFactory.getLog(DynamicOTPAuthenticator.class);

  public boolean canHandle(HttpServletRequest request) {
    return false;
  }

  public String getContextIdentifier(HttpServletRequest request) {
    return request.getParameter("sessionDataKey");
  }

  public String getFriendlyName() {
    return DynamicOTPAuthenticator.AUTHENTICATOR_FRIENDLY_NAME;
  }

  public String getName() {
    return DynamicOTPAuthenticator.AUTHENTICATOR_NAME;
  }

  @Override
  protected void initiateAuthenticationRequest(HttpServletRequest request, HttpServletResponse response,
      AuthenticationContext context) throws AuthenticationFailedException {
    super.initiateAuthenticationRequest(request, response, context);
    FederatedAuthenticatorUtil.setUsernameFromFirstStep(context);
  }

  @Override
  public AuthenticatorFlowStatus process(HttpServletRequest request, HttpServletResponse response,
      AuthenticationContext context) throws AuthenticationFailedException, LogoutFailedException {

    // no initiation since this is transparent to user
    SequenceConfig sequenceConfig = context.getSequenceConfig();
    // find the user name
    FederatedAuthenticatorUtil.setUsernameFromFirstStep(context);
    String username = String.valueOf(context.getProperty(USERNAME));
  
    if (getClaimValueForUsername(username, DynamicOTPAuthenticator.DISABLE_SMS_OTP_CLAIM)) {
      log.debug("Removing SMS OTP for user " + username);
      removeOTPOption(sequenceConfig, SMSOTP);
    }
    if (getClaimValueForUsername(username, DynamicOTPAuthenticator.DISABLE_EMAIL_OTP_CLAIM)) {
      log.debug("Removing EMAIL OTP for user " + username);
      removeOTPOption(sequenceConfig, EMAILOTP);
    }

    return AuthenticatorFlowStatus.SUCCESS_COMPLETED;
  }

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

  }

  private boolean getClaimValueForUsername(String username, String claimURL) throws AuthenticationFailedException {
    UserRealm userRealm;
    try {
      String tenantDomain = MultitenantUtils.getTenantDomain(username);
      int tenantId = IdentityTenantUtil.getTenantId(tenantDomain);
      RealmService realmService = IdentityTenantUtil.getRealmService();
      userRealm = realmService.getTenantUserRealm(tenantId);
      username = MultitenantUtils.getTenantAwareUsername(String.valueOf(username));
      if (userRealm != null) {
        Map<String, String> claimValues = userRealm.getUserStoreManager().getUserClaimValues(username,
            new String[] { claimURL }, null);
        String isEmailOTPEnabledByUser = claimValues.get(claimURL);
        return Boolean.parseBoolean(isEmailOTPEnabledByUser);
      } else {
        throw new AuthenticationFailedException(
            "Cannot find the user realm for the given tenant domain : " + tenantDomain);
      }
    } catch (UserStoreException e) {
      log.error("Failed while trying to access userRealm of the user : " + username, e);
    }
    return false;
  }

  private void removeOTPOption(SequenceConfig sequenceConfig, String optName) {
    if (sequenceConfig.getStepMap().size() > 2) {
      StepConfig stepConfig = sequenceConfig.getStepMap().get(3);
      AuthenticatorConfig authConfig = null;
      if (stepConfig.getAuthenticatorList().size() > 1) {
        for (AuthenticatorConfig authConf : stepConfig.getAuthenticatorList()) {
          if (authConf.getIdps().get(optName) != null) {
            authConfig = authConf;
            break;
          }
        }
        stepConfig.getAuthenticatorList().remove(authConfig);
        stepConfig.setMultiOption(stepConfig.getAuthenticatorList().size() > 1);
      }
    }
  }

}
  • Create another package called com.wso2.poc.internal (you can use your package name + .internal for this)
  • Create a class called DynamicOTPAuthenticatorServiceComponent and update the code as follows. This class is for the OSGi component.
package com.wso2.poc.authenticators.internal;

import java.util.Hashtable;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.osgi.service.component.ComponentContext;
import org.wso2.carbon.identity.application.authentication.framework.ApplicationAuthenticator;
import org.wso2.carbon.user.core.service.RealmService;

import com.wso2.poc.authenticators.DynamicOTPAuthenticator;

/**
 * @scr.component name="com.wso2.poc.authenticators.dynamicOTPAuthenticator.component"
 *                immediate="true"
 */
public class DynamicOTPAuthenticatorServiceComponent {
  private static Log log = LogFactory.getLog(DynamicOTPAuthenticatorServiceComponent.class);

  private static RealmService realmService;

  protected void activate(ComponentContext ctxt) {
    try {
      Hashtable<String, String> props = new Hashtable<String, String>();

      DynamicOTPAuthenticator authenticator = new DynamicOTPAuthenticator();
      ctxt.getBundleContext().registerService(ApplicationAuthenticator.class.getName(), authenticator, props);

      if (log.isDebugEnabled()) {
        log.debug("Dynamic OTP Authenticator is activated");
      }
    } catch (Throwable e) {
      log.fatal("Dynamic OTP Authenticator error in bundle activation", e);
    }
  }

  protected void deactivate(ComponentContext ctxt) {
    if (log.isDebugEnabled()) {
      log.debug("Dynamic OTP Authenticator is deactivated");
    }
  }
}
  • Open the pom.xml and add the build section as below. Make sure to change the package names if it is different. This section will make the jar file an OSGi bundle.
<project ...>
...
<build>
  <pluginManagement>
    <plugins>
      <plugin>
        <groupId>org.apache.felix</groupId>
        <artifactId>maven-bundle-plugin</artifactId>
        <version>2.3.5</version>
        <extensions>true</extensions>
      </plugin>
    </plugins>
  </pluginManagement>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-compiler-plugin</artifactId>
      <version>2.0</version>
      <configuration>
        <source>1.5</source>
        <target>1.5</target>
      </configuration>
    </plugin>
    <plugin>
      <groupId>org.apache.felix</groupId>
      <artifactId>maven-bundle-plugin</artifactId>
      <extensions>true</extensions>
      <configuration>
        <instructions>
          <Bundle-SymbolicName>${project.artifactId}</Bundle-SymbolicName>
          <Bundle-Name>${project.artifactId}</Bundle-Name>
          <Private-Package>com.wso2.poc.authenticators.internal</Private-Package>
          <Import-Package>
            org.apache.commons.logging.*; version="1.0.4",
            org.osgi.framework,
            org.wso2.carbon.identity.application.authentication.framework.*,
            javax.servlet,
            javax.servlet.http,
            *;resolution:=optional
          </Import-Package>
          <Export-Package>
            !com.wso2.poc.authenticators.internal,
            com.wso2.poc.authenticators.*
          </Export-Package>
          <DynamicImport-Package>*</DynamicImport-Package>
        </instructions>
      </configuration>
    </plugin>
    <plugin>
          <groupId>org.apache.felix</groupId>
          <artifactId>maven-scr-plugin</artifactId>
          <version>1.7.2</version>
          <executions>
            <execution>
              <id>generate-scr-scrdescriptor</id>
              <goals>
                <goal>scr</goal>
              </goals>
            </execution>
          </executions>
        </plugin>
  </plugins>
</build>
...
</project>
  • Build the project and once it is completed you will have a jar file under the target directory.

Configuring the WSO2 IS

  • Configure the WSO2 IS and Tomcat by following the steps mentioned in Setup the WSO2 OAuth2.0 Playground sample section in [1]. We are going to use the playground app to test our authenticator.
  • Configure the SMS OTP by following the steps under Deploying SMS OTP artifacts section in [2].
  • Configure the Email OTP by following the steps under Enabling email configuration on WSO2 IS and Configure the Email OTP provider sections in [3].
  • Copy the jar file from our project (the OSGi bundle) to <WSO2_IS_HOME>/repository/components/dropins/
  • Start the WSO2 IS by executing the
  • Now configure the Playground app by following the steps given under Register the Playground application in WSO2 Identity Server section in [1].
  • Once you complete the previous step, again go to the Service Providers section and select the Playground app and expand the Local & Outbound Authentication Configuration section.

  • Click the Advanced Configuration option then you will be taken to Authenticator configuration page.
  • Configure the options as below.

  • Once the configurations are done click update and save the changes.

Testing the configuration.

  • Create three users as follows
    • user1 – with a phone number but no email address
    • user2 – no phone number but with an email address
    • user3 – with a phone number and email address
  • Once you try to login with each user, you should be able to see the different behavior for each user for OTP step.

References

[1] https://docs.wso2.com/display/IS530/Basic+Client+Profile+with+Playground

[2] https://docs.wso2.com/display/IS530/Configuring+SMSOTP

[3] https://docs.wso2.com/display/IS530/Configuring+Email+OTP

Comments are closed.