Bug 57937 - Request for a form based two factor authentication support in Tomcat
Summary: Request for a form based two factor authentication support in Tomcat
Status: RESOLVED WONTFIX
Alias: None
Product: Tomcat 7
Classification: Unclassified
Component: Catalina (show other bugs)
Version: 7.0.55
Hardware: PC Linux
: P2 enhancement with 5 votes (vote)
Target Milestone: ---
Assignee: Tomcat Developers Mailing List
URL:
Keywords: PatchAvailable
Depends on:
Blocks:
 
Reported: 2015-05-19 14:39 UTC by Marco Bellavia
Modified: 2016-06-05 17:06 UTC (History)
1 user (show)



Attachments
Result of a diff -u operated on the original code and the pseudo-code containing a 2fa workaround (2.03 KB, patch)
2015-05-19 14:39 UTC, Marco Bellavia
Details | Diff

Note You need to log in before you can comment on or make changes to this bug.
Description Marco Bellavia 2015-05-19 14:39:27 UTC
Created attachment 32744 [details]
Result of a diff -u operated on the original code and the pseudo-code containing a 2fa workaround

Hi there,

for an application to be deployed on a TomEE we need the support for two 
factor authentication.
Are there planned development efforts going into that direction?


As temporary workaround we patched the class FormAuthenticator, so that a second factor (a 2fa token) is additionlly evaluated. This patch is in the style of AuthenticRoast, an solution available for Tomcat 1.6.

Here the code patch in pseudo code:



package org.apache.catalina.authenticator;

[original code omitted with imports]

/**
 * An <b>Authenticator</b> and <b>Valve</b> implementation of FORM BASED Authentication, as
 * described in the Servlet API Specification, Version 2.2.
 */
public class FormAuthenticator extends AuthenticatorBase {

  [original code omitted]


  // --------------------------------------------------------- Public Methods

  /**
   * Authenticate the user making this request, based on the specified login configuration. Return
   * <code>true</code> if any specified constraint has been satisfied, or 
<code>false</code> if we
   * have created a response challenge already.
   *
   * @param request Request we are processing
   * @param response Response we are creating
   * @param config Login configuration describing how authentication should be performed
   *
   * @exception IOException if an input/output error occurs
   */
  @Override
  public boolean authenticate(Request request, HttpServletResponse response, LoginConfig config) throws IOException {

    // References to objects we will need later 
    Session session = null;
    // Have we already authenticated someone?
    Principal principal = request.getUserPrincipal();
    String ssoId = (String) request.getNote(Constants.REQ_SSOID_NOTE);
 
    [original code omitted]

    // Yes -- Acknowledge the request, validate the specified credentials
    // and redirect to the error page if they are not correct
    request.getResponse().sendAcknowledgement();
    Realm realm = context.getRealm();
    if (characterEncoding != null) {
      request.setCharacterEncoding(characterEncoding);
    }

    String username = request.getParameter(Constants.FORM_USERNAME);
    String password = request.getParameter(Constants.FORM_PASSWORD);
    if (log.isDebugEnabled()) {
      log.debug("Authenticating username '" + username + "'");
    }


    // BEGIN --- Customized code for two factor authentication Part I --- BEGIN
    String password2factor = request.getParameter("j_2fa");
    // A customized Realm Class which implements a second factor authentication
    final TwoFactorRealm twoFactorRealm = new TwoFactorRealm(...);
    boolean twoFactorSuccessful = twoFactorRealm.authenticate(username, password2factor);
    if (log.isWarnEnabled() && !twoFactorSuccessful) {
      log.warn("Authentication 2FA failed for user " + username);
    }
    // END --- Customized code for two factor authentication Part I --- END
 

    principal = realm.authenticate(username, password);

    // BEGIN --- Customized code for two factor authentication Part II --- BEGIN
    if (principal == null || !twoFactorSuccessful) {
      log.warn("Authentication of '" + username + "' failed: [principal=" + principal + "], [twoFactorSuccessful=" + twoFactorSuccessful + "]");
      forwardToErrorPage(request, response, config);
      return (false);
    }
    // END --- Customized code for two factor authentication Part II --- END

    if (log.isDebugEnabled()) {
      log.debug("Authentication of '" + username + "' was successful");
    }

    if (session == null) {
      session = request.getSessionInternal(false);
    }
    if (session == null) {
      if (containerLog.isDebugEnabled()) {
        containerLog.debug("User took so long to log on the session expired");
      }
      if (landingPage == null) {
        response.sendError(HttpServletResponse.SC_REQUEST_TIMEOUT, sm.getString("authenticator.sessionExpired"));
      } else {
        // Make the authenticator think the user originally requested
        // the landing page
        String uri = request.getContextPath() + landingPage;
        SavedRequest saved = new SavedRequest();
        saved.setMethod("GET");
        saved.setRequestURI(uri);
        saved.setDecodedRequestURI(uri);
        request.getSessionInternal(true).setNote(Constants.FORM_REQUEST_NOTE, saved);
        response.sendRedirect(response.encodeRedirectURL(uri));
      }
      return (false);
    }

    // Save the authenticated Principal in our session
    session.setNote(Constants.FORM_PRINCIPAL_NOTE, principal);

    // Save the username and password as well
    session.setNote(Constants.SESS_USERNAME_NOTE, username);
    session.setNote(Constants.SESS_PASSWORD_NOTE, password);

    // Redirect the user to the original request URI (which will cause
    // the original request to be restored)
    requestURI = savedRequestURL(session);
    if (log.isDebugEnabled()) {
      log.debug("Redirecting to original '" + requestURI + "'");
    }
    if (requestURI == null) {
      if (landingPage == null) {
        response.sendError(HttpServletResponse.SC_BAD_REQUEST,sm.getString("authenticator.formlogin"));
      } else {
        // Make the authenticator think the user originally requested
        // the landing page
        String uri = request.getContextPath() + landingPage;
        SavedRequest saved = new SavedRequest();
        saved.setMethod("GET");
        saved.setRequestURI(uri);
        saved.setDecodedRequestURI(uri);
        session.setNote(Constants.FORM_REQUEST_NOTE, saved);
        response.sendRedirect(response.encodeRedirectURL(uri));
      }
    } else {
      response.sendRedirect(response.encodeRedirectURL(requestURI));
    }
    return (false);

  }

  [original code omitted] 
 
}

Best regards,

Marco
--
Dipl.-Inf. Marco Bellavia 
Software Engineering

DENIC eG
Kaiserstraße 75-77
60329 Frankfurt am Main
GERMANY

Angaben nach § 25a Absatz 1 GenG:
DENIC eG (Sitz: Frankfurt am Main)
Vorstand: Helga Krüger, Andreas Musielak, Carsten Schiefner, Dr. Jörg 
Schweiger 
Vorsitzender des Aufsichtsrats: Thomas Keller
Eingetragen unter Nr. 770 im Genossenschaftsregister, Amtsgericht 
Frankfurt am Main
Comment 1 Christopher Schultz 2015-05-19 19:09:31 UTC
I'm not s big fan of the current implementation as a patch to FormAuthenticator. Why not do this as a subclass?
Comment 2 Mark Thomas 2016-06-05 17:06:31 UTC
The patch is incomplete - it does not include the Realm changes necessary.

This patch implements one view of how this could be done. It is not implementing a standard.

This could be implemented as a JASPIC module (or even with JAAS).

Given all of the above, I don't think this should be included in the standard Tomcat distribution. If a plug-in module was developed to implement this, we could add a pointer to it on the wiki.