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.  

Thank you to Praveen Dubey and Lokesh Bs, two top Experience Manager Community members, for contributing Java code that is used in this article.

In addition, thank you to Sumanta Pakira for contributing the original code and idea for this use case.  

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.4

Download

Note:

To successfully use this authenitcation handler, you must white list this bundle after you deploy the it. The Bundle-Symbolic name is TwoFactorAEM.core. For information on how to configure a white list for a bundle, see https://forums.adobe.com/thread/2355506.

Introduction

You can create a custom authentication handler for Experience Manager 6.4. A custom authentication handler improves security for your Experience Manager instance. To create a custom authentication handler, you create a custom Java class that implements Interface AuthenticationHandler

In this article, to show an example of a custom authentication handler, two-factor authentication is used. That is, you can configure Experience Manager 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 a user logs into Experience Manager, they enter their user name, password, and OTP value. 

authent
A user logs into Experience Manager using an OTP value

The following illustration shows the Experience Manager login screen with an OTP field. 

loginPage
An OTP field located in a login screen

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

nologin
Wrong OTP value

This article walks you through building a custom authentication handler using R7 annotations and a Maven Archetype 15 project.

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.

maven
Default files created by the Maven archetype plugin

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 -DarchetypeGroupId=com.adobe.granite.archetypes -DarchetypeArtifactId=aem-project-archetype -DarchetypeVersion=15

3. When prompted, specify the following information:

  • groupId - TwoFactorAEM
  • artifactId - TwoFactorAEM
  • version - 1.0-SNAPSHOT
  • package - com.aem.authen
  • appsFolderName - TwoFactorAEM
  • artifactName - TwoFactorAEM
  • componentGroupName - TwoFactorAEM
  • confFolderName - TwoFactorAEM
  • contentFolderName - TwoFactorAEM
  • cssId - TwoFactorAEM
  • packageGroup - TwoFactorAEM
  • siteName - TwoFactorAEM

4. WHen prompted, specify Y.

5. Once done, you will see a message like:

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 01:42 min
[INFO] Finished at: 2016-04-25T14:34:19-04:00
[INFO] Final Memory: 16M/463M
[INFO] ------------------------------------------------------------------------

6. Change the working directory to TwoFactorAEM, and then enter the following 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 Eclipse Import Project dialog

The next step is to add these Java files to your project:

  • OTPBasedAuthenticationHandler - the Java class that uses two factor authentication
  • SecurityUtils - a utility class the helps with the required token
  • TwoFactorAuthHandler - a class that works with configuration values. For example, references the default login page. 
  • QRCode - create a barcode that you can scan using your mobile device

All other Java files were removed from the project. Only these Java files are part of the project, as shown in this illustration. 

JavaPro

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 following code represents the QRCode servlet that extends SlingAllMethodsServlet. This interface contains the doGet method that accepts a HTTP GET  operation. 

package com.aem.authen.core.servlets;

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.sling.api.servlets.HttpConstants;
import org.osgi.framework.Constants;

import javax.jcr.Session;
import javax.servlet.Servlet;
import org.osgi.service.component.annotations.Component;

/*
  This is generic implmentation of QR to show how it works using path based, Not a production ready code.
*/

@Component(service = Servlet.class, property = { Constants.SERVICE_DESCRIPTION + "= QR Code Servlet for Authenticator",
    "sling.servlet.methods=" + HttpConstants.METHOD_GET, "sling.servlet.paths=" + "/bin/qrcode" })

public class QRCode extends SlingAllMethodsServlet {

  private static final long serialVersionUID = 1L;
  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.debug("Generating QR Code for user {} ", userId);

      Session userSession =  resourceResolver.adaptTo(Session.class);
      secretKey = com.aem.authen.core.SecurityUtils.updateSecurityKey(auth, userSession);

      // generate a bar code using google chart api,
      URL qrURL = new URL("https://www.google.com/chart?chs=250x250&cht=qr&chl=otpauth://totp/Example:" + userId
          + "?secret=" + secretKey);

      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) {
      log.error("Exception while generate QR Code for User {}, {}", userId, e);

    }

  }

  
}

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.

Also notice the @ServiceRanking(60000) line of code. This is responsible to ensure this service is invoked to handle the authentication request. Without this line of code, this authentication handler is not invoked. (This is a R7 annotation). 

package com.aem.authen.core;

import com.day.crx.security.token.TokenUtil;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Date;
import java.util.LinkedHashMap;
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.Repository;
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.commons.lang3.StringUtils;
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.jackrabbit.commons.JcrUtils;
import org.apache.sling.api.auth.Authenticator;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.auth.core.AuthUtil;
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.osgi.service.component.annotations.ConfigurationPolicy;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.propertytypes.ServiceDescription;
import org.osgi.service.component.propertytypes.ServiceRanking;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.AttributeType;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/*
  This is sample implmentation of 2F Authentication Handler to show how it works, Not a production ready code.
*/

@Component(service = AuthenticationHandler.class, immediate = true, property = { "path=/" })
@ServiceRanking(60000)
@ServiceDescription("Google 2F Authentication Handler")
@Designate(ocd = TwoFactorAuthHandler.class)

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);
	static final String REQUEST_URL_SUFFIX = "/j_security_check";
	private static final String PAR_LOOP_PROTECT = "$$login$$";

	@Reference
	private ResourceResolverFactory resourceResolverFactory;

	@Activate
	private com.aem.authen.core.TwoFactorAuthHandler config;

	@Reference
	private SlingRepository repository;

	private final Logger log = LoggerFactory.getLogger(OTPBasedAuthenticationHandler.class);

	public AuthenticationInfo extractCredentials(HttpServletRequest request, HttpServletResponse response) {
		log.info("OTPBasedAuthenticationHandler : Inside extractCredentials {}", request.getRequestURI());

		if ((REQUEST_METHOD.equals(request.getMethod())) && (request.getRequestURI().endsWith(REQUEST_URL_SUFFIX))
				&& (request.getParameter(USER_NAME) != null)) {
			try {
				SimpleCredentials creds = new SimpleCredentials(request.getParameter(USER_NAME),
						request.getParameter(PASSWORD).toCharArray());
				Session session = this.repository.login(creds);

				if (session != null) {
					boolean is2StepAuthEnabled = check2StepAuthPreference(request.getParameter("j_username"), session);

					// Authenticate with OTP
					if (is2StepAuthEnabled) {
						return twoFactorAuthentication(is2StepAuthEnabled, request, response, session);

					} else {
						return createAuthenticationInfo(request, response, request.getParameter("j_username"));

					}
				}
			}

			catch (LoginException e) {
				log.error("[LoginException] in extractCredentials while processing the request {}", e);

			} catch (RepositoryException e) {
				log.error("[RepositoryException] in extractCredentials while processing the request {}", e);

			} catch (Exception e) {
				log.error("[RepositoryException] in extractCredentials while processing the request {}", e);

			}
		}
		return null;
	}

	private boolean check2StepAuthPreference(String userId, Session session1)
			throws AccessDeniedException, RepositoryException, LoginException {
		log.info("OTPBasedAuthenticationHandler : Inside check2StepAuthPreference ");
		Session adminSession = null;
		boolean is2StepEnabled = false;
		try {
			adminSession = this.resourceResolverFactory.getAdministrativeResourceResolver(null).adaptTo(Session.class);

			UserManager um = ((JackrabbitSession) session1).getUserManager();
			org.apache.jackrabbit.api.security.user.Authorizable authorizable = um.getAuthorizable(userId);

			if (adminSession.itemExists(authorizable.getPath() + "/preferences")) {
				Node pref = adminSession.getNode(authorizable.getPath() + "/preferences");
				if (pref.hasProperty("twostep")) {
					Property references = pref.getProperty("twostep");
					String value = references.getValue().getString();
					if ((value != null) && (value.equals("yes"))) {
						is2StepEnabled = true;
					} else {
						is2StepEnabled = false;
					}
				}
			}
		} catch (org.apache.sling.api.resource.LoginException e) {
			log.error("Error while checking user 2 factor authentication preference, logging out user session {}", e);
			throw new LoginException("failed to retrieve user preference, logging out user session");

		} catch (NullPointerException e) {
			throw new NullPointerException("failed to retrieve user preference, logging out user session");

		} finally {
			if (adminSession != null) {
				adminSession.logout();
			}

		}
		return is2StepEnabled;

	}

	private AuthenticationInfo createAuthenticationInfo(HttpServletRequest request, HttpServletResponse response,
			String userId) throws RepositoryException {
		log.info("OTPBasedAuthenticationHandler : Inside createAuthenticationInfo ");
		AuthenticationInfo authinfo = TokenUtil.createCredentials(request, response, this.repository, userId, true);

		return authinfo;
	}

	public void dropCredentials(HttpServletRequest arg0, HttpServletResponse arg1) throws IOException {

	}

	public boolean requestCredentials(HttpServletRequest request, HttpServletResponse arg1) throws IOException {
		// use a linked hash map to guarantee order of parameters
		log.info("OTPBasedAuthenticationHandler : INside CREDS ");
		String reason = getReason(request, FAILURE_REASON);
		String reasonCode = getReason(request, FAILURE_REASON_CODE);

		try {
			String path = rewrite(request, this.resourceResolverFactory.getResourceResolver(null),
					config.loginPageValue());
			LinkedHashMap<String, String> params = new LinkedHashMap<String, String>();
			params.put(Authenticator.LOGIN_RESOURCE, path);
			params.put(PAR_LOOP_PROTECT, PAR_LOOP_PROTECT);

			// append indication of previous login failure
			params.put(FAILURE_REASON, reason);
			params.put(FAILURE_REASON_CODE, reasonCode);
			AuthUtil.sendRedirect(request, arg1, path, params);

		} catch (IOException e) {
			log.error("[IOException] Failed to redirect to the login / change password form [{}]", e);

		} catch (org.apache.sling.api.resource.LoginException e) {
			log.error("[LoginException] Failed to redirect to the login / change password form [{}]", e);

		}

		return true;
	}

	private String checkOrCreateSecurityKey(String userId, Session session)
			throws AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException {
		log.info("OTPBasedAuthenticationHandler : INside checkOrCreateSecurityKey ");
		Session adminSession;
		Authorizable authorizable = null;
		String key = null;
		try {
			adminSession = this.resourceResolverFactory.getAdministrativeResourceResolver(null).adaptTo(Session.class);
			UserManager um = ((JackrabbitSession) adminSession).getUserManager();
			authorizable = um.getAuthorizable(userId);
			String profilePath = authorizable.getPath() + "/profile";
			Node node = adminSession.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 = SecurityUtils.updateSecurityKey(authorizable, session);
			}

		} catch (Exception e) {
			log.error("Error while retrieving or creating new security key {}", e);
		}

		return key;
	}

	/*----------- Private Uility Methods below -------------*/

	// Attemp to authenticate user with OTP
	private AuthenticationInfo twoFactorAuthentication(boolean is2StepAuthEnabled, HttpServletRequest request,
			HttpServletResponse response, Session session) throws RepositoryException {

		String needNewKey = checkOrCreateSecurityKey(request.getParameter("j_username"), session);
		if ((needNewKey != null) && (needNewKey.length() > 0)) {
			// 2FA enabled but no OTP provided by user, logout the session.
			if (request.getParameter("j_otpcode").length() <= 0) {
				log.info("2FA enabled but no OTP provided by user: {}, logout the session.",
						request.getParameter(USER_NAME));
				request.setAttribute("j_reason", "invalid_otp");
				return AuthenticationInfo.FAIL_AUTH;
			}

			// Check if current OTP matches with users last used token in same browser
			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");
				return AuthenticationInfo.FAIL_AUTH;
			}

			log.info("Last used OTP is similar current OTP, logout the session.", request.getParameter(USER_NAME));
			request.setAttribute("j_reason", "invalid_otp");
			session.logout();
			return AuthenticationInfo.FAIL_AUTH;
		}
		return null;

	}

	private String rewrite(final HttpServletRequest request, final ResourceResolver resolver, final String path) {
		return StringUtils.endsWith(path, ".html") ? path : path.concat(".html");
	}

	private static String getReason(HttpServletRequest request, String parameter) {
		Object reason = request.getAttribute(parameter);
		if (null == reason) {
			reason = request.getParameter(parameter);
		}
		if (null == reason) {
			reason = FAILURE_REASON_CODES.UNKNOWN;
		}
		return (reason instanceof Enum) ? ((Enum) reason).name().toLowerCase() : reason.toString();
	}

	// Verify the user token with set of tokens in given window.
	private boolean checkCode(HttpServletResponse response, String secret, long code, long timestamp, int window) {
		log.info("OTPBasedAuthenticationHandler : INside checkCode ");
		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;
			try {
				hash = verify_code(decodedKey, timeWindow + i);
				log.info("HASH {}, code {}", hash, code);
				if (hash == code) {
					createCookie(response, code);
					return true;
				}
			} catch (InvalidKeyException e) {
				log.error("[InvalidKeyException] while checking security code for user {}", e);
			} catch (NoSuchAlgorithmException e) {
				log.error("[NoSuchAlgorithmException] while checking security code for user {}", e);
			}

		}
		return false;
	}

	/*
	 * Create a cooke with current valid token in browser after successfulle login.
	 * Every login will be compared with last used key and will be denied if those
	 * are same.
	 */
	private void createCookie(HttpServletResponse response, long code) {
		log.info("OTPBasedAuthenticationHandler : INside createCookie ");
		Cookie cookie = new Cookie("validtoken", String.valueOf(code));
		cookie.setMaxAge(600);
		cookie.setPath("/");
		response.addCookie(cookie);
	}

	private static int verify_code(byte[] key, long t) throws NoSuchAlgorithmException, InvalidKeyException {
		byte[] data = new byte[8];
		long value = t;
		for (int i = 8; i-- > 0; value >>>= 8) {
			data[i] = (byte) value;
		}

		SecretKeySpec signKey = new SecretKeySpec(key, "HmacSHA1");
		Mac mac = Mac.getInstance("HmacSHA1");
		mac.init(signKey);
		byte[] hash = mac.doFinal(data);

		int offset = hash[20 - 1] & 0xF;

		// We're using a long because Java hasn't got unsigned int.
		long truncatedHash = 0;
		for (int i = 0; i < 4; ++i) {
			truncatedHash <<= 8;
			truncatedHash |= (hash[offset + i] & 0xFF);
		}

		truncatedHash &= 0x7FFFFFFF;
		truncatedHash %= 1000000;

		return (int) truncatedHash;
	}

	private boolean getCookie(HttpServletRequest request) {
		boolean foundCookie = false;
		if (request.getCookies() != null) {
			Cookie[] cookies = request.getCookies();
			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;
		}
	}

}

Note:

Log messages are written to project-TwoFactorAEM.log by default. 

SecurityUtils class

The following Java code represents the SecurityUtils class. 

package com.aem.authen.core;

import java.util.Arrays;
import java.util.Random;

import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.Value;
import javax.jcr.ValueFactory;

import org.apache.commons.codec.binary.Base32;
import org.apache.jackrabbit.api.security.user.Authorizable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

final public class SecurityUtils {

  private final static Logger log = LoggerFactory.getLogger(SecurityUtils.class);

  public static String updateSecurityKey(Authorizable userId, Session adminSession) throws RepositoryException {
    String key = null;
    
    try {
      ValueFactory vf = adminSession.getValueFactory();
      String userPath = userId.getPath();
      String userProfilePath = userPath + "/profile";
      key = createSecretKey();

      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) {
      log.error("[Exception] while creating security key for user: {}",userId, e);

    } finally {
      if (adminSession != null) {
        adminSession.logout();
      }

    }
    return key;
  }

  private static String createSecretKey() {
    byte[] buffer = new byte[30];
    new Random().nextBytes(buffer);

    byte[] secretKey = Arrays.copyOf(buffer, 10);
    String generatedKey = new Base32().encodeToString(secretKey);
    return generatedKey;
  }
}

TwoFactorAuthHandler class

The following class is responsible for OSGi configuration values. 

package com.aem.authen.core;

import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;

@ObjectClassDefinition(name = "Two Factor Authentication Handler Interface", description = "Two Factor Authentication Handler Interface Configuration")
public @interface TwoFactorAuthHandler {

	@AttributeDefinition(name = "Path", description = "Repository path for which this authentication handler should be used by Sling. If this is empty, the authentication handler will be disabled. (path)")
	String pathValue() default "/";

	@AttributeDefinition(name = "Default Login Page", description = "If no mappings are defined, nor no mapping matches the request, this is the default login page being redirected to. This can be overridden in the content page configuration")
	String loginPageValue() default "/libs/granite/core/content/login.html";
}

Modify the Maven POM file 

Add the following POM dependency to the POM file located at C:\AdobeCQ\TwoFactorAEM. The following represents this POM file. 

<?xml version="1.0" encoding="UTF-8"?>
<!--
 |  Copyright 2015 Adobe Systems Incorporated
 |
 |  Licensed 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.
-->
<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>
    <groupId>TwoFactorAEM</groupId>
    <artifactId>TwoFactorAEM</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <description>TwoFactorAEM</description>

    <modules>
        <module>core</module>
        <module>ui.apps</module>
        <module>ui.content</module>
    </modules>

	<properties>
		<aem.host>localhost</aem.host>
		<aem.port>4502</aem.port>
		<aem.publish.host>localhost</aem.publish.host>
		<aem.publish.port>4503</aem.publish.port>
		<sling.user>admin</sling.user>
		<sling.password>admin</sling.password>
		<vault.user>admin</vault.user>
		<vault.password>admin</vault.password>
	</properties>
	<build>
		<plugins>
			<!-- Maven Release Plugin -->
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-release-plugin</artifactId>
				<version>2.5.1</version>
				<configuration>
					<scmCommentPrefix>[maven-scm] :</scmCommentPrefix>
					<preparationGoals>clean install</preparationGoals>
					<goals>install</goals>
					<releaseProfiles>release</releaseProfiles>
				</configuration>
			</plugin>
			<!-- Maven Source Plugin -->
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-source-plugin</artifactId>
				<version>2.4</version>
				<inherited>true</inherited>
			</plugin>
			<!-- Maven Resources Plugin -->
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-resources-plugin</artifactId>
				<configuration>
					<encoding>UTF-8</encoding>
				</configuration>
			</plugin>
			<!-- Maven Jar Plugin -->
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-jar-plugin</artifactId>
				<version>2.5</version>
			</plugin>
			<!-- Maven Enforcer Plugin -->
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-enforcer-plugin</artifactId>
				<executions>
					<execution>
						<id>enforce-maven</id>
						<goals>
							<goal>enforce</goal>
						</goals>
						<configuration>
							<rules>
								<requireMavenVersion>
									<version>[2.2.1,)</version>
								</requireMavenVersion>
								<requireJavaVersion>
									<message>Project must be compiled with Java 8 or higher</message>
									<version>1.8.0</version>
								</requireJavaVersion>
							</rules>
						</configuration>
					</execution>
				</executions>
			</plugin>
			<!-- Maven Compiler Plugin -->
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<configuration>
					<source>1.8</source>
					<target>1.8</target>
					<encoding>UTF-8</encoding>
				</configuration>
			</plugin>
			<!-- Maven IntelliJ IDEA Plugin -->
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-idea-plugin</artifactId>
				<version>2.2.1</version>
				<configuration>
					<jdkLevel>1.8</jdkLevel>
					<linkModules>true</linkModules>
					<downloadSources>true</downloadSources>
				</configuration>
			</plugin>
			<!-- Maven Eclipse Plugin -->
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-eclipse-plugin</artifactId>
				<version>2.9</version>
				<configuration>
					<downloadSources>true</downloadSources>
				</configuration>
			</plugin>
		</plugins>
		<pluginManagement>
			<plugins>
				<!-- Maven Clean Plugin -->
				<plugin>
					<groupId>org.apache.maven.plugins</groupId>
					<artifactId>maven-clean-plugin</artifactId>
					<version>2.6.1</version>
				</plugin>
				<!-- Maven Resources Plugin -->
				<plugin>
					<groupId>org.apache.maven.plugins</groupId>
					<artifactId>maven-resources-plugin</artifactId>
					<version>2.7</version>
				</plugin>
				<!-- Maven Compiler Plugin -->
				<plugin>
					<groupId>org.apache.maven.plugins</groupId>
					<artifactId>maven-compiler-plugin</artifactId>
					<version>3.2</version>
				</plugin>
				<!-- Apache Felix SCR Plugin -->
				<plugin>
					<groupId>org.apache.felix</groupId>
					<artifactId>maven-scr-plugin</artifactId>
					<version>1.20.0</version>
					<executions>
						<execution>
							<id>generate-scr-scrdescriptor</id>
							<goals>
								<goal>scr</goal>
							</goals>
							<configuration>
								<!-- Private service properties for all services. -->
								<properties>
									<service.vendor>Adobe</service.vendor>
								</properties>
							</configuration>
						</execution>
					</executions>
					<configuration>
						<outputDirectory>${project.build.directory}/classes</outputDirectory>
					</configuration>
					<dependencies>
						<dependency>
							<groupId>org.slf4j</groupId>
							<artifactId>slf4j-simple</artifactId>
							<version>1.5.11</version>
						</dependency>
					</dependencies>
				</plugin>
				<!-- Maven Installer Plugin -->
				<plugin>
					<groupId>org.apache.maven.plugins</groupId>
					<artifactId>maven-install-plugin</artifactId>
					<version>2.5.2</version>
				</plugin>
				<!-- Maven Surefire Plugin -->
				<plugin>
					<groupId>org.apache.maven.plugins</groupId>
					<artifactId>maven-surefire-plugin</artifactId>
					<version>2.18.1</version>
				</plugin>
				<!-- Maven Failsafe Plugin -->
				<plugin>
					<groupId>org.apache.maven.plugins</groupId>
					<artifactId>maven-failsafe-plugin</artifactId>
					<version>2.18.1</version>
				</plugin>
				<!-- Maven Deploy Plugin -->
				<plugin>
					<groupId>org.apache.maven.plugins</groupId>
					<artifactId>maven-deploy-plugin</artifactId>
					<version>2.8.2</version>
				</plugin>
				<!-- Apache Sling Plugin -->
				<plugin>
					<groupId>org.apache.sling</groupId>
					<artifactId>maven-sling-plugin</artifactId>
					<version>2.1.0</version>
					<executions>
						<execution>
							<goals>
								<goal>install</goal>
							</goals>
						</execution>
					</executions>
					<configuration>
						<slingUrl>http://${aem.host}:${aem.port}/crx/repository/crx.default</slingUrl>
						<usePut>true</usePut>
						<failOnError>true</failOnError>
					</configuration>
				</plugin>
				<!-- Content Package Plugin -->
				<plugin>
					<groupId>com.day.jcr.vault</groupId>
					<artifactId>content-package-maven-plugin</artifactId>
					<version>0.5.24</version>
					<configuration>
						<targetURL>http://${aem.host}:${aem.port}/crx/packmgr/service.jsp</targetURL>
						<failOnError>true</failOnError>
						<failOnMissingEmbed>true</failOnMissingEmbed>
					</configuration>
				</plugin>
				<!-- Apache Felix Bundle Plugin -->
				<plugin>
					<groupId>org.apache.felix</groupId>
					<artifactId>maven-bundle-plugin</artifactId>
					<version>4.1.0</version>
					<inherited>true</inherited>
					<!-- use 3.2.0 or higher to use the R6 annotations -->
					<!-- use 4.1.0 or higher yo use the R7 annotations -->
				</plugin>
				<!-- Maven Enforcer Plugin -->
				<plugin>
					<groupId>org.apache.maven.plugins</groupId>
					<artifactId>maven-enforcer-plugin</artifactId>
					<version>1.4</version>
				</plugin>
				<!-- Maven Dependency Plugin -->
				<plugin>
					<groupId>org.apache.maven.plugins</groupId>
					<artifactId>maven-dependency-plugin</artifactId>
					<version>2.10</version>
				</plugin>
				<!-- Build Helper Maven Plugin -->
				<plugin>
					<groupId>org.codehaus.mojo</groupId>
					<artifactId>build-helper-maven-plugin</artifactId>
					<version>1.9.1</version>
				</plugin>
				<!--This plugin's configuration is used to store Eclipse m2e settings 
					only. It has no influence on the Maven build itself. -->
				<plugin>
					<groupId>org.eclipse.m2e</groupId>
					<artifactId>lifecycle-mapping</artifactId>
					<version>1.0.0</version>
					<configuration>
						<lifecycleMappingMetadata>
							<pluginExecutions>
								<pluginExecution>
									<pluginExecutionFilter>
										<groupId>org.apache.maven.plugins</groupId>
										<artifactId>maven-enforcer-plugin</artifactId>
										<versionRange>[1.0.0,)</versionRange>
										<goals>
											<goal>enforce</goal>
										</goals>
									</pluginExecutionFilter>
									<action>
										<ignore />
									</action>
								</pluginExecution>
								<pluginExecution>
									<pluginExecutionFilter>
										<groupId>
											org.apache.maven.plugins
										</groupId>
										<artifactId>
											maven-dependency-plugin
										</artifactId>
										<versionRange>
											[2.2,)
										</versionRange>
										<goals>
											<goal>copy-dependencies</goal>
											<goal>unpack</goal>
										</goals>
									</pluginExecutionFilter>
									<action>
										<ignore />
									</action>
								</pluginExecution>
								<pluginExecution>
									<pluginExecutionFilter>
										<groupId>
											org.codehaus.mojo
										</groupId>
										<artifactId>
											build-helper-maven-plugin
										</artifactId>
										<versionRange>
											[1.5,)
										</versionRange>
										<goals>
											<goal>
												reserve-network-port
											</goal>
										</goals>
									</pluginExecutionFilter>
									<action>
										<ignore />
									</action>
								</pluginExecution>
							</pluginExecutions>
						</lifecycleMappingMetadata>
					</configuration>
				</plugin>
			</plugins>
		</pluginManagement>
	</build>

	<profiles>
		<!-- ====================================================== -->
		<!-- A D O B E P U B L I C P R O F I L E -->
		<!-- ====================================================== -->
		<profile>
			<id>adobe-public</id>

			<activation>
				<activeByDefault>true</activeByDefault>
			</activation>

			<properties>
				<releaseRepository-Id>adobe-public-releases</releaseRepository-Id>
				<releaseRepository-Name>Adobe Public Releases</releaseRepository-Name>
				<releaseRepository-URL>https://repo.adobe.com/nexus/content/groups/public</releaseRepository-URL>
			</properties>

			<repositories>
				<repository>
					<id>adobe-public-releases</id>
					<name>Adobe Public Repository</name>
					<url>https://repo.adobe.com/nexus/content/groups/public</url>
					<releases>
						<enabled>true</enabled>
						<updatePolicy>never</updatePolicy>
					</releases>
					<snapshots>
						<enabled>false</enabled>
					</snapshots>
				</repository>
			</repositories>

			<pluginRepositories>
				<pluginRepository>
					<id>adobe-public-releases</id>
					<name>Adobe Public Repository</name>
					<url>https://repo.adobe.com/nexus/content/groups/public</url>
					<releases>
						<enabled>true</enabled>
						<updatePolicy>never</updatePolicy>
					</releases>
					<snapshots>
						<enabled>false</enabled>
					</snapshots>
				</pluginRepository>
			</pluginRepositories>
		</profile>
	</profiles>


	<!-- ====================================================================== -->
	<!-- D E P E N D E N C I E S -->
	<!-- ====================================================================== -->
	<dependencyManagement>
		<dependencies>
			<!-- OSGi Dependencies -->
			<dependency>
				<groupId>org.apache.felix</groupId>
				<artifactId>org.apache.felix.scr</artifactId>
				<version>2.1.12</version>
				<scope>provided</scope>
			</dependency>
			<dependency>
				<groupId>org.apache.felix</groupId>
				<artifactId>org.apache.felix.scr.annotations</artifactId>
				<version>1.9.6</version>
				<scope>provided</scope>
			</dependency>
			<dependency>
				<groupId>biz.aQute</groupId>
				<artifactId>bndlib</artifactId>
				<version>1.50.0</version>
				<scope>provided</scope>
			</dependency>
			<dependency>
				<groupId>org.osgi</groupId>
				<artifactId>org.osgi.core</artifactId>
				<version>6.0.0</version>
				<scope>provided</scope>
			</dependency>
			<dependency>
				<groupId>org.osgi</groupId>
				<artifactId>org.osgi.compendium</artifactId>
				<version>6.0.0</version>
				<scope>provided</scope>
			</dependency>
			<!-- Logging Dependencies -->
			<dependency>
				<groupId>org.slf4j</groupId>
				<artifactId>slf4j-api</artifactId>
				<version>1.5.11</version>
				<scope>provided</scope>
			</dependency>

			<!-- dependency for AEM6.4 Please check for other dependencies: https://repo.adobe.com/nexus/content/groups/public/com/adobe/aem/uber-jar/ -->
			<dependency>
				<groupId>com.adobe.aem</groupId>
				<artifactId>uber-jar</artifactId>
				<version>6.4.2</version>
				<classifier>apis</classifier>
				<scope>provided</scope>
			</dependency>

			<!-- needed for javax.Inject -->
			<dependency>
				<groupId>javax.inject</groupId>
				<artifactId>javax.inject</artifactId>
				<version>1</version>
				<scope>provided</scope>
			</dependency>
			<!-- OSGi r7 dependencies -->
			<dependency>
				<groupId>org.osgi</groupId>
				<artifactId>org.osgi.service.component.annotations</artifactId>
				<version>1.4.0</version>
				<scope>provided</scope>
			</dependency>

			<dependency>
				<groupId>org.osgi</groupId>
				<artifactId>org.osgi.annotation</artifactId>
				<version>6.0.0</version>
				<scope>provided</scope>
			</dependency>

			<dependency>
				<groupId>org.osgi</groupId>
				<artifactId>org.osgi.service.metatype.annotations</artifactId>
				<version>1.4.0</version>
				<scope>provided</scope>
			</dependency>

			<dependency>
				<groupId>org.osgi</groupId>
				<artifactId>org.osgi.service.component</artifactId>
				<version>1.4.0</version>
				<scope>provided</scope>
			</dependency>

			<!-- Servlet API -->
			<dependency>
				<groupId>javax.servlet</groupId>
				<artifactId>servlet-api</artifactId>
				<version>2.4</version>
				<scope>provided</scope>
			</dependency>
			<dependency>
				<groupId>javax.servlet.jsp</groupId>
				<artifactId>jsp-api</artifactId>
				<version>2.1</version>
				<scope>provided</scope>
			</dependency>
			<!-- JCR -->
			<dependency>
				<groupId>javax.jcr</groupId>
				<artifactId>jcr</artifactId>
				<version>2.0</version>
				<scope>provided</scope>
			</dependency>
			<!-- Taglibs -->
			<dependency>
				<groupId>com.day.cq.wcm</groupId>
				<artifactId>cq-wcm-taglib</artifactId>
				<version>5.7.4</version>
				<scope>provided</scope>
			</dependency>
			<!-- Core Components -->
			<dependency>
				<groupId>com.adobe.cq</groupId>
				<artifactId>core.wcm.components.core</artifactId>
				<version>2.2.0</version>
			</dependency>

			<dependency>
				<groupId>com.adobe.cq</groupId>
				<artifactId>core.wcm.components.all</artifactId>
				<type>zip</type>
				<version>2.2.0</version>
			</dependency>

			<dependency>
				<groupId>com.adobe.acs</groupId>
				<artifactId>acs-aem-commons-bundle</artifactId>
				<version>3.19.0</version>
			</dependency>

			<!-- Testing -->
			<dependency>
				<groupId>junit</groupId>
				<artifactId>junit</artifactId>
				<version>4.8.2</version>
				<scope>test</scope>
			</dependency>
			<dependency>
				<groupId>org.slf4j</groupId>
				<artifactId>slf4j-simple</artifactId>
				<version>1.5.11</version>
				<scope>test</scope>
			</dependency>
			<dependency>
				<groupId>org.mockito</groupId>
				<artifactId>mockito-all</artifactId>
				<version>1.9.5</version>
				<scope>test</scope>
			</dependency>
			<dependency>
				<groupId>junit-addons</groupId>
				<artifactId>junit-addons</artifactId>
				<version>1.4</version>
				<scope>test</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>

</project>

When you add new Java classes under core, you need to modify a POM file to successfully build the OSGi bundle. You modify the POM file located at C:\AdobeCQ\TwoFactorAEM\core.

The following code represents this POM file.

<?xml version="1.0" encoding="UTF-8"?>
<!--
 |  Copyright 2017 Adobe Systems Incorporated
 |
 |  Licensed 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.
-->
<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>
    <parent>
        <groupId>TwoFactorAEM</groupId>
        <artifactId>TwoFactorAEM</artifactId>
        <version>1.0-SNAPSHOT</version>
        <relativePath>../pom.xml</relativePath>
    </parent>
    <artifactId>TwoFactorAEM.core</artifactId>
    <packaging>bundle</packaging>
    <name>TwoFactorAEM - Core</name>
    <description>Core bundle for TwoFactorAEM</description>
    <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.sling</groupId>
                <artifactId>maven-sling-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.felix</groupId>
                <artifactId>maven-bundle-plugin</artifactId>
                <extensions>true</extensions>
                <configuration>
                    <instructions>
                        <!-- Import any version of javax.inject, to allow running on multiple versions of AEM -->
                        <Import-Package>javax.inject;version=0.0.0,*</Import-Package>
                        <Sling-Model-Packages>
                            com.aem.authen.core
                        </Sling-Model-Packages>
                    </instructions>
                </configuration>
            </plugin>
        </plugins>
    </build>
 
    <dependencies>
		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>servlet-api</artifactId>
		</dependency>

		<dependency>
			<groupId>commons-io</groupId>
			<artifactId>commons-io</artifactId>
			<version>2.4</version>
		</dependency>

		<dependency>
			<groupId>javax.inject</groupId>
			<artifactId>javax.inject</artifactId>
		</dependency>

		<dependency>
			<groupId>org.osgi</groupId>
			<artifactId>org.osgi.service.component.annotations</artifactId>
		</dependency>

		<dependency>
			<groupId>org.osgi</groupId>
			<artifactId>org.osgi.service.metatype.annotations</artifactId>
		</dependency>

		<dependency>
			<groupId>org.osgi</groupId>
			<artifactId>org.osgi.annotation</artifactId>
		</dependency>

		<dependency>
			<groupId>com.google.code.findbugs</groupId>
			<artifactId>jsr305</artifactId>
			<version>2.0.0</version>
			<scope>provided</scope>
		</dependency>

		<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-lang3</artifactId>
			<version>3.0</version>
			<scope>provided</scope>
		</dependency>


		<!-- OSGi Dependencies -->
		<dependency>
			<groupId>org.apache.felix</groupId>
			<artifactId>org.apache.felix.scr</artifactId>
		</dependency>
		<dependency>
			<groupId>org.apache.felix</groupId>
			<artifactId>org.apache.felix.scr.annotations</artifactId>
		</dependency>
		<dependency>
			<groupId>biz.aQute</groupId>
			<artifactId>bndlib</artifactId>
		</dependency>
		<dependency>
			<groupId>org.osgi</groupId>
			<artifactId>org.osgi.core</artifactId>
		</dependency>
		<!-- Other Dependencies -->
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-api</artifactId>
		</dependency>
        <dependency>
            <groupId>org.osgi</groupId>
            <artifactId>org.osgi.service.component</artifactId>
        </dependency>
		<dependency>
			<groupId>com.adobe.aem</groupId>
			<artifactId>uber-jar</artifactId>
			<classifier>apis</classifier>
		</dependency>

		<dependency>
			<groupId>org.apache.sling</groupId>
			<artifactId>org.apache.sling.api</artifactId>
			<version>2.16.2</version>
			<scope>provided</scope>
		</dependency>

		<dependency>
			<groupId>org.apache.sling</groupId>
			<artifactId>org.apache.sling.jcr.jcr-wrapper</artifactId>
			<version>2.0.0</version>
			<scope>provided</scope>
		</dependency>


		<!-- Core Components -->
		<dependency>
			<groupId>com.adobe.cq</groupId>
			<artifactId>core.wcm.components.core</artifactId>
		</dependency>

		<dependency>
			<groupId>com.adobe.acs</groupId>
			<artifactId>acs-aem-commons-bundle</artifactId>
		</dependency>

		<dependency>
			<groupId>org.apache.httpcomponents</groupId>
			<artifactId>httpmime</artifactId>
			<version>4.5.1</version>
		</dependency>


		<dependency>
			<groupId>commons-httpclient</groupId>
			<artifactId>commons-httpclient</artifactId>
			<version>3.1</version>
			<scope>provided</scope>
		</dependency>

		<dependency>
			<groupId>org.apache.httpcomponents</groupId>
			<artifactId>httpclient</artifactId>
			<version>4.5.1</version>
		</dependency>

		<dependency>
			<groupId>commons-codec</groupId>
			<artifactId>commons-codec</artifactId>
			<version>1.6</version>
			<scope>provided</scope>
		</dependency>

	</dependencies>
</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\TwoFactorAEM.
  2. Run the following maven command: mvn -PautoInstallPackage install.
  3. The OSGi component can be found in the following folder: C:\AdobeCQ\TwoFactorAEM\core\target. The file name of the OSGi component is TwoFactorAEM.core-1.0-SNAPSHOT.jar.

The command -PautoInstallPackage automatically deploys the OSGi bundle to AEM.

After the OSGi bundle is deployed, notice that you can see it under Sling Authentication (http://localhost:4502/system/console/slingauth), as shown here.

Handler
The Authentication Handler, as defined by @ServiceDescription("Google 2F Authentication Handler")

Install Google Authenticator

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

googleAuthen
Google Authenticator

Note:

To synchronize Experience Manager with Google Authenticator, you use the Bar Code Scan produced by the QRCode servlet, which is explained later in this article. 

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

Note:

If you install the package that is shown at the start of this article, you can skip this step

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 Experience Manager 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. 

Note:

If you install the package at the start, you do not need to modify these files.  

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 Experience Manager login page (as shown in the illustration shown at the beginning of this article).

The following code represents the modified file. 

<%--
 
ADOBE CONFIDENTIAL
__________________
 
Copyright 2015 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.List,
                  java.util.Iterator,
                  java.util.Arrays,
                  java.util.Locale,
                  java.util.ResourceBundle,
                  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.SlingHttpServletRequest,
                  org.apache.sling.api.resource.ValueMap,
                  com.adobe.granite.xss.XSSAPI,
                  com.day.cq.i18n.I18n,
                  com.adobe.granite.ui.clientlibs.HtmlLibrary,
                  com.adobe.granite.ui.clientlibs.HtmlLibraryManager,
                  com.adobe.granite.ui.clientlibs.LibraryType,
                  com.adobe.granite.license.ProductInfoProvider,
                  com.adobe.granite.auth.ims.ImsConfigProvider,
                  com.adobe.granite.security.user.UserManagementService,
                  org.apache.sling.auth.core.AuthUtil,
                  org.apache.sling.auth.core.AuthConstants,
                  java.util.Calendar"%><%
%><%@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" %><%--
login
=====
 
    The component to render the login screen.
 
    It has the following content structure:
 
   /**
    * The HTML title.
    * Defaults to "Adobe Experience Cloud".
    */
    - title (String)
 
 
   /**
    * The favicon.
    * Defaults to "login/adobe-logo.png".
    */
    - favicon (String)
 
 
   /**
    * The title in the box.
    * Defaults to "Welcome to Adobe Experience Cloud".
    */
    - box/title (String)
 
 
   /**
    * The text in the box.
    * Defaults to "All the tools you need to solve these complex digital business challenges.".
    */
    - box/text (String)
 
 
   /**
    * The text of the learn more link. The link is following the text.
    * Defaults to "Learn More".
    */
    - /box/learnMore/text (String)
 
 
   /**
    * The href of the learn more link.
    * Defaults to "#".
    */
    - /box/learnMore/link (String)
 
 
   /**
    * Enables autocomplete for fields username and password.
    * Defaults to "false".
    */
    - box/autocomplete (Boolean)
 
 
   /**
    * The title of the login form. Note that this title is not shown in browsers that display field labels instead of
    * placeholders (IE8 and older).
    * Defaults to "Sign In".
    */
    - box/formTitle (String)
 
 
   /**
    * The title of the change password form. Note that this title is not shown in browsers that display field labels instead of
    * placeholders (IE8 and older).
    * Defaults to "Change Password".
    */
    - box/changePasswordTitle (String)
 
 
   /**
    * The placeholder of the user field.
    * Defaults to "User name".
    */
    - box/userPlaceholder (String)
 
 
   /**
    * The placeholder of the password field in the login form.
    * Defaults to "Password".
    */
    - box/passwordPlaceholder (String)
 
 
   /**
    * The placeholder of the password field in the change password form.
    * Defaults to "Old password".
    */
    - box/oldPasswordPlaceholder (String)
 
 
   /**
    * The placeholder of the new password field.
    * Defaults to "New password".
    */
    - box/newPasswordPlaceholder (String)
 
 
   /**
    * The placeholder of the confirm password field.
    * Defaults to "Confirm new password".
    */
    - box/confirmPasswordPlaceholder (String)
 
 
   /**
    * The text of the submit button in the login form.
    * Defaults to "Sign In".
    */
    - box/submitText (String)
 
 
   /**
    * The text of the submit button in the change password form.
    * Defaults to "Submit".
    */
    - box/changePasswordSubmitText (String)
 
 
   /**
    * The text of the back button.
    * Defaults to "Back".
    */
    - box/backText (String)
 
 
   /**
    * The error message displayed when login fails.
    * Defaults to "User name and password do not match".
    */
    - box/invalidLoginText (String)
 
 
   /**
    * The error message displayed when the session timed out.
    * Defaults to "Session timed out, please login again".
    */
    - box/sessionTimedOutText (String)
 
 
   /**
    * The error message displayed when the password is expired.
    * Defaults to "Your password has expired".
    */
    - box/loginExpiredText (String)
 
   /**
    * The error message displayed when the password is expired and the newly chosen password is in the password history.
    * Defaults to "New password is in password history".
    */
    - box/loginInHistoryText (String)
 
   /**
    * The error message displayed when the new and confirm passwords do not match.
    * Defaults to "New passwords do not match".
    */
    - box/passwordsDoNotMatchText (String)
 
 
   /**
    * The error message displayed when the new password is blank.
    * Defaults to "New password must not be blank".
    */
    - box/passwordEmptyText (String)
 
 
    /**
     * The title of the success modal.
     * Defaults to "Password Changed"
     */
     - changePasswordSuccessTitle
 
 
    /**
     * The text of the success modal.
     * Defaults to "Your password has been changed successfully."
     */
     - changePasswordSuccessText
 
 
   /**
    * The items on the left side of the footer.
    * Default items are "Help", "Term of Use" and "Privacy Policy and Cookies".
    */
    - footer/items (String)
 
 
   /**
    * The copyright on the right side of the footer.
    * Defaults to "© 2014 Adobe Systems Incorporated. All Rights Reserved.".
    */
    - footer/copy/text (String)
 
 
--%><%!
 
    static final String PARAM_NAME_REASON = "j_reason";
 
    static final String REASON_KEY_INVALID_LOGIN = "invalid_login";
    static final String REASON_KEY_SESSION_TIMED_OUT = "session_timed_out";
    static final String REASON_KEY_INVALID_OTP = "invalid_otp";
 
    static final String DEFAULT_AUTH_URL_SUFFIX  = "/j_security_check";
 
    static final String ERROR_SELECTOR = "error";
    static final String CHANGE_PWD_SELECTOR = "changepassword";
 
    String imsLoginUrl = null;
 
    private String printProperty(ValueMap cfg, I18n i18n, XSSAPI xssAPI, String name, String defaultText) {
        String text = getText(cfg, i18n, name, defaultText);
        return xssAPI.encodeForHTML(text);
    }
 
    private String printAttribute(ValueMap cfg, I18n i18n, XSSAPI xssAPI, String name, String defaultText) {
        String text = getText(cfg, i18n, name, defaultText);
        return xssAPI.encodeForHTMLAttr(text);
    }
 
    private String getText(ValueMap cfg, I18n i18n, String name, String defaultText) {
        String text = cfg.get(name, String.class);
        return 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.
     */
    private 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;
    }
 
    /**
     * Returns a URL suffix which ensures that the request is handled by {@link org.apache.sling.auth.core.impl.SlingAuthenticator}
     * If no custom suffices are found, this method returns <code>DEFAULT_AUTH_URL_SUFFIX</code>
     *
     * @return a URL suffix which will ensure that the URL is handled by the authenticator.
     */
    private String getAuthURLSuffix(SlingHttpServletRequest req) {
        final Object authUriSufficesObj = req.getAttribute(AuthConstants.ATTR_REQUEST_AUTH_URI_SUFFIX);
        if (authUriSufficesObj instanceof String[]) {
            final String[] authUriSuffices = (String[]) authUriSufficesObj;
            if (authUriSuffices.length > 0) {
                // Any suffix from this array would be valid. Return the first.
                return authUriSuffices[0];
            }
        }
        return DEFAULT_AUTH_URL_SUFFIX;
    }
 
%><sling:defineObjects /><%
 
    final Resource configs = getConfigRoot(resource);
 
    final String browserAcceptLang = request.getHeader("Accept-Language");
    String browserLang;
    String browserRegion;
    Locale browserLocale = null;
 
    if (browserAcceptLang != null) {
        if (browserAcceptLang.matches("^[a-zA-Z][a-zA-Z](-|_)[a-zA-Z][a-zA-Z].*")) {
            browserLang = browserAcceptLang.substring(0,2);
            browserRegion = browserAcceptLang.substring(3,5);
            browserLocale = new Locale(browserLang, browserRegion);
        } else if (browserAcceptLang.matches("^[a-zA-Z][a-zA-Z].*")) {
            browserLang = browserAcceptLang.substring(0,2);
            browserLocale = new Locale(browserLang);
        }
    }
 
    final I18n i18n;
    if (browserLocale != null) {
        ResourceBundle browserLocaleBundle = slingRequest.getResourceBundle(browserLocale);
        i18n = new I18n(browserLocaleBundle);
    } else {
        i18n = new I18n(slingRequest);
    }
 
    final XSSAPI xssAPI = sling.getService(XSSAPI.class).getRequestSpecificAPI(slingRequest);
    final UserManagementService userManagementService = sling.getService(UserManagementService.class);
    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 and password do not match")));
    validReasons.put(REASON_KEY_SESSION_TIMED_OUT, printProperty(cfg, i18n, xssAPI, "box/sessionTimedOutText", i18n.get("Session timed out, please login again")));
    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")));
// load custom error types
    Resource errors = resource.getChild("errors");
    if (errors != null) {
        for (Iterator<Resource> customErrors = errors.listChildren() ; customErrors.hasNext() ; ) {
            Resource customError = customErrors.next();
            validReasons.put(customError.getName(), printProperty(customError.adaptTo(ValueMap.class), i18n, xssAPI, "/text", i18n.get("Error")));
        }
    }
 
    String reason = request.getParameter(PARAM_NAME_REASON) != null
            ? request.getParameter(PARAM_NAME_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 = "";
        }
    }
 
    List<String> selectors = Arrays.asList(slingRequest.getRequestPathInfo().getSelectors());
 
    boolean isLogin = ! selectors.contains(CHANGE_PWD_SELECTOR);
    boolean isError = selectors.contains(ERROR_SELECTOR);
 
%><!DOCTYPE html>
<html>
<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" />
 
    <% ImsConfigProvider imsConfigProvider = sling.getService(ImsConfigProvider.class);
        if (imsConfigProvider != null) {
            imsLoginUrl = imsConfigProvider.getImsLoginUrl();
            %><meta name="granite.login.imsLoginUrl" content="<%= xssAPI.getValidHref(imsLoginUrl) %>"><%
        }
    %>
 
 
    <title><%= printProperty(cfg, i18n, xssAPI, "title", i18n.get("Adobe Experience 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 categories="coralui3" />
    <%
        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"/>
    <ui:includeClientLib css="granite.core.login.extension"/>
</head>
<body class="coral--light">
<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() + getAuthURLSuffix(slingRequest);
 
        if (authType == null || user == null || userManagementService.getAnonymousId().equals(user)) {
 
    %><div id="login-box" class="coral--dark">
        <div id="leftbox" class="box">
            <div class="header">
                <h1 class="coral-Heading coral-Heading--1"><%= printProperty(cfg, i18n, xssAPI, "box/title", i18n.get("Welcome to Adobe Experience Cloud")) %></h1>
            </div>
            <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>
 
        <%-- If IMS is provided we render the choice --%>
        <% if (imsLoginUrl != null) { %>
            <div id="rightbox" class="box">
                <coral-accordion variant="quiet">
                    <coral-accordion-item selected>
                        <coral-accordion-item-label><%= xssAPI.encodeForHTML(i18n.get("Sign in with Adobe")) %></coral-accordion-item-label>
                        <coral-accordion-item-content>
                            <div class="coral-Form coral-Form--vertical">
                                <div class="coral-Form-fieldwrapper">
                                    <input is="coral-textfield" aria-label="<%= xssAPI.encodeForHTMLAttr(i18n.get("Email address")) %>" class="coral-Form-field" id="adobeId" name="j_usernameIMS" type="text" autofocus="autofocus" pattern=".*" placeholder="<%= xssAPI.encodeForHTMLAttr(i18n.get("Email address")) %>" spellcheck="false">
                                </div>
                                <button is="coral-button" id="submit-button-ims" variant="primary" type="submit"><%= xssAPI.encodeForHTML(i18n.get("Sign In")) %></button>
                            </div>
                        </coral-accordion-item-content>
                    </coral-accordion-item>
                    <coral-accordion-item>
                        <coral-accordion-item-label><%= xssAPI.encodeForHTML(i18n.get("Sign in locally (admin tasks only)")) %></coral-accordion-item-label>
                        <coral-accordion-item-content>
                            <% if (isError && reason.length() > 0) { %>
                            <p><%= xssAPI.encodeForHTML(i18n.get("Please contact your administrator or try again later.")) %></p>
                            <coral-alert variant="error">
                                <coral-alert-content><%= reason %></coral-alert-content>
                            </coral-alert>
                            <% } else { %>
                            <% 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" id="resource" value="<%= xssAPI.encodeForHTMLAttr(redirect) %>">
                                <%
                                    String loginTitle = printProperty(cfg, i18n, xssAPI, "box/formTitle", i18n.get("Sign In"));
                                    String changeTitle = printProperty(cfg, i18n, xssAPI, "box/changePasswordTitle", i18n.get("Change Password"));
                                    String loginSubmitText = printProperty(cfg, i18n, xssAPI, "box/submitText", i18n.get("Sign In"));
                                    String changeSubmitText = printProperty(cfg, i18n, xssAPI, "box/changePasswordSubmitText", i18n.get("Submit"));
                                    String userPlaceholder = printAttribute(cfg, i18n, xssAPI, "box/userPlaceholder", i18n.get("User name"));
                                    String loginPasswordPlaceholder = printAttribute(cfg, i18n, xssAPI, "box/passwordPlaceholder", i18n.get("Password"));
                                    String changePasswordPlaceholder = printAttribute(cfg, i18n, xssAPI, "box/oldPasswordPlaceholder", i18n.get("Old password"));
                                    String newPasswordPlaceholder = printAttribute(cfg, i18n, xssAPI, "box/newPasswordPlaceholder", i18n.get("New password"));
                                    String confirmPasswordPlaceholder = printAttribute(cfg, i18n, xssAPI, "box/confirmPasswordPlaceholder", i18n.get("Confirm new password"));
                                %>
                                <p id="sign-in-title"><%= isLogin ? loginTitle : changeTitle %></p>
                                <div class="coral-Form-fieldwrapper">
                                    <input is="coral-textfield" aria-label="<%= userPlaceholder %>" class="coral-Form-field" id="username" name="j_username" type="text" autofocus="autofocus" pattern=".*" placeholder="<%= userPlaceholder %>" spellcheck="false" autocomplete="<%= autocomplete %>">
                                </div>
                                <div class="coral-Form-fieldwrapper">
                                    <input is="coral-textfield" aria-label="<%= isLogin ? loginPasswordPlaceholder : changePasswordPlaceholder %>" class="coral-Form-field" id="password" name="j_password" type="password"  placeholder="<%= isLogin ? loginPasswordPlaceholder : changePasswordPlaceholder %>" spellcheck="false" autocomplete="<%= autocomplete %>">
                                </div>
                                <div class="coral-Form-fieldwrapper">
                                    <input is="coral-textfield" aria-label="<%= newPasswordPlaceholder %>" class="coral-Form-field" id="new_password" name="<%= isLogin ? "" : "j_newpassword" %>" type="password"  placeholder="<%= newPasswordPlaceholder %>" spellcheck="false" autocomplete="false" <%= isLogin ? "hidden" : "" %>>
                                </div>
                                <div class="coral-Form-fieldwrapper">
                                    <input is="coral-textfield" aria-label="<%= confirmPasswordPlaceholder %>" class="coral-Form-field" id="confirm_password" name="" type="password"  placeholder="<%= confirmPasswordPlaceholder %>" spellcheck="false" autocomplete="false" <%= isLogin ? "hidden" : "" %>>
                                </div>
                                <coral-alert id="error" variant="error" <%= reason.length() > 0 ? "" : "hidden" %>>
                                    <coral-alert-content><%= reason %></coral-alert-content>
                                </coral-alert>
                                <button is="coral-button" id="submit-button" variant="primary" type="submit"><%= isLogin ? loginSubmitText : changeSubmitText %></button>
                                <button is="coral-button" id="back-button" hidden><%= printProperty(cfg, i18n, xssAPI, "box/backText", i18n.get("Back")) %></button>
                            </form>
                            <input id="login_title" type="hidden" value="<%= loginTitle %>">
                            <input id="change_title" type="hidden" value="<%= changeTitle %>">
                            <input id="login_password_placeholder" type="hidden" value="<%= loginPasswordPlaceholder %>">
                            <input id="change_password_placeholder" type="hidden" value="<%= changePasswordPlaceholder %>">
                            <input id="login_submit_text" type="hidden" value="<%= loginSubmitText %>">
                            <input id="change_submit_text" type="hidden" value="<%= changeSubmitText %>">
                            <input id="invalid_message" type="hidden" value="<%= validReasons.get(REASON_KEY_INVALID_LOGIN) %>"/>
                            <input id="expired_message" type="hidden" value="<%= printProperty(cfg, i18n, xssAPI, "box/loginExpiredText", i18n.get("Your password has expired")) %>"/>
                            <input id="in_history_message" type="hidden" value="<%= printProperty(cfg, i18n, xssAPI, "box/loginInHistoryText", i18n.get("New password was found in password history")) %>"/>
                            <input id="not_match_message" type="hidden" value="<%= printProperty(cfg, i18n, xssAPI, "box/passwordsDoNotMatchText", i18n.get("New passwords do not match")) %>"/>
                            <input id="empty_message" type="hidden" value="<%= printProperty(cfg, i18n, xssAPI, "box/passwordEmptyText", i18n.get("New password must not be blank")) %>"/>
                            <% } %>
                        </coral-accordion-item-content>
                    </coral-accordion-item>
                </coral-accordion>
            </div>
        <%-- else render standard local login --%>
        <% } else { %>
            <div id="rightbox" class="box">
                <% if (isError && reason.length() > 0) { %>
                <p><%= xssAPI.encodeForHTML(i18n.get("Please contact your administrator or try again later.")) %></p>
                <coral-alert variant="error">
                    <coral-alert-content><%= reason %></coral-alert-content>
                </coral-alert>
                <% } else { %>
                <% 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" id="resource" value="<%= xssAPI.encodeForHTMLAttr(redirect) %>">
                    <%
                        String loginTitle = printProperty(cfg, i18n, xssAPI, "box/formTitle", i18n.get("Sign In"));
                        String changeTitle = printProperty(cfg, i18n, xssAPI, "box/changePasswordTitle", i18n.get("Change Password"));
                        String loginSubmitText = printProperty(cfg, i18n, xssAPI, "box/submitText", i18n.get("Sign In"));
                        String changeSubmitText = printProperty(cfg, i18n, xssAPI, "box/changePasswordSubmitText", i18n.get("Submit"));
                        String userPlaceholder = printAttribute(cfg, i18n, xssAPI, "box/userPlaceholder", i18n.get("User name"));
                        String loginPasswordPlaceholder = printAttribute(cfg, i18n, xssAPI, "box/passwordPlaceholder", i18n.get("Password"));
                        String changePasswordPlaceholder = printAttribute(cfg, i18n, xssAPI, "box/oldPasswordPlaceholder", i18n.get("Old password"));
                        String newPasswordPlaceholder = printAttribute(cfg, i18n, xssAPI, "box/newPasswordPlaceholder", i18n.get("New password"));
                        String confirmPasswordPlaceholder = printAttribute(cfg, i18n, xssAPI, "box/confirmPasswordPlaceholder", i18n.get("Confirm new password"));
                    %>
                    <p id="sign-in-title"><%= isLogin ? loginTitle : changeTitle %></p>
                    <div class="coral-Form-fieldwrapper">
                        <input is="coral-textfield" aria-label="<%= userPlaceholder %>" class="coral-Form-field" id="username" name="j_username" type="text" autofocus="autofocus" pattern=".*" placeholder="<%= userPlaceholder %>" spellcheck="false" autocomplete="<%= autocomplete %>">
                    </div>
                    <div class="coral-Form-fieldwrapper">
                        <input is="coral-textfield" aria-label="<%= isLogin ? loginPasswordPlaceholder : changePasswordPlaceholder %>" class="coral-Form-field" id="password" name="j_password" type="password"  placeholder="<%= isLogin ? loginPasswordPlaceholder : changePasswordPlaceholder %>" spellcheck="false" autocomplete="<%= autocomplete %>">
                    </div>
                    <div class="coral-Form-fieldwrapper">
                        <input is="coral-textfield" aria-label="<%= isLogin ? loginPasswordPlaceholder : changePasswordPlaceholder %>" class="coral-Form-field" id="otpcode" name="j_otpcode" type="password" placeholder="OTP Token" spellcheck="false" autocomplete="<%= autocomplete %>">
                    </div>
 
                    <div class="coral-Form-fieldwrapper">
                        <input is="coral-textfield" aria-label="<%= newPasswordPlaceholder %>" class="coral-Form-field" id="new_password" name="<%= isLogin ? "" : "j_newpassword" %>" type="password"  placeholder="<%= newPasswordPlaceholder %>" spellcheck="false" autocomplete="false" <%= isLogin ? "hidden" : "" %>>
                    </div>
 
                    <div class="coral-Form-fieldwrapper">
                        <input is="coral-textfield" aria-label="<%= confirmPasswordPlaceholder %>" class="coral-Form-field" id="confirm_password" name="" type="password"  placeholder="<%= confirmPasswordPlaceholder %>" spellcheck="false" autocomplete="false" <%= isLogin ? "hidden" : "" %>>
                    </div>
                    <coral-alert id="error" variant="error" <%= reason.length() > 0 ? "" : "hidden" %>>
                        <coral-alert-content><%= reason %></coral-alert-content>
                    </coral-alert>
                    <button is="coral-button" id="submit-button" variant="primary" type="submit"><%= isLogin ? loginSubmitText : changeSubmitText %></button>
                    <button is="coral-button" id="back-button" hidden><%= printProperty(cfg, i18n, xssAPI, "box/backText", i18n.get("Back")) %></button>
                </form>
                <input id="login_title" type="hidden" value="<%= loginTitle %>">
                <input id="change_title" type="hidden" value="<%= changeTitle %>">
                <input id="login_password_placeholder" type="hidden" value="<%= loginPasswordPlaceholder %>">
                <input id="change_password_placeholder" type="hidden" value="<%= changePasswordPlaceholder %>">
                <input id="login_submit_text" type="hidden" value="<%= loginSubmitText %>">
                <input id="change_submit_text" type="hidden" value="<%= changeSubmitText %>">
                <input id="invalid_message" type="hidden" value="<%= validReasons.get(REASON_KEY_INVALID_LOGIN) %>"/>
                <input id="expired_message" type="hidden" value="<%= printProperty(cfg, i18n, xssAPI, "box/loginExpiredText", i18n.get("Your password has expired")) %>"/>
                <input id="in_history_message" type="hidden" value="<%= printProperty(cfg, i18n, xssAPI, "box/loginInHistoryText", i18n.get("New password was found in password history")) %>"/>
                <input id="not_match_message" type="hidden" value="<%= printProperty(cfg, i18n, xssAPI, "box/passwordsDoNotMatchText", i18n.get("New passwords do not match")) %>"/>
                <input id="empty_message" type="hidden" value="<%= printProperty(cfg, i18n, xssAPI, "box/passwordEmptyText", i18n.get("New password must not be blank")) %>"/>
                <% } %>
            </div>
        <% } %>
    </div>
    <div id="push"></div>
</div>
<div id="footer">
    <div class="legal-footer"><%
        // Footer: default copyright (removable)
        if (cfg.containsKey("footer/copy/text")) {
            ProductInfoProvider productInfoProvider = sling.getService(ProductInfoProvider.class);
            String year = productInfoProvider == null ? null : productInfoProvider.getProductInfo().getYear();
            if (year == null) {
                year = String.valueOf(Calendar.getInstance().get(Calendar.YEAR));
            }
            String text = cfg.get("footer/copy/text","");
            %><span><%= xssAPI.encodeForHTML(i18n.getVar(text, "{0} is the product year", year)) %></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>
 
 
<%
String modalTitle = printProperty(cfg, i18n, xssAPI, "changePasswordSuccessTitle", i18n.get("Password Changed"));
%>
<coral-dialog id="success-dialog" variant="success" closable="true">
    <coral-dialog-header><%= modalTitle %></coral-dialog-header>
    <coral-dialog-content>
        <%= printProperty(cfg, i18n, xssAPI, "changePasswordSuccessText", i18n.get("Your password has been changed successfully.")) %>
    </coral-dialog-content>
    <coral-dialog-footer>
        <button is="coral-button" variant="primary" coral-close><%= i18n.get("Ok") %></button>
    </coral-dialog-footer>
</coral-dialog>
 
<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.
 */
console.log("test");
jQuery(function($){
 
    // disable 403 handler in granite
    $.ajaxSetup({
        statusCode: {
            403: $.noop
        }
    });
 
    function flushError() {
        // adds the class to hide the alert
        $('#error').attr('hidden');
        // clears the text
        $('#error > .coral-Alert-content').text('');
    }
 
    function displayError(message) {
        // removes the hidden class to show the component
        $('#error').removeAttr('hidden');
        // adds the text inside the coral-Alert-message
        $('#error > .coral3-Alert-content').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 user

The final task is to login to Experience Manager (http://localhost:4502) with Admin credentials (you can add the new user to the admin group), and create a test account (for example, malcolm). Next, login with your malcolm user and password. Do not need to enter the OTP yet. 

After you login, go to user preferences by clicking the user icon in the upper right hand corner. Click My Preferences. 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. Notice that the values are stored under the user node. For example, look at this illustration. 

 

twostepProps
The twostep props under a user node

This is the property that determines where two-factor authentication is used for a given user. If user, the secret key is persisted under the profile node, as shown here. 

twostepPropsA
Location of the key

The following video shows the two-factor authentication succcessfully working.  


See also

Congratulations, you have just created an AEM OSGi bundle that support two-factor authentication. Please refer to the Experience League 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