Nowadays 2-step user authentication is getting more and more mainstream. Take as an example a Google account where you have to verify your identity by providing special code that is sent to you using the phone number bound to your account. So to get into your mailbox for example you have to pass 2-step authorization process – the first step is to provide your username and password followed by the code you received via SMS.
You can easily implement 2-step Authentication with WSO2 Identity Server. Being an flexible and extendable authentication framework it provides enough facilities to add extra steps to the authentication process so it fits your specific needs.
Let’s assume the following use case: A sample application utilizes WSO2 Identity Server as Single Sign-On provider. WSO2 Identity server in its turn is going to use 2-step authentication process. In addition to the usual username and password the user is obliged to supply a code he receives over email in order to get authenticated.
To achieve that we’re going to introduce a custom authenticator as well as configure custom authentication flow for SSO. Let’s start with creation of a custom authenticator:
WSO2 Authentication Framework provides an abstract class to extend in order to implement custom authenticators. org.wso2.carbon.identity.application.authentication.framework. framework.AbstractApplicationAuthenticator is the abstract class provided by the framework we’re going to extend in order to implement custom authenticator. Please pay attention to the fact that you also need to implement one of the interfaces to explicitly tell the framework which type of authenticator it is:
- org.wso2.carbon.identity.application.authentication.framework.LocalApplicationAuthenticator – this interface standsfor local authenticators used in the Identity Server
- org.wso2.carbon.identity.application.authentication.framework. FederatedApplicationAuthenticator – this interface stands for federated authenticators for example Google or Facebook or even 3-rd party federated authenticators
- org.wso2.carbon.identity.application.authentication.framework.RequestPathApplicationAuthenticator – interface to implement in case you want customize request path authenticators
In our case we’re going to implement LocalApplicationAuthenticator interface to use it in an additional step of the user verification. The example is simplified down to some hardcoded values which in real life should be replace by real steps like sending a real email with generated verification code etc.
The authenticator implementation looks like following:
package com.yenlo.identity.application.authenticator.custom;
importcom.yenlo.identity.application.authenticator.custom.internal.YenloCustomAuthenticatorConstants;
importcom.yenlo.identity.application.authenticator.custom.internal.YenloCustomAuthenticatorEmailSender;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
importorg.wso2.carbon.identity.application.authentication.framework.AbstractApplicationAuthenticator;
importorg.wso2.carbon.identity.application.authentication.framework.AuthenticatorFlowStatus;
importorg.wso2.carbon.identity.application.authentication.framework.LocalApplicationAuthenticator;
importorg.wso2.carbon.identity.application.authentication.framework.config.ConfigurationFacade;
importorg.wso2.carbon.identity.application.authentication.framework.context.AuthenticationContext;
importorg.wso2.carbon.identity.application.authentication.framework.exception.AuthenticationFailedException;
importorg.wso2.carbon.identity.application.authentication.framework.exception.InvalidCredentialsException;
importorg.wso2.carbon.identity.application.authentication.framework.exception.LogoutFailedException;
importorg.wso2.carbon.identity.application.authentication.framework.util.FrameworkUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class YenloCustomAuthenticator extendsAbstractApplicationAuthenticator implementsLocalApplicationAuthenticator {
private static Log log = LogFactory.getLog(YenloCustomAuthenticator.class);
public static final String CONFIRMATION_CODE = "1234";
@Override
public boolean canHandle(HttpServletRequest request) {
String confirmationCode = request.getParameter("confirmationCode");
return confirmationCode != null;
}
@Override
public AuthenticatorFlowStatus process(HttpServletRequest request,
HttpServletResponse response, AuthenticationContext context)
throws AuthenticationFailedException, LogoutFailedException {
if (context.isLogoutRequest()) {
return AuthenticatorFlowStatus.SUCCESS_COMPLETED;
} else {
return super.process(request, response, context);
}
}
@Override
protected void initiateAuthenticationRequest(HttpServletRequest request,
HttpServletResponse response, AuthenticationContext context)
throws AuthenticationFailedException {
String loginPage = ConfigurationFacade.getInstance().getAuthenticationEndpointURL();
String queryParams = FrameworkUtils
.getQueryStringWithFrameworkContextId(context.getQueryParams(),
context.getCallerSessionKey(),
context.getContextIdentifier());
try {
YenloCustomAuthenticatorEmailSender.sendEmail(request.getParameter("username"), CONFIRMATION_CODE);
String retryParam = "";
if (context.isRetrying()) {
retryParam ="&authFailure=true&authFailureMsg=login.fail.message";
}
response.sendRedirect(response.encodeRedirectURL(loginPage + ("?" + queryParams))
+ "&authenticators=" + getName() + ":" + "LOCAL" + retryParam);
} catch (IOException e) {
throw new AuthenticationFailedException(e.getMessage(), e);
}
}
@Override
protected void processAuthenticationResponse(HttpServletRequest request,
HttpServletResponse response, AuthenticationContext context)
throws AuthenticationFailedException {
String confirmationCode = request.getParameter("confirmationCode");
boolean isAuthenticated = false;
if (confirmationCode != null) {
isAuthenticated =CONFIRMATION_CODE.equals(confirmationCode);
} else {
throw new AuthenticationFailedException("Can not confirm authorization code.");
}
if (!isAuthenticated) {
if (log.isDebugEnabled()) {
log.debug("user authentication failed due to invalid credentials.");
}
throw new InvalidCredentialsException();
}
elsecontext.setSubject(context.getCurrentAuthenticatedIdPs().get("LOCAL").getUsername());
}
@Override
protected boolean retryAuthenticationEnabled() {
return true;
}
@Override
public String getContextIdentifier(HttpServletRequest request) {
return request.getParameter("sessionDataKey");
}
@Override
public String getFriendlyName() {
returnYenloCustomAuthenticatorConstants.AUTHENTICATOR_FRIENDLY_NAME;
}
@Override
public String getName() {
return YenloCustomAuthenticatorConstants.AUTHENTICATOR_NAME;
}
}
Authenticator Constants. Please pay attention to the AUTHENTICATOR_FRIENTLY_NAME– the value of this constant is going to be used further down in order to configure custom authenticator:
package
com.yenlo.identity.application.authenticator.custom.internal;
/**
* Created by vklevko on 20-7-2015.
*/
public class YenloCustomAuthenticatorConstants {
public static final String AUTHENTICATOR_NAME ="YenloCustomAuthenticator";
public static final String AUTHENTICATOR_FRIENDLY_NAME ="YenloCustomAuthenticator";
}
To make the new component to register itself during Identity Server startup we need to introduce component activator class, that is going to register the authenticator inside OSGi runtime so it becomes available for future use:
package com.yenlo.identity.application.authenticator.custom.internal;
import
com.yenlo.identity.application.authenticator.custom.YenloCustomAuthenticator;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.osgi.service.component.ComponentContext;
importorg.wso2.carbon.identity.application.authentication.framework.ApplicationAuthenticator;
import org.wso2.carbon.user.core.service.RealmService;
import java.util.Hashtable;
/**
* @scr.componentname="com.yenlo.identity.application.authenticator.custom.component" immediate="true"
* @scr.reference name="realm.service"
* interface="org.wso2.carbon.user.core.service.RealmService"cardinality="1..1"
* policy="dynamic" bind="setRealmService" unbind="unsetRealmService"
*/
public class YenloCustomAuthenticatorComponent {
private static Log log = LogFactory.getLog(YenloCustomAuthenticatorComponent.class);
private static RealmService realmService;
protected void activate(ComponentContext ctxt) {
YenloCustomAuthenticator customAuth = newYenloCustomAuthenticator();
Hashtable<String, String> props = new Hashtable<String, String>();
ctxt.getBundleContext().registerService(ApplicationAuthenticator.class.getName(), customAuth, props);
if (log.isDebugEnabled()) {
log.info("YenloCustomAuthenticator bundle is activated");
}
}
protected void deactivate(ComponentContext ctxt) {
if (log.isDebugEnabled()) {
log.info("YenloCustomAuthenticator bundle is deactivated");
}
}
protected void setRealmService(RealmService realmService) {
log.debug("Setting the Realm Service");
YenloCustomAuthenticatorComponent.realmService = realmService;
}
protected void unsetRealmService(RealmService realmService) {
log.debug("UnSetting the Realm Service");
YenloCustomAuthenticatorComponent.realmService = null;
}
public static RealmService getRealmService() {
return realmService;
}
}
The implementation also contains utility class to send out email with the predefined code. The full source is available in the Yenlo Git Public Repository here:https://github.com/Yenlo/wso2-two-step-verification
Once you clone and build the sources drop the resulting JAR into <IS_HOME>/repository/components/dropins.
Also it’s required to register you authenticator. For this modify <IS_HOME>/repository/conf/security/application-authenticators.xml Add the following values to appropriate sections:
<AuthenticatorNameMappings>
…
<AuthenticatorNameMapping name=”YenloCustomAuthenticator” alias=”yenlo-custom” />
</AuthenticatorNameMappings>
And
<AuthenticatorConfigs>
…
<AuthenticatorConfig name=”YenloCustomAuthenticator” enabled=”true”></AuthenticatorConfig>
</AuthenticatorConfigs>
Start the Identity Server.
To configure Single Sign-On please use the following documentation from WSO2 as well as the test application suggested in the documentation:https://docs.wso2.com/display/IS500/Configuring+Single+Sign-On+with+SAML+2.0
After you have successfully configured SAML SSO we need to setup some additional configuration for the service provider. For that go to the service provider configuration page, expand ‘Local & Outbound Authentication Configuration’ and click on the advanced configuration as shown on the screenshot below:
After you’re redirected to the advanced configuration page you have to configure 2 steps for the authentication as following:
Update the configuration and save service provider configuration.
To make YenloCustomAuthenticator visible during the authentication process we need to modify the authenticator endpoint located at <IS_HOME>/repository/deployment/server/webapps/authenticatorendpoint.war Open the application archive and place <SOURCE_ROOT>/jsp/yenloauth.jsp as well as replace the original login.jsp with <SOURCE_ROOT>/jsp/login.jsp
Remove <IS_HOME>/repository/deployment/server/webapps/authenticatorendpoint folder and restart Identity Server.