Article summary

Summary

Discusses how to create an Adobe Experience Manager OSGi bundle that uses Sling authentication APIs to create a bundle that supports two factor authentication.  

A special thank you to SUMANTA PAKIRA, a member of the AEM community for contributing AEM code that is used in this article. 

This article uses an Adobe Maven Archetype project to build an OSGi bundle. If you are not familiar with an Adobe Maven Archetype project, it is recommended that you read the following article: Creating your first AEM Service using an Adobe Maven Archetype project.

This HELPX article is based on community content that community members have implemented in various projects. If you see content that needs to be corrected or other updates, please email Scott Macdonald and Kautuk Sahni at scottm@adobe.com and ksahni@adobe.com. 

Digital Marketing Solution(s) Adobe Experience Manager (Adobe CQ)
Audience
Developer (intermediate)
Required Skills
Java, Sling, HTML
Tested On Adobe Experience Manager 6.1

Introduction

You can create an OSGi bundle for Adobe Experience Manager (AEM) that is responsible for supporting two factor authentication. That is, you can configure AEM to use a one-time password (OTP). An OTP is an automatically generated numeric or alphanumeric string of characters that authenticates the user for a single transaction or session. 

The benefit of using an OTP is it's more secure than a static password. An OTP token is typically generated by a mobile application that displays a number. The number changes every 30 or 60 seconds, depending on how the token is configured. 

 

When the user logs into AEM, they enter their user name, password, and OTP value. 

authent

The following illustration shows an AEM login screen with an OTP field. 

loginPage
An OTP field located in an AEM login screen

If the user enters a wrong OTP value, the user cannot successfully log into AEM, as shown here.

nologin
Wrong OTP value

This development article guides you through creating an OSGi bundle that supports two factor authentication. To create Java logic that supports two factor authentication, create a class that extends org.apache.sling.auth.core.spi.DefaultAuthenticationFeedbackHandler.

Note:

If you install the package that is available with this article, you can skip all steps except for steps 7 and 10. You still need to perform these two steps. 

Setup Maven in your development environment

You can use Maven to build an OSGi bundle that uses the JCR API and is deployed to Experience Manager. Maven manages required JAR files that a Java project needs in its class path. Instead of searching the Internet trying to find and download third-party JAR files to include in your project’s class path, Maven manages these dependencies for you.

You can download Maven 3 from the following URL:

http://maven.apache.org/download.html

After you download and extract Maven, create an environment variable named M3_HOME. Assign the Maven install location to this environment variable. For example:

C:\Programs\Apache\apache-maven-3.0.4

Set up a system environment variable to reference Maven. To test whether you properly setup Maven, enter the following Maven command into a command prompt:

%M3_HOME%\bin\mvn -version

This command provides Maven and Java install details and resembles the following message:

OS name: "windows 7", version: "6.1", arch: "amd64", family: "windows"

 

Note:

For more information about setting up Maven and the Home variable, see: Maven in 5 Minutes.

Next, copy the Maven configuration file named settings.xml from [install location]\apache-maven-3.0.4\conf\ to your user profile. For example, C:\Users\scottm\.m2\.

You have to configure your settings.xml file to use Adobe’s public repository. For information, see Adobe Public Maven Repository at http://repo.adobe.com/.

The following XML code represents a settings.xml file that you can use.

<?xml version="1.0" encoding="UTF-8"?>

<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements.  See the NOTICE file
distributed with this work for additional information
regarding copyright ownership.  The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License.  You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied.  See the License for the
specific language governing permissions and limitations
under the License.
-->

<!--
 | This is the configuration file for Maven. It can be specified at two levels:
 |
 |  1. User Level. This settings.xml file provides configuration for a single user, 
 |                 and is normally provided in ${user.home}/.m2/settings.xml.
 |
 |                 NOTE: This location can be overridden with the CLI option:
 |
 |                 -s /path/to/user/settings.xml
 |
 |  2. Global Level. This settings.xml file provides configuration for all Maven
 |                 users on a machine (assuming they're all using the same Maven
 |                 installation). It's normally provided in 
 |                 ${maven.home}/conf/settings.xml.
 |
 |                 NOTE: This location can be overridden with the CLI option:
 |
 |                 -gs /path/to/global/settings.xml
 |
 | The sections in this sample file are intended to give you a running start at
 | getting the most out of your Maven installation. Where appropriate, the default
 | values (values used when the setting is not specified) are provided.
 |
 |-->
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0" 
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
          xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
  <!-- localRepository
   | The path to the local repository maven will use to store artifacts.
   |
   | Default: ~/.m2/repository
  <localRepository>/path/to/local/repo</localRepository>
  -->

  <!-- interactiveMode
   | This will determine whether maven prompts you when it needs input. If set to false,
   | maven will use a sensible default value, perhaps based on some other setting, for
   | the parameter in question.
   |
   | Default: true
  <interactiveMode>true</interactiveMode>
  -->

  <!-- offline
   | Determines whether maven should attempt to connect to the network when executing a build.
   | This will have an effect on artifact downloads, artifact deployment, and others.
   |
   | Default: false
  <offline>false</offline>
  -->

  <!-- pluginGroups
   | This is a list of additional group identifiers that will be searched when resolving plugins by their prefix, i.e.
   | when invoking a command line like "mvn prefix:goal". Maven will automatically add the group identifiers
   | "org.apache.maven.plugins" and "org.codehaus.mojo" if these are not already contained in the list.
   |-->
  <pluginGroups>
    <!-- pluginGroup
     | Specifies a further group identifier to use for plugin lookup.
    <pluginGroup>com.your.plugins</pluginGroup>
    -->
  </pluginGroups>

  <!-- proxies
   | This is a list of proxies which can be used on this machine to connect to the network.
   | Unless otherwise specified (by system property or command-line switch), the first proxy
   | specification in this list marked as active will be used.
   |-->
  <proxies>
    <!-- proxy
     | Specification for one proxy, to be used in connecting to the network.
     |
    <proxy>
      <id>optional</id>
      <active>true</active>
      <protocol>http</protocol>
      <username>proxyuser</username>
      <password>proxypass</password>
      <host>proxy.host.net</host>
      <port>80</port>
      <nonProxyHosts>local.net|some.host.com</nonProxyHosts>
    </proxy>
    -->
  </proxies>

  <!-- servers
   | This is a list of authentication profiles, keyed by the server-id used within the system.
   | Authentication profiles can be used whenever maven must make a connection to a remote server.
   |-->
  <servers>
    <!-- server
     | Specifies the authentication information to use when connecting to a particular server, identified by
     | a unique name within the system (referred to by the 'id' attribute below).
     | 
     | NOTE: You should either specify username/password OR privateKey/passphrase, since these pairings are 
     |       used together.
     |
    <server>
      <id>deploymentRepo</id>
      <username>repouser</username>
      <password>repopwd</password>
    </server>
    -->
    
    <!-- Another sample, using keys to authenticate.
    <server>
      <id>siteServer</id>
      <privateKey>/path/to/private/key</privateKey>
      <passphrase>optional; leave empty if not used.</passphrase>
    </server>
    -->
  </servers>

  <!-- mirrors
   | This is a list of mirrors to be used in downloading artifacts from remote repositories.
   | 
   | It works like this: a POM may declare a repository to use in resolving certain artifacts.
   | However, this repository may have problems with heavy traffic at times, so people have mirrored
   | it to several places.
   |
   | That repository definition will have a unique id, so we can create a mirror reference for that
   | repository, to be used as an alternate download site. The mirror site will be the preferred 
   | server for that repository.
   |-->
  <mirrors>
    <!-- mirror
     | Specifies a repository mirror site to use instead of a given repository. The repository that
     | this mirror serves has an ID that matches the mirrorOf element of this mirror. IDs are used
     | for inheritance and direct lookup purposes, and must be unique across the set of mirrors.
     |
    <mirror>
      <id>mirrorId</id>
      <mirrorOf>repositoryId</mirrorOf>
      <name>Human Readable Name for this Mirror.</name>
      <url>http://my.repository.com/repo/path</url>
    </mirror>
     -->
  </mirrors>
  
  <!-- profiles
   | This is a list of profiles which can be activated in a variety of ways, and which can modify
   | the build process. Profiles provided in the settings.xml are intended to provide local machine-
   | specific paths and repository locations which allow the build to work in the local environment.
   |
   | For example, if you have an integration testing plugin - like cactus - that needs to know where
   | your Tomcat instance is installed, you can provide a variable here such that the variable is 
   | dereferenced during the build process to configure the cactus plugin.
   |
   | As noted above, profiles can be activated in a variety of ways. One way - the activeProfiles
   | section of this document (settings.xml) - will be discussed later. Another way essentially
   | relies on the detection of a system property, either matching a particular value for the property,
   | or merely testing its existence. Profiles can also be activated by JDK version prefix, where a 
   | value of '1.4' might activate a profile when the build is executed on a JDK version of '1.4.2_07'.
   | Finally, the list of active profiles can be specified directly from the command line.
   |
   | NOTE: For profiles defined in the settings.xml, you are restricted to specifying only artifact
   |       repositories, plugin repositories, and free-form properties to be used as configuration
   |       variables for plugins in the POM.
   |
   |-->
  <profiles>
    <!-- profile
     | Specifies a set of introductions to the build process, to be activated using one or more of the
     | mechanisms described above. For inheritance purposes, and to activate profiles via <activatedProfiles/>
     | or the command line, profiles have to have an ID that is unique.
     |
     | An encouraged best practice for profile identification is to use a consistent naming convention
     | for profiles, such as 'env-dev', 'env-test', 'env-production', 'user-jdcasey', 'user-brett', etc.
     | This will make it more intuitive to understand what the set of introduced profiles is attempting
     | to accomplish, particularly when you only have a list of profile id's for debug.
     |
     | This profile example uses the JDK version to trigger activation, and provides a JDK-specific repo.
    <profile>
      <id>jdk-1.4</id>

      <activation>
        <jdk>1.4</jdk>
      </activation>

      <repositories>
        <repository>
          <id>jdk14</id>
          <name>Repository for JDK 1.4 builds</name>
          <url>http://www.myhost.com/maven/jdk14</url>
          <layout>default</layout>
          <snapshotPolicy>always</snapshotPolicy>
        </repository>
      </repositories>
    </profile>
    -->

    <!--
     | Here is another profile, activated by the system property 'target-env' with a value of 'dev',
     | which provides a specific path to the Tomcat instance. To use this, your plugin configuration
     | might hypothetically look like:
     |
     | ...
     | <plugin>
     |   <groupId>org.myco.myplugins</groupId>
     |   <artifactId>myplugin</artifactId>
     |   
     |   <configuration>
     |     <tomcatLocation>${tomcatPath}</tomcatLocation>
     |   </configuration>
     | </plugin>
     | ...
     |
     | NOTE: If you just wanted to inject this configuration whenever someone set 'target-env' to
     |       anything, you could just leave off the <value/> inside the activation-property.
     |
    <profile>
      <id>env-dev</id>

      <activation>
        <property>
          <name>target-env</name>
          <value>dev</value>
        </property>
      </activation>

      <properties>
        <tomcatPath>/path/to/tomcat/instance</tomcatPath>
      </properties>
    </profile>
    -->
  

<profile>

                <id>adobe-public</id>

                <activation>

                    <activeByDefault>true</activeByDefault>

                </activation>

                <repositories>

                  <repository>

                    <id>adobe</id>

                    <name>Nexus Proxy Repository</name>

                    <url>http://repo.adobe.com/nexus/content/groups/public/</url>

                    <layout>default</layout>

                  </repository>

                </repositories>

                <pluginRepositories>

                  <pluginRepository>

                    <id>adobe</id>

                    <name>Nexus Proxy Repository</name>

                    <url>http://repo.adobe.com/nexus/content/groups/public/</url>

                    <layout>default</layout>

                  </pluginRepository>

                </pluginRepositories>

            </profile>

</profiles>

  <!-- activeProfiles
   | List of profiles that are active for all builds.
   |
  <activeProfiles>
    <activeProfile>alwaysActiveProfile</activeProfile>
    <activeProfile>anotherAlwaysActiveProfile</activeProfile>
  </activeProfiles>
  -->
</settings>

Create an Experience Manager archetype project 

You can create an Experience Manager archetype project by using the Maven archetype plugin. In this example, assume that the working directory is C:\AdobeCQ.

plugin1

To create an Experience Manager archetype project, perform these steps:

1. Open the command prompt and go to your working directory (for example, C:\AdobeCQ).

2. Run the following Maven command:

mvn archetype:generate -DarchetypeRepository=https://repo.adobe.com/nexus/content/groups/public/ -DarchetypeGroupId=com.day.jcr.vault -DarchetypeArtifactId=multimodule-content-package-archetype -DarchetypeVersion=1.0.2 -DgroupId=com.adobe.cq.authen -DartifactId=twofactor -Dversion=1.0-SNAPSHOT -Dpackage=com.adobe.cq.authen -DappsFolderName=myproject -DartifactName="My Project" -DcqVersion="5.6.1" -DpackageGroup="My Company"

3. When prompted for additional information, specify Y.

4. Once done, you will see a message like:
[INFO] Finished at: Wed Mar 27 13:38:58 EDT 2013
[INFO] Final Memory: 10M/184M

5. Change the command prompt to the generated project. For example: C:\AdobeCQ\twofactor. Run the following Maven command:
mvn eclipse:eclipse

After you run this command, you can import the project into Eclipse as discussed in the next section.
 

Add Java files to the Maven project using Eclipse 

To make it easier to work with the Maven generated project, import it into the Eclipse development environment, as shown in the following illustration.

project

The next step is to add these Java files to the com.adobe.cq.authen package: 

  • OTPBasedAuthenticationHandler - the Java class that uses two factor authentication
  • QRCode - create a barcode that you can scan using your mobile device

QRCode Servlet

The QRCode servlet is used to generate a barcode that generates an OTP value.

barcode
A barcode that helps generate an OTP value

The QRCode servlet uses these annotations:

@Component(immediate = true,metatype=false)
@Service(value = QRCode.class)
@Properties({
@Property(name = "service.description", value = "QR Image"),
@Property(name = "sling.servlet.paths", value = "/bin/qrcode"),
@Property(name = "sling.servlet.methods", value = "GET") })

The sling.servlet.paths property corresponds to the URL that you specify when using an AJAX request. 

The following code represents the QRCode servlet that extends SlingAllMethodsServlet. This interface contains the doGet method that accepts a HTTP GET  operation. 

package com.adobe.cq.authen;

import com.adobe.granite.security.user.UserProperties;
import com.adobe.granite.security.user.UserPropertiesManager;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.net.URLConnection;
import org.apache.jackrabbit.api.security.user.Authorizable;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
import org.apache.felix.scr.annotations.Properties;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Service;
import org.apache.felix.scr.annotations.sling.SlingServlet;
 
 
@SlingServlet(paths="/bin/qrcode", methods = "GET", metatype=true) 
public class QRCode
  extends SlingAllMethodsServlet
{
  public static final String GURL = "https://www.google.com/chart?chs=250x250&cht=qr&chl=otpauth://totp/";
  private final Logger log = LoggerFactory.getLogger(QRCode.class);
   
  protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response)
    throws IOException
  {
    String userId = "";
    String secretKey = "";
    try
    {
      ResourceResolver resourceResolver = request.getResourceResolver();
       Authorizable auth = (Authorizable)resourceResolver.adaptTo(Authorizable.class);
      userId = auth.getID();
      
      log.info("Auth is " +userId); 
      
     
     //generate a bar code  
      URL qrURL = new URL("https://www.google.com/chart?chs=250x250&cht=qr&chl=otpauth://totp/Example:"+userId+"?secret=JBSWY3DPEHPK3PXP&issuer=Example");
      URLConnection conn = qrURL.openConnection();
       
      response.setContentType("image/png");
      response.setStatus(200);
       
 
      InputStream is = conn.getInputStream();
      BufferedInputStream bis = new BufferedInputStream(is);
      OutputStream os = response.getOutputStream();
      BufferedOutputStream bos = new BufferedOutputStream(os);
      byte[] buff = new byte[8192];
      int sz = 0;
      while ((sz = bis.read(buff)) != -1) {
        bos.write(buff, 0, sz);
      }
      bos.flush();
    }
    catch (Exception e)
    {
      this.log.error(e.getMessage());
      e.printStackTrace();
    }
  }
}

OTPBasedAuthenticationHandler

The OTPBasedAuthenticationHandler class extends DefaultAuthenticationFeedbackHandler and implements these interfaces:

This class is responsible for validating the two factor authentication. This class contains a method named extractCredentials that checks the username, password, and OTP value, as shown in this code example.

 

 public AuthenticationInfo extractCredentials(HttpServletRequest request, HttpServletResponse response)
  {
    if (("POST".equals(request.getMethod())) && (request.getRequestURI().endsWith("/j_security_check")) && (request.getParameter("j_username") != null))
    {
      SimpleCredentials creds = new SimpleCredentials(request.getParameter("j_username"), request.getParameter("j_password").toCharArray());
      try
      {
        Session session = this.repository.login(creds);
        if (session != null)
        {
          boolean is2StepAuthEnabled = check2StepAuthPreference(request.getParameter("j_username"), session);
          if (is2StepAuthEnabled)
          {
            String needNewKey = checkOrCreateSecurityKey(request.getParameter("j_username"), session);
            if ((needNewKey != null) && (needNewKey.length() > 0))
            {
              if (request.getParameter("j_otpcode").length() <= 0)
              {
                request.setAttribute("j_reason", "invalid_otp");
                session.logout();
                
                return AuthenticationInfo.FAIL_AUTH;
              }
              boolean isSameCode = getCookie(request);
              if (!isSameCode)
              {
                if (checkCode(response, needNewKey, Long.parseLong(request.getParameter("j_otpcode")), new Date().getTime(), 23)) {
                  return createAuthenticationInfo(request, response, request.getParameter("j_username"));
                }
                request.setAttribute("j_reason", "invalid_otp");
                session.logout();
                return AuthenticationInfo.FAIL_AUTH;
              }
              request.setAttribute("j_reason", "invalid_otp");
              session.logout();
              return AuthenticationInfo.FAIL_AUTH;
            }
          }
          else
          {
            String key = checkOrCreateSecurityKey(request.getParameter("j_username"), session);
            return createAuthenticationInfo(request, response, request.getParameter("j_username"));
          }
        }
      }
      catch (LoginException e1)
      {
        e1.printStackTrace();
      }
      catch (RepositoryException e1)
      {
        e1.printStackTrace();
      }
      catch (Exception e)
      {
        e.printStackTrace();
      }
    }
    return null;
  }

The following Java code represents the entire class. 

package com.adobe.cq.authen;

import com.day.crx.security.token.TokenUtil;
import java.io.IOException;
import java.util.Arrays;
import java.util.Date;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.jcr.AccessDeniedException;
import javax.jcr.LoginException;
import javax.jcr.Node;
import javax.jcr.Property;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.SimpleCredentials;
import javax.jcr.UnsupportedRepositoryOperationException;
import javax.jcr.Value;
import javax.jcr.ValueFactory;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.codec.binary.Base32;
import org.apache.jackrabbit.api.JackrabbitSession;
import org.apache.jackrabbit.api.security.user.Authorizable;
import org.apache.jackrabbit.api.security.user.UserManager;
import org.apache.sling.auth.core.spi.AuthenticationFeedbackHandler;
import org.apache.sling.auth.core.spi.AuthenticationHandler;
import org.apache.sling.auth.core.spi.AuthenticationInfo;
import org.apache.sling.auth.core.spi.DefaultAuthenticationFeedbackHandler;
import org.apache.sling.jcr.api.SlingRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class OTPBasedAuthenticationHandler
  extends DefaultAuthenticationFeedbackHandler
  implements AuthenticationHandler, AuthenticationFeedbackHandler
{
  private static final String REQUEST_METHOD = "POST";
  private static final String USER_NAME = "j_username";
  private static final String PASSWORD = "j_password";
  private static final String OTPCODE = "j_otpcode";
  private static final String SECRET_KEY = "secretKey";
  private static boolean isNewKey = false;
  private static final String HMAC_HASH_FUNCTION = "HmacSHA1";
  private static long timeStepSizeInMillis = TimeUnit.SECONDS.toMillis(30L);
  private static int keyModulus = (int)Math.pow(10.0D, 6.0D);
  private SlingRepository repository;
  static final String REQUEST_URL_SUFFIX = "/j_security_check";
  private final Logger log = LoggerFactory.getLogger(OTPBasedAuthenticationHandler.class);
  
  public boolean authenticationSucceeded(HttpServletRequest request, HttpServletResponse response, AuthenticationInfo authInfo)
  {
    boolean result = false;
    
    return false;
  }
  
  public AuthenticationInfo extractCredentials(HttpServletRequest request, HttpServletResponse response)
  {
    if (("POST".equals(request.getMethod())) && (request.getRequestURI().endsWith("/j_security_check")) && (request.getParameter("j_username") != null))
    {
      SimpleCredentials creds = new SimpleCredentials(request.getParameter("j_username"), request.getParameter("j_password").toCharArray());
      try
      {
        Session session = this.repository.login(creds);
        if (session != null)
        {
          boolean is2StepAuthEnabled = check2StepAuthPreference(request.getParameter("j_username"), session);
          if (is2StepAuthEnabled)
          {
            String needNewKey = checkOrCreateSecurityKey(request.getParameter("j_username"), session);
            if ((needNewKey != null) && (needNewKey.length() > 0))
            {
              if (request.getParameter("j_otpcode").length() <= 0)
              {
                request.setAttribute("j_reason", "invalid_otp");
                session.logout();
                
                return AuthenticationInfo.FAIL_AUTH;
              }
              boolean isSameCode = getCookie(request);
              if (!isSameCode)
              {
                if (checkCode(response, needNewKey, Long.parseLong(request.getParameter("j_otpcode")), new Date().getTime(), 23)) {
                  return createAuthenticationInfo(request, response, request.getParameter("j_username"));
                }
                request.setAttribute("j_reason", "invalid_otp");
                session.logout();
                return AuthenticationInfo.FAIL_AUTH;
              }
              request.setAttribute("j_reason", "invalid_otp");
              session.logout();
              return AuthenticationInfo.FAIL_AUTH;
            }
          }
          else
          {
            String key = checkOrCreateSecurityKey(request.getParameter("j_username"), session);
            return createAuthenticationInfo(request, response, request.getParameter("j_username"));
          }
        }
      }
      catch (LoginException e1)
      {
        e1.printStackTrace();
      }
      catch (RepositoryException e1)
      {
        e1.printStackTrace();
      }
      catch (Exception e)
      {
        e.printStackTrace();
      }
    }
    return null;
  }
  
  private boolean check2StepAuthPreference(String userId, Session session1)
    throws AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException
  {
    Session adminSession = this.repository.loginAdministrative(null);
    
    UserManager um = ((JackrabbitSession)adminSession).getUserManager();
    org.apache.jackrabbit.api.security.user.Authorizable authorizable = um.getAuthorizable(userId);
    
    boolean is2StepEnabled = false;
    try
    {
      if (adminSession.itemExists(authorizable.getPath() + "/preferences"))
      {
        Node pref = adminSession.getNode(authorizable.getPath() + "/preferences");
        if (pref.hasProperty("twostep"))
        {
          Property references = pref.getProperty("twostep");
          String values = references.getValue().getString();
          if ((values != null) && (values.equals("Yes"))) {
            is2StepEnabled = true;
          } else {
            is2StepEnabled = false;
          }
        }
      }
    }
    catch (Exception e)
    {
      e.printStackTrace();
    }
    finally
    {
      if (adminSession != null) {
        adminSession.logout();
      }
    }
    return is2StepEnabled;
  }
  
  private AuthenticationInfo createAuthenticationInfo(HttpServletRequest request, HttpServletResponse response, String userId)
    throws RepositoryException
  {
    AuthenticationInfo authinfo = TokenUtil.createCredentials(request, response, this.repository, userId, true);
    
    return authinfo;
  }
  
  private String checkOrCreateSecurityKey(String userId, Session session)
    throws AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException
  {
    UserManager um = ((JackrabbitSession)session).getUserManager();
    Authorizable authorizable = um.getAuthorizable(userId);
    
    String key = null;
    
    String profilePath = authorizable.getPath() + "/profile";
    
    Node node = session.getNode(profilePath);
    if (node.hasProperty("secretKey"))
    {
      Property references = node.getProperty("secretKey");
      
      String secretKey = references.getValue().getString();
      if ((secretKey != null) && (secretKey.length() > 0)) {
        key = secretKey;
      }
    }
    else
    {
      key = updateSecurityKey(authorizable, session);
    }
    return key;
  }
  
  private String updateSecurityKey(Authorizable userId, Session session)
    throws RepositoryException
  {
    Session adminSession = null;
    
    adminSession = this.repository.loginAdministrative(null);
    ValueFactory vf = session.getValueFactory();
    String userPath = userId.getPath();
    String userProfilePath = userPath + "/profile";
    String key = createSecretKey();
    try
    {
      Value val = vf.createValue(key);
      if (adminSession.itemExists(userProfilePath))
      {
        Node profile = adminSession.getNode(userProfilePath);
        profile.setProperty("secretKey", val);
        adminSession.save();
      }
      else
      {
        Node user = adminSession.getNode(userPath);
        Node profile = user.addNode("profile", "nt:unstructured");
        profile.setProperty("secretKey", val);
        adminSession.save();
      }
    }
    catch (Exception e)
    {
      e.printStackTrace();
    }
    finally
    {
      if (adminSession != null) {
        adminSession.logout();
      }
    }
    return key;
  }
  
  private String createSecretKey()
  {
    byte[] buffer = new byte[30];
    

    new Random().nextBytes(buffer);
    
    byte[] secretKey = Arrays.copyOf(buffer, 10);
    String generatedKey = new Base32().encodeToString(secretKey);
    
    isNewKey = true;
    return generatedKey;
  }
  
  public void dropCredentials(HttpServletRequest arg0, HttpServletResponse arg1)
    throws IOException
  {}
  
  public boolean requestCredentials(HttpServletRequest request, HttpServletResponse arg1)
    throws IOException
  {
    return false;
  }
  
  private boolean checkCode(HttpServletResponse response, String secret, long code, long timestamp, int window)
  {
    Base32 codec32 = new Base32();
    byte[] decodedKey = codec32.decode(secret);
    
    long timeWindow = timestamp / timeStepSizeInMillis;
    for (int i = -((window - 1) / 2); i <= window / 2; i++)
    {
      long hash = calculateCode(decodedKey, timeWindow + i);
      if (hash == code)
      {
        createCookie(response, code);
        return true;
      }
    }
    return false;
  }
  
  private void createCookie(HttpServletResponse response, long code)
  {
    Cookie cookie = new Cookie("validtoken", String.valueOf(code));
    cookie.setMaxAge(600);
    cookie.setPath("/");
    response.addCookie(cookie);
  }
  
  private int calculateCode(byte[] key, long tm)
  {
    byte[] data = new byte[8];
    long value = tm;
    
    int code = 0;
    for (int i = 8; i-- > 0; value >>>= 8) {
      data[i] = ((byte)(int)value);
    }
    SecretKeySpec signKey = new SecretKeySpec(key, "HmacSHA1");
    try
    {
      Mac mac = Mac.getInstance("HmacSHA1");
      
      mac.init(signKey);
      
      byte[] hash = mac.doFinal(data);
      
      int offset = hash[(hash.length - 1)] & 0xF;
      
      long truncatedHash = 0L;
      for (int i = 0; i < 4; i++)
      {
        truncatedHash <<= 8;
        
        truncatedHash |= hash[(offset + i)] & 0xFF;
      }
      truncatedHash &= 0x7FFFFFFF;
      truncatedHash %= keyModulus;
      code = (int)truncatedHash;
    }
    catch (Exception ex) {}
    return code;
  }
  
  private boolean getCookie(HttpServletRequest request)
  {
    Cookie[] cookies = request.getCookies();
    boolean foundCookie = false;
    String otp = request.getParameter("j_otpcode");
    for (int i = 0; i < cookies.length; i++)
    {
      Cookie cookie1 = cookies[i];
      if ((cookie1.getName().equals("validtoken")) && 
        (cookie1.getValue().equals(otp))) {
        foundCookie = true;
      }
    }
    return foundCookie;
  }
  
  protected void bindRepository(SlingRepository paramSlingRepository)
  {
    this.repository = paramSlingRepository;
  }
  
  protected void unbindRepository(SlingRepository paramSlingRepository)
  {
    if (this.repository == paramSlingRepository) {
      this.repository = null;
    }
  }
}

Modify the Maven POM file 

Modify the POM files to successfully build the OSGi bundle. In the POM file located at C:\AdobeCQ\twofactor\bundle, add the following dependencies.

  • org.apache.felix.scr
  • org.apache.felix.scr.annotations
  • org.apache.jackrabbit
  • org.apache.sling

The following XML represents this POM file.

<?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/maven-v4_0_0.xsd ">
    <modelVersion>4.0.0</modelVersion>
    <!-- ====================================================================== -->
    <!-- P A R E N T P R O J E C T D E S C R I P T I O N -->
    <!-- ====================================================================== -->
    <parent>
        <groupId>com.adobe.cq.authen</groupId>
        <artifactId>twofactor</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <!-- ====================================================================== -->
    <!-- P R O J E C T D E S C R I P T I O N -->
    <!-- ====================================================================== -->

    <artifactId>twofactor-bundle</artifactId>
    <packaging>bundle</packaging>
    <name>My Project Bundle</name>

   <dependencies>
         
      
 
 
 <dependency>
	<groupId>commons-codec</groupId>
	<artifactId>commons-codec</artifactId>
	<version>1.5</version>
</dependency>
 
 
<dependency>
        <groupId>com.adobe.aem</groupId>
        <artifactId>aem-api</artifactId>
        <version>6.0.0.1</version>
        <scope>provided</scope>
      </dependency>

      <!-- All AEM 6.1 Dependency added to end of file -->

      <dependency>
        <groupId>com.adobe.cq.social</groupId>
        <artifactId>cq-socialcommunities-api</artifactId>
        <version>1.7.197</version>
        <scope>provided</scope>
      </dependency>

      <dependency>
        <groupId>com.adobe.aem</groupId>
        <artifactId>uber-jar</artifactId>
        <version>6.1.0</version>
        <scope>provided</scope>
        <classifier>obfuscated-apis</classifier>
      </dependency>
 
       <dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>servlet-api</artifactId>
    <version>2.5</version>
</dependency>
       
    <dependency>
        <groupId>org.apache.sling</groupId>
        <artifactId>org.apache.sling.jcr.api</artifactId>
        <version>2.0.4</version>
      </dependency>
             
      <dependency>
         <groupId>javax.jcr</groupId>
         <artifactId>jcr</artifactId>
         <version>2.0</version>
      </dependency>
         
        <dependency>
    <groupId>commons-lang</groupId>
    <artifactId>commons-lang</artifactId>
    <version>2.2</version>
</dependency>
         
    
         
         
        <dependency>
            <groupId>com.day.cq</groupId>
            <artifactId>cq-search</artifactId>
            <version>5.5.4</version>
            <scope>provided</scope>
        </dependency>
           
           
           
        <dependency>
            <groupId>org.osgi</groupId>
            <artifactId>org.osgi.compendium</artifactId>
        </dependency>
        <dependency>
            <groupId>org.osgi</groupId>
            <artifactId>org.osgi.core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.felix</groupId>
            <artifactId>org.apache.felix.scr.annotations</artifactId>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
        </dependency>
            
        <dependency>
         <groupId>org.apache.felix</groupId>
     
         <artifactId>org.osgi.core</artifactId>
     
         <version>1.4.0</version>
      </dependency>
           
      <dependency>
  <groupId>com.day.crx.sling</groupId>
  <artifactId>crx-auth-token</artifactId>
  <version>2.3.15</version>
</dependency>    
            
    <dependency>
    <groupId>org.apache.jackrabbit</groupId>
    <artifactId>jackrabbit-core</artifactId>
    <version>2.4.3</version>
    </dependency>
         
    <dependency>
    <groupId>org.apache.jackrabbit</groupId>
    <artifactId>jackrabbit-jcr-commons</artifactId>
    <version>2.4.3</version>
    </dependency>
     
    <dependency>
        <groupId>org.apache.sling</groupId>
        <artifactId>org.apache.sling.jcr.api</artifactId>
        <version>2.0.4</version>
      </dependency>
           
      <dependency>
         <groupId>javax.jcr</groupId>
         <artifactId>jcr</artifactId>
         <version>2.0</version>
      </dependency>
         
       <dependency>
	<groupId>org.apache.jackrabbit</groupId>
	<artifactId>jackrabbit-api</artifactId>
	<version>2.10.0</version>
	<scope>provided</scope>
</dependency>
    
    <dependency>
	<groupId>org.apache.sling</groupId>
	<artifactId>org.apache.sling.auth.core</artifactId>
	<version>1.0.4</version>
</dependency>
    
    
 <dependency>
      <groupId>org.apache.sling</groupId>
      <artifactId>org.apache.sling.api</artifactId>
      <version>2.1.0</version>
    </dependency>          
    </dependencies>

    <!-- ====================================================================== -->
    <!-- B U I L D D E F I N I T I O N -->
    <!-- ====================================================================== -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.felix</groupId>
                <artifactId>maven-scr-plugin</artifactId>
                <executions>
                    <execution>
                        <id>generate-scr-descriptor</id>
                        <goals>
                            <goal>scr</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.felix</groupId>
                <artifactId>maven-bundle-plugin</artifactId>
                <extensions>true</extensions>
                <configuration>
                    <instructions>
                        <Bundle-SymbolicName>com.adobe.cq.authen.twofactor-bundle</Bundle-SymbolicName>
                    </instructions>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.sling</groupId>
                <artifactId>maven-sling-plugin</artifactId>
                <configuration>
                    <slingUrl>http://${crx.host}:${crx.port}/apps/myproject/install</slingUrl>
                    <usePut>true</usePut>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-javadoc-plugin</artifactId>
                 <configuration>
                    <excludePackageNames>
                        *.impl
                    </excludePackageNames>
                 </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Build the OSGi bundle using Maven 

To build the OSGi bundle by using Maven, perform these steps:

 

  1. Open the command prompt and go to the C:\AdobeCQ\twofactor folder.
  2. Run the following maven command: mvn clean install.
  3. The OSGi component can be found in the following folder: C:\AdobeCQ\twofactor\bundle\target. The file name of the OSGi component is twofactor-bundle-1.0-SNAPSHOT.jar.

Deploy the bundle to Experience Manager

Once you deploy the OSGi bundle, you are able to invoke the AEM servlet that generates the barcode (configured later in this devlopment article) and sign into AEM using an OTP value. After you deploy the OSGi bundle, you will be able to see it in the Apache Felix Web Conole.

deployment

Deploy the OSGi bundle to Experience Manager by performing these steps:

  1. Login to Apache Felix Web Console at http://server:port/system/console/bundles (default admin user = admin with password= admin).
  2. Click the Bundles tab, sort the bundle list by Id, and note the Id of the last bundle.
  3. Click the Install/Update button.
  4. Browse to the bundle JAR file you just built using Maven. (C:\AdobeCQ\twofactor\bundle\target).
  5. Click Install.
  6. Click the Refresh Packages button.
  7. Check the bundle with the highest Id.
  8. Click Active. Your new bundle should now be listed with the status Active.
  9. If the status is not Active, check the CQ error.log for exceptions.

 

Install Google Authenticator

Download Google Authenticator to your mobile device. This application generates the OTP value that you use to login into AEM. 

googleAuthen
Google Authenticator

Note:

To synchronize AEM with Google Authenticator, you use the Bar Code Scan produced by the QRCode servlet. 

Modify the User Preference JCR Nodes

The next step is to modify the User Preference section in CRXDE lite so it appears like the following illustration.

 

UserPref
User Preference options

As shown in the previous illustration, notice there is a Two Step Verification option. To use two step authentication, you click Yes for the user. Then you click the GET Scan Code link. This invokes the Servlet created earily in this article. You scan the code with Google Authenticator.

To add these options, you modify the JCR as shown in the following illustration. 

nodes
JCR nodes that produce two controls to the Users Preference

Modify the AEM JCR to produce the two-step authentication options in the User Preference screen by performing these steps: 

1. In CRXDE lite, select /libs/granite/ui/content/userproperties/preferences/form/items.

2. Right-click, then select Create, Create Node.

3. Enter the following values:

  • Name: two-step-verification
  • Type: two-step-verification
4. Add the following properties:
  • class (String) - coral-RadioGroup--vertical
  • sling:resourceType (String) - granite/ui/components/foundation/form/radiogroup
  • text (String) - Two Step Verification
5. Select the /libs/granite/ui/content/userproperties/preferences/form/items/two-step-verification node.
6. Right-click, then select Create, Create Node.
7. Enter the following values:
  • Name: items
  • Type: nt:unstructured
8. Select the /libs/granite/ui/content/userproperties/preferences/form/items/two-step-verification/items node.
9. Right-click, then select Create, Create Node.
10. Enter the following values:
  • Name: yes
  • Type: nt:unstructured
11. Add the following properties:
  • name (String) - twostep
  • sling:resourceType (String) - granite/ui/components/foundation/form/radio
  • text (String) - Yes
  • value (String) - Yes
12. Select the /libs/granite/ui/content/userproperties/preferences/form/items/two-step-verification/items node.
13. Right-click, then select Create, Create Node.
14. Enter the following values:
  • Name: no
  • Type: nt:unstructured

15. Add the following properties:

  • name (String) - twostep
  • sling:resourceType (String) - granite/ui/components/foundation/form/radio
  • text (String) - No
  • value (String) - No
16. Select /libs/granite/ui/content/userproperties/preferences/form/items.
17. Right-click, then select Create, Create Node.
18. Enter the following values:
  • Name: qrcode
  • Type: nt:unstructured
19. Add the following properties:
  • href (String) - /bin/qrcode
  • sling:resourceType (String) - granite/ui/components/foundation/hyperlink
  • target (String) - _blank
  • text (String) - Get Scan Code
  • x-cq-linkchecker (String) - skip

Modify the Login JSP file 

In this section, you modify the login JSP and login JS files to use the OTP value. 

login JSP

Modify the login.jsp file located here:

/libs/granite/core/components/login/login.jsp

You need to add an extra OTP field that is located in the AEM login page (as shown in the illustration shown at the beginning of this article).

The following code represents the modified file. 

<%--

ADOBE CONFIDENTIAL
__________________

Copyright 2012 Adobe Systems Incorporated
All Rights Reserved.

NOTICE:  All information contained herein is, and remains
the property of Adobe Systems Incorporated and its suppliers,
if any.  The intellectual and technical concepts contained
herein are proprietary to Adobe Systems Incorporated and its
suppliers and are protected by trade secret or copyright law.
Dissemination of this information or reproduction of this material
is strictly forbidden unless prior written permission is obtained
from Adobe Systems Incorporated.
--%>
<%@page session="false"
        contentType="text/html"
        pageEncoding="utf-8"
        import="java.util.HashMap,
                  java.util.Map,
                  java.util.Iterator,
                  org.apache.commons.io.IOUtils,
                  org.apache.commons.lang3.StringUtils,
                  org.apache.sling.api.resource.Resource,
                  org.apache.sling.api.resource.ResourceUtil,
                  org.apache.sling.api.resource.ValueMap,
                  com.adobe.granite.xss.XSSAPI,
                  com.day.cq.i18n.I18n,
                  com.day.cq.widget.HtmlLibrary,
                  com.day.cq.widget.HtmlLibraryManager,
                  com.day.cq.widget.LibraryType,
                  org.apache.sling.auth.core.AuthUtil"%><%
%><%@taglib prefix="sling" uri="http://sling.apache.org/taglibs/sling/1.0"%><%
%><%@ taglib prefix="ui" uri="http://www.adobe.com/taglibs/granite/ui/1.0" %><%!

    static final String PARAM_NAME_REASON = "j_reason";

    static final String REASON_KEY_INVALID_LOGIN = "invalid_login";
    static final String REASON_KEY_INVALID_OTP = "invalid_otp";
    static final String REASON_KEY_SESSION_TIMED_OUT = "session_timed_out";

    String printProperty(ValueMap cfg, I18n i18n, XSSAPI xssAPI, String name, String defaultText) {
        String text = cfg.get(name, String.class);
        return xssAPI.encodeForHTML( text != null ? i18n.getVar(text) : defaultText );
    }

    /**
     * Select the configuration root resource among those stored under <code>configs</code> node.
     * The configuration with the highest order property is selected.
     * @param current the
     * @return the selected configuration root resource or <code>null</code> if no configuration root could be found.
     */
    Resource getConfigRoot(Resource current) {
        Resource configs = current.getChild("configs");
        Resource configRoot = null;
        if (configs != null) {
            long maxOrder = Long.MIN_VALUE;
            for (Iterator<Resource> cfgs = configs.listChildren() ; cfgs.hasNext() ; ) {
                Resource cfg = cfgs.next();
                ValueMap props = ResourceUtil.getValueMap(cfg);
                Long order = props.get("order", Long.class);
                if (order != null) {
                    if (order > maxOrder) {
                        configRoot = cfg;
                        maxOrder = order;
                    }
                }
            }
        }
        return configRoot;
    }

%><sling:defineObjects /><%

    final Resource configs = getConfigRoot(resource);

    final I18n i18n = new I18n(slingRequest);
    final XSSAPI xssAPI = sling.getService(XSSAPI.class).getRequestSpecificAPI(slingRequest);
    final ValueMap cfg = ResourceUtil.getValueMap(configs);

    final String authType = request.getAuthType();
    final String user = request.getRemoteUser();
    final String contextPath = slingRequest.getContextPath();

    // used to map readable reason codes to valid reason messages to avoid phishing attacks through j_reason param
    Map<String,String> validReasons = new HashMap<String, String>();
    validReasons.put(REASON_KEY_INVALID_LOGIN, printProperty(cfg, i18n, xssAPI, "box/invalidLoginText", i18n.get("User name, password and OTP do not match")));
    validReasons.put(REASON_KEY_INVALID_OTP, printProperty(cfg, i18n, xssAPI, "box/invalidLoginText", i18n.get("Invalid OTP")));
    validReasons.put(REASON_KEY_SESSION_TIMED_OUT, printProperty(cfg, i18n, xssAPI, "box/sessionTimedOutText", i18n.get("Session timed out, please login again")));

    String reason = request.getParameter(PARAM_NAME_REASON) != null
            ? request.getParameter(PARAM_NAME_REASON)
            : "";
System.out.println(reason);

    if (!StringUtils.isEmpty(reason)) {
        if (validReasons.containsKey(reason)) {
            reason = validReasons.get(reason);
        } else {
            // a reason param value not matching a key in the validReasons map is considered bogus
            log.warn("{} param value '{}' cannot be mapped to a valid reason message: ignoring", PARAM_NAME_REASON, reason);
            reason = "";
        }
    }



//reason = request.getParameter(PARAM_NAME_REASON) != null
//   ? request.getParameter(PARAM_NAME_REASON)
//        : "";

//}

%><!DOCTYPE html>
<!--[if lt IE 7 ]> <html class="ie6 oldie" class="coral-App"> <![endif]-->
<!--[if IE 7 ]> <html class="ie7 oldie" class="coral-App"> <![endif]-->
<!--[if IE 8 ]> <html class="ie8 oldie" class="coral-App"> <![endif]-->
<!--[if !(lt IE 9)|!(IE)]><!--> <html class="coral-App"> <!--<![endif]-->
<head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
    <%-- optimized for mobile, zoom/scaling disabled --%>
    <meta name="viewport" content="width = device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
    <meta http-equiv="X-UA-Compatible" content="chrome=1" />
    <title><%= printProperty(cfg, i18n, xssAPI, "title", i18n.get("Adobe Marketing Cloud")) %></title>
    <style type="text/css">
        <%
            HtmlLibraryManager htmlMgr = sling.getService(HtmlLibraryManager.class);
            HtmlLibrary lib = htmlMgr.getLibrary(LibraryType.CSS, "/libs/granite/core/content/login/clientlib");
            IOUtils.copy(lib.getInputStream(true), out, "utf-8");
        %>
    </style>
    <ui:includeClientLib css="coralui2" />
    <%
        String favicon = xssAPI.getValidHref(cfg.get("favicon", "login/adobe-logo.png"));
        favicon = xssAPI.getValidHref(favicon);
    %>
    <link rel="shortcut icon" href="<%= favicon %>" type="image/png">
    <link rel="icon" href="<%= favicon %>" type="image/png">
    <%-- Load the clientlib(s). Extension libraries should use the  'granite.core.login.extension' category. --%>
    <ui:includeClientLib js="jquery,typekit,granite.core.login,granite.core.login.extension"/>
</head>
<body class="coral--light coral-App">
<div id="wrap">
    <div id="backgrounds">
        <%-- this holds all the background divs that are dynamically loaded --%>
        <div id="bg_default" class="background"></div>
    </div>
    <div id="tag"></div>

    <%
        // make sure the redirect path is valid and prefixed with the context path
        String redirect = request.getParameter("resource");
        if (redirect == null || !AuthUtil.isRedirectValid(request, redirect)) {
            redirect = "/";
        }
        if (!redirect.startsWith(contextPath)) {
            redirect = contextPath + redirect;
        }
        String urlLogin = request.getContextPath() + resource.getPath() + ".html/j_security_check";

        if (authType == null || user == null || user.equals("anonymous")) {

    %>
    <div id="login-box">
        <div class="header">
            <h1 class="coral-Heading coral-Heading--1"><%= printProperty(cfg, i18n, xssAPI, "box/title", i18n.get("Welcome to Adobe Marketing Cloud")) %></h1>
        </div>
        <div id="leftbox" class="box">
            <p>
                <%= printProperty(cfg, i18n, xssAPI, "box/text", i18n.get("All the tools you need to solve these complex digital business challenges.")) %>
                <a class="coral-Link" id="learnmore" href="<%= xssAPI.getValidHref(i18n.getVar(cfg.get("box/learnMore/href", "#"))) %>" x-cq-linkchecker="skip"><%= printProperty(cfg, i18n, xssAPI, "box/learnMore/text", i18n.get("Learn More")) %></a>
            </p>
        </div>
        <div id="rightbox" class="box">
            <% String autocomplete = cfg.get("box/autocomplete", false) ? "on" : "off" ; %>
            <form class="coral-Form coral-Form--vertical" name="login" method="POST" id="login" action="<%= xssAPI.getValidHref(urlLogin) %>" novalidate="novalidate">
                <input type="hidden" name="_charset_" value="UTF-8"/>
                <input type="hidden" name="errorMessage" value="<%= validReasons.get(REASON_KEY_INVALID_LOGIN) %>"/>
                <input type="hidden" name="resource" value="<%= xssAPI.encodeForHTMLAttr(redirect) %>"/>
                <p class="sign-in-title"><%= printProperty(cfg, i18n, xssAPI, "box/formTitle", i18n.get("Sign in")) %></p>
                <%
                    String userPlaceholder = printProperty(cfg, i18n, xssAPI, "box/userPlaceholder", i18n.get("User name"));
                    String passwordPlaceholder = printProperty(cfg, i18n, xssAPI, "box/passwordPlaceholder", i18n.get("Password"));
                %>
                <label for="username"><span><%= userPlaceholder %></span></label>
                <input class="coral-Form-field coral-Textfield" id="username" name="j_username" type="text" autofocus="autofocus" pattern=".*" placeholder="<%= userPlaceholder %>" spellcheck="false" autocomplete="<%= autocomplete %>"/>
                <label for="password"><span><%= passwordPlaceholder %></span></label>
                <input class="coral-Form-field coral-Textfield" id="password" name="j_password" type="password"  placeholder="<%= passwordPlaceholder %>" spellcheck="false" autocomplete="<%= autocomplete %>"/>
                <input class="coral-Form-field coral-Textfield" id="otpcode" name="j_otpcode" type="password"  placeholder="OTP Code" spellcheck="false" autocomplete="<%= autocomplete %>"/>
                <div id="error" class="coral-Form-field coral-Alert coral-Alert--error <%= reason.length() > 0 ? "" : "hidden" %>">
                    <i class="coral-Alert-typeIcon coral-Icon coral-Icon--sizeS coral-Icon--alert"></i>
                    <div class='coral-Alert-message'><%= reason %></div>
                </div>
                <button type="submit" class="coral-Button coral-Button--primary"><%= printProperty(cfg, i18n, xssAPI, "box/submitText", i18n.get("Sign In")) %></button>
            </form>
        </div>
    </div>
    <div id="push"></div>
</div>
<div id="footer">
    <div class="legal-footer"><%
        // Footer: default copyright (removable)
        if (cfg.containsKey("footer/copy/text")) {
            %><span><%= printProperty(cfg, i18n, xssAPI, "footer/copy/text", "") %></span><%
        }
        %><ul id="usage-box">
            <%

                // Footer: dynamic items (config/footer/items)
                if (configs.getChild("footer/items") != null) {
                    Iterator<Resource> footerItems = configs.getChild("footer/items").listChildren();
                    while (footerItems.hasNext()) {
            %><li><%
            String itemName = footerItems.next().getName();
            String href = i18n.getVar(cfg.get("footer/items/" + itemName + "/href", String.class));
            if (href != null) {
        %><a href="<%= xssAPI.getValidHref(href) %>"><%
            }
        %><%= printProperty(cfg, i18n, xssAPI, "footer/items/" + itemName + "/text", "") %><%
            if (href != null) {
        %></a><%
            }
        %></li><%
                }
            }

        %>
        </ul>
    </div>
</div>
<script type="text/javascript">
    // try to append the current hash/fragment to the redirect resource
    if (window.location.hash) {
        var resource = document.getElementById("resource");
        if (resource) {
            resource.value += window.location.hash;
        }
    }
</script>
<% } else { %>
<script type="text/javascript">
    var redirect = '<%= xssAPI.encodeForJSString(xssAPI.getValidHref(redirect)) %>';
    if (window.location.hash) {
        redirect += window.location.hash;
    }
    document.location = redirect;
</script>
<% } %>
<!-- QUICKSTART_HOMEPAGE - (string used for readyness detection, do not remove) -->
</body>
</html>

login js

Modify the login.js file located here:

/libs/granite/core/content/login/clientlib/login.js

You need to add application logic that handes the extra OTP field. The following code represents the modified file. 

 

/*
 *
 * ADOBE CONFIDENTIAL
 * __________________
 *
 * Copyright 2012 Adobe Systems Incorporated
 * All Rights Reserved.
 *
 * NOTICE: All information contained herein is, and remains
 * the property of Adobe Systems Incorporated and its suppliers,
 * if any. The intellectual and technical concepts contained
 * herein are proprietary to Adobe Systems Incorporated and its
 * suppliers and are protected by trade secret or copyright law.
 * Dissemination of this information or reproduction of this material
 * is strictly forbidden unless prior written permission is obtained
 * from Adobe Systems Incorporated.
 */
jQuery(function($){

    // disable 403 handler in granite
    $.ajaxSetup({
        statusCode: {
            403: $.noop
        }
    });

    function flushError() {
        // adds the class to hide the alert
        $('#error').addClass('hidden');
        // clears the text
        $('#error > .coral-Alert-message').text('');
    }

    function displayError(message) {
        // removes the hidden class to show the component
        $('#error').removeClass('hidden');
        // adds the text inside the coral-Alert-message
        $('#error > .coral-Alert-message').text(message);
    }

    // Bind an event listener on login form to make an ajax call
    $("#login").submit(function(event) {
        event.preventDefault();
        var form = this;
        var path = form.action;
        var user = form.j_username.value;
        var pass = form.j_password.value;
        var otp = form.j_otpcode.value;
        var errorMessage = form.errorMessage.value;
        var resource = form.resource.value;

        // if no user is given, avoid login request
        if (!user) {
            return true;
        }

        // send user/id password to check and persist
        $.ajax({
            url: path,
            type: "POST",
            async: false,
            global: false,
            dataType: "text",
            data: {
                _charset_: "utf-8",
                j_username: user,
                j_password: pass,
                j_otpcode: otp,
                j_validate: true
            },
            success: function (data, code, jqXHR){
                var u = resource;
                if (window.location.hash && u.indexOf('#') < 0) {
                    u = u + window.location.hash;
                }
                document.location = u;
            },
            error: function() {
                displayError(errorMessage);
                form.j_password.value="";
                form.j_otpcode.value="";
            }
        });
        return true;
    });

    // workaround for typekit which takes away any focus
    var typekitTries = 5;
    function checkForTypekit() {
        if (!$('html').hasClass('wf-active') && typekitTries-- > 0) {
            setTimeout(checkForTypekit, 500);
        } else {
            $('#username').trigger('focus');
        }
    }

    $(document).on('ready', checkForTypekit);
});

Setup a test AEM user

The final task is to login to AEM (http://localhost:4502) with Admin credentials, and create a test account (for example, test2). Next, login with your test2 user and password. Do not need to enter OTP yet. 

After you login, go to user preferences by clicking the user icon in the upper right hand corner. Click the User preferences icon (it looks like a gear icon). You will see the new User Preferences screen as shown earilier in this article. Click Yes under Two Step Verification.

Then click the Get Scan Code. Using the Google Authenticor application that you installed on your mobile device, scan the bar code that appears, as shown in this illustration.  

scanner
Google Authenticator Barcode scanner

Once done, your device produces the OTP value that you can use to sign into AEM with testUser1 account.

iphone1
OTC on a mobile device

Be sure to select Yes for test2 account. Log out with test2 and then log back in using the password and the OTP value.

 

See also

Congratulations, you have just created an AEM OSGi bundle that support two-factor authentication. Please refer to the AEM community page for other articles that discuss how to build AEM services/applications.

This work is licensed under a Creative Commons Attribution-Noncommercial-Share Alike 3.0 Unported License  Twitter™ and Facebook posts are not covered under the terms of Creative Commons.

Legal Notices   |   Online Privacy Policy