Shailesh K. Mishra
Using multi-factor authentication to protect web applications deployed on Oracle WebLogic.
October 2013
In multi factor-authentication, user genuineness is validated based on multiple factors, not merely the traditional mechanism of user name/password. For example, when a bank customer visits an ATM, one authentication factor is the physical ATM card ("something the user has"); the second factor is the customer’s account PIN ("something the user knows"). Without verification of both of these factors, authentication fails.
The multi-factor authentication concept can also be applied to web applications deployed on Oracle WebLogic Server, as the following sections detail.
Note: This article assumes that reader has good understanding of Oracle WebLogic security concepts and authentication mechanisms. Oracle WebLogic version 10.3.5 was used for this article.
In the interests of concision, this article describes only weblogic.security.spi.ChallengeIdentityAsserterV2 and weblogic.security.spi.ServletAuthenticationFilter interfaces. For further details on interfaces and for lifecycle details about authentication providers in Oracle WebLogic, please see Developing Security Providers for Oracle WebLogic Server in the References section.
This interface allows Identity Assertion providers to support such authentication protocols as Microsoft's Windows NT Challenge/Response (NTLM), Simple and Protected GSS-API Negotiation Mechanism (SPNEGO), and other challenge/response authentication mechanisms. Its two methods -- assertChallengeIdentity and continueChallengeIdentity -- are of interest to this article. The first method uses the supplied token to establish client identity, possibly with multiple challenges; the second is used to continue establishing the client identity until the process is complete.
This interface is used to signal that the Servlet container should include this authentication provider’s authentication filters during the authentication process. It defines one method, getServletAuthenticationFilters, which returns an ordered list of javax.servlet.Filters that will be executed during the authentication process of the Servlet container.
We need to create the following artifacts:
For demonstration, this web application contains only two jsp pages: the first collects the user name/password and second collects a code, sent to the user’s cell phone. Below are snippets of these JSP pages.
JSP page to collect user name/password:
<%@ page language="java" contentType="text/html;charset=UTF-8"%>
<h1>Multi Factor Login</h1>
<form method="post" action="login">
<input type="hidden" name="originalRequest" value="<%=request.getParameter("originalRequest")
%>"/> <br>
<input type="text" name="j_username" value=""/><br>
<input type="password" name="j_password" value=""/><br>
<input type="submit" name="submit" title="submit" value="submit">
</form>
JSP page for collection code sent to user’s cell phone:
<%@ page language="java" contentType="text/html;charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<h1>Multi Factor Login</h1>
<form method="post" action="login">
<input type="text" name="OTP" value=""/><br>
<input type="submit" name="submit" title="submit" value="submit">
</form>
web.xml snippet of this web app
Please note the protected url "/login" used to submit user name/password and OTP in the jsp pages above.
login
/login
secured
authenticatedusers
Below is the sample code for a custom authentication provider that was used to test this concept. To improve readability, some code has been removed.
MultiFactorIdentityAsserterProviderImpl.java
public final class MultiFactorIdentityAsserterProviderImpl implements
AuthenticationProviderV2, ChallengeIdentityAsserterV2,
ServletAuthenticationFilter {
......................................................................
public javax.servlet.Filter[] getServletAuthenticationFilters() {
System.out.println("You are getting my filters....");
return new Filter[] { new MultiFactorFilter(this.config), new SampleFilter() };
}
@Override
public ProviderChallengeContext assertChallengeIdentity(String token,
Object value, ContextHandler ctx) throws IdentityAssertionException {
System.out.println("AssertChallengeIdentity");
System.out.println("AssertChallengeContext " + token + " " + value
+ " " + Arrays.asList(ctx.getNames()));
MultiFactorChallengeContext challengeCtx =
new MultiFactorChallengeContext(this.config);
try {
challengeCtx.processRequest(ctx);
} catch (Exception e) {
throw new IdentityAssertionException(e.getMessage());
}
if (challengeCtx.hasChallengeIdentityCompleted()) {
complete(challengeCtx,ctx);
}
return challengeCtx;
}
@Override
public void continueChallengeIdentity(
ProviderChallengeContext providerChallengeContext, String arg1,
Object arg2, ContextHandler ctx) throws IdentityAssertionException {
// TODO Auto-generated method stub
System.out.println("Continuing....");
System.out.println("AssertChallengeContext " + arg1 + " " + arg2 + " "
+ Arrays.asList(ctx.getNames()));
MultiFactorChallengeContext challengeCtx =
(MultiFactorChallengeContext) providerChallengeContext;
try {
challengeCtx.processRequest(ctx);
} catch (Exception e) {
throw new IdentityAssertionException(e.getMessage());
}}}
MultiFactorAuthenticationFilter.java
public class MultiFactorAuthenticationFilter implements Filter {
private String [] protectedPaths;
private String webAppPath;
public MultiFactorAuthenticationFilter(MultiFactorAuthenticationProviderMBean config){
// TODO Auto-generated constructor stub
this.protectedPaths = config.getProtectedPaths();
this.webAppPath = config.getMultiFactorWebAppPath();
System.out.println("Filtering: "+Arrays.asList(this.protectedPaths));
}
@Override
public void destroy() {
// TODO Auto-generated method stub
}
@Override
public void doFilter(ServletRequest req, ServletResponse resp,
FilterChain chain) throws IOException, ServletException {
// TODO Auto-generated method stub
HttpServletRequest hReq = (HttpServletRequest)req;
HttpServletResponse hResp = ((HttpServletResponse)resp);
System.out.println("In MultiFactorFilter requestURI="+hReq.getRequestURI());
if (!hReq.getRequestURI().startsWith(this.webAppPath)) {
boolean isProtectedPath = false;
for (int i=0; i if they don't get access, they'll need to log in again
session.removeAttribute("ChallengeCtx");
Subject subject = (Subject)session.getAttribute("subject");
ServletAuthentication.runAs(subject, hReq);
} else {
Object challengeToken = acc.getChallengeToken();
if (challengeToken == null) {
//Redirect to Login
throw new ServletException("Challenge Token is NULL");
} else {
hResp.sendRedirect(this.webAppPath+"/otp.jsp");
}
}
} catch (Exception e) {
e.printStackTrace();
throw new ServletException(e.getMessage());
}
}
@Override
public void init(FilterConfig arg0) throws ServletException {
// TODO Auto-generated method stub
System.out.println("Initializing.....");
}
}
MultiFactorAuthenticationChallengeContext.java
public class MultiFactorAuthenticationChallengeContext implements ProviderChallengeContext {
private boolean completed = false;
private Object challengeToken = null;
private CallbackHandler handler = null;
private Subject discovered = null;
private String originalRequest;
private String webAppPath = null;
public MultiFactorAuthenticationChallengeContext
(MultiFactorAuthenticationProviderMBean config) {
super();
try {
this.webAppPath = config.getMultiFactorWebAppPath();
} catch (Exception e) {
throw new SecurityException(e.getMessage());
}
}
................................................................................
@Override
public boolean hasChallengeIdentityCompleted() {
// TODO Auto-generated method stub
return completed;
}
void processRequest(ContextHandler ctx) throws Exception {
HttpServletRequest req = (HttpServletRequest) ctx.getValue("req");
HttpSession session = req.getSession(true);
if (session.getAttribute("subject") == null) {
String userName = req.getParameter("j_username");
String password = req.getParameter("j_password");
if (userName == null || password == null) {
throw new Exception("No user name and password in request");
} else {
this.originalRequest = req.getParameter("originalRequest");
System.out.println("Saving the original request: "+this.originalRequest);
//validate the user name password
Authentication auth = new weblogic.security.services.Authentication();
try{
Subject s = auth.login(new MyCallBackHandler(userName, password));
session.setAttribute("subject", s);
}catch(LoginException le){
throw new Exception("Invalid user name/password", le);
}
this.generateOTP(req);
}
} else {
this.validateResponse(req);
}
}
private void validateResponse(HttpServletRequest req)
throws Exception {
// TODO Auto-generated method stub
System.out.println("Getting Validated....");
String token = req.getParameter("OTP");
if(token == null || token.length() == 0 || !this.challengeToken.equals(token)){
throw new Exception("Invalid OTP");
}
this.completed = true;
if (this.originalRequest!=null) {
//Set it as an attribute on the request, so the filter can use it
req.setAttribute("originalRequest", this.originalRequest);
} else {
throw new Exception("Couldn't locate original request in ChallengeContext");
}
//return verified; // success
}
private void generateOTP(HttpServletRequest req)
throws Exception {
// TODO Auto-generated method stub
/*
*this is for demo only. Write your custom code to
*generate a token and send it to user
*using a mechanism which works for your env.
this.challengeToken = "0123456789";
}
}
Note the methods processRequest and generateOTP in the MultifactorAuthenticationContext class. The processRequest method is responsible for validating user name/password using one of the WebLogic APIs. Once user name and password are valid, this method calls the generateOTP method to generate an OTP and send it to the user. The user is then redirected to the otp.jsp page of the web application, as mentioned above, by the MultiFactorAuthenticationFilter class. The user enters otp, which is validated by the validateResponse method of the MultifactorAuthenticationContext class. At this point, overall authentication is successful and the user is allowed access to a protected web application.
It’s important to note that it may not be desirable or appropriate to protect all your web apps using this mechanism. For example, we should not use this protection mechanism to protect an Oracle WebLogic administrative console, because any code error could prevent access to the administrative console. For more details on configuring the authentication provider, please see Developing Security Providers for Oracle WebLogic Server in the References section .
Below are the important mbean attributes for this custom authentication provider:
<MBeanAttribute
Name = "MultiFactorWebAppPath"
Type = "java.lang.String"
Default = ""/multifactor""
Description = "The Path (Context Root) of the custom Web App
which is responsible for collecting user's credentials and OTP"
/>
<MBeanAttribute
Name = "ProtectedPaths"
Type = "java.lang.String[]"
Default = "new String[] { "/App" }"
Description = "The paths of web apps that are protected by
multi factor authentication"
/>
This article has shown how multi-factor authentication (user name plus password and OTP combination) can be achieved to protect web applications deployed in the WebLogic server. It has also shown that only a set of web applications can be protected by multi-factor authentication, and that the Oracle WebLogic administrative console should not be protected by the mechanism described in this article.
The author would like to thank Yi Wang for reviewing and providing feedback for this article.
Shailesh K. Mishra is part of the Oracle Identity Manager team. He earned his B. Tech. from IIT BHU. He explores middleware performance and security on his free time.