SecurityManagerAuthenticator.java

/*
 ** Module   : SecurityManagerAuthenticator.java
 ** Abstract : SecurityManager implementation of Authenticator interface moved to its own class.
 **
 ** Copyright (c) 2023, Golden Code Development Corporation.
 **
 ** -#- -I- --Date-- -----------------------Description------------------------
 ** 001 GBB 20230825 Initial version.
 */

/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
**   Under Section 7 of the GNU Affero GPL version 3, the following additional
**   terms apply to the works covered under the License.  These additional terms
**   are non-permissive additional terms allowed under Section 7 of the GNU
**   Affero GPL version 3 and may not be removed by you.
**
**   0. Attribution Requirement.
**
**     You must preserve all legal notices or author attributions in the covered
**     work or Appropriate Legal Notices displayed by works containing the covered
**     work.  You may not remove from the covered work any author or developer
**     credit already included within the covered work.
**
**   1. No License To Use Trademarks.
**
**     This license does not grant any license or rights to use the trademarks
**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
**     of Golden Code Development Corporation. You are not authorized to use the
**     name Golden Code, FWD, or the names of any author or contributor, for
**     publicity purposes without written authorization.
**
**   2. No Misrepresentation of Affiliation.
**
**     You may not represent yourself as Golden Code Development Corporation or FWD.
**
**     You may not represent yourself for publicity purposes as associated with
**     Golden Code Development Corporation, FWD, or any author or contributor to
**     the covered work, without written authorization.
**
**   3. No Misrepresentation of Source or Origin.
**
**     You may not represent the covered work as solely your work.  All modified
**     versions of the covered work must be marked in a reasonable way to make it
**     clear that the modified work is not originating from Golden Code Development
**     Corporation or FWD.  All modified versions must contain the notices of
**     attribution required in this license.
*/

package com.goldencode.p2j.security;

import com.goldencode.p2j.cfg.*;
import com.goldencode.p2j.directory.*;
import com.goldencode.p2j.net.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.logging.*;

import java.io.*;
import java.util.*;
import java.util.logging.*;

/**
 * SecurityManager implementation of Authenticator interface moved to its own class.
 * Using it through {@link SecurityManager#serverAuthenticator} allows resetting SecurityManager instance.
 */
class SecurityManagerAuthenticator
implements Authenticator
{
   /** Logger. */
   private static final CentralLogger LOG = CentralLogger.get(SecurityManagerAuthenticator.class);

   /** SecurityManager instance. */
   private SecurityManager sm;
   
   /** The value of "option" node from AuthPlugin for this Authenticator */
   @SuppressWarnings("unused")
   private String authOption = null;
   
   /**
    * Public constructor. 
    * 
    * @param    securityManager
    *           SecurityManager instance.
    */
   SecurityManagerAuthenticator(SecurityManager securityManager)
   {
      sm = securityManager;
   }
   
   /**
    * Implements client side standard authentication logic.
    * <p>
    * Returns a byte array to be transmitted to the server as authentication
    * input.  The userid and password values will be obtained by prompting
    * the user using <code>stdin</code> and <code>stdout</code>.
    * <p>
    * If this method is called, it simply provides a userID and password no
    * matter what authentication mode is. It works, because certificates are
    * verified before the call, if the authentication mode required that.
    * <p>
    * This logic allows specifying 
    * <code>com.goldencode.p2j.security.SecurityManager</code> as a hook name.
    *
    * @param    parameters
    *           Additional configuration parameters. Not used in this
    *           implementation.
    * @param    code
    *           The result of the most recent attempt to authenticate or
    *           <code>AUTH_RESULT_NONE</code> if this is the first attempt.
    *
    * @return   Array of bytes that is taken as an authentication input.
    *           The array is suitable for passing to {@link #serverAuthHook}
    *           for authentication processing.
    */
   @Override
   public byte[] clientAuthHook(Map<String, Object> parameters, int code)
   {
      return clientAuthHookWorker(parameters, code, sm.getConfig());
   }

   /**
    * Finalizes any resources allocated during authentication by the client.
    */
   @Override
   public void clientFinalize()
   {
   }

   /**
    * Implements server side standard authorization logic.
    * <p>
    * Accepts the byte array produced by the client side authorization hook
    * as the authentication input, and custom parameters.
    *
    * @param    auth
    *           The authentication input from the client in a form that is
    *           created using {@link SecurityUtil#packageIdPassword}.
    * @param    entity
    *           Entity to be processed.
    *           Not used in this implementation.
    *
    * @return   The authentication result.
    */
   @Override
   public AuthenticationResponse serverAuthHook(byte[] auth, String entity)
   {
      if (!sm.isServer())
         return null;

      // parse the byte array input
      ByteArrayInputStream bis = new ByteArrayInputStream(auth);
      DataInputStream      dis = new DataInputStream(bis);

      String userId = null;
      String pw     = null;
      char[] userPw = null;

      try
      {
         userId = dis.readUTF();
         pw     = dis.readUTF();
         userPw = pw.toCharArray();
      }
      catch (IOException ioe)
      {
         return new AuthenticationResponse(null, AUTH_RESULT_UNSPECIFIED_FAILURE);
      }

      // WARNING: the security cache accessed here may be different than the caller's security
      // cache generation

      // Safely query cached data
      SecurityCache sc = sm.getCache();

      // locate the account and its password hash
      LOG.finer("Received user ID <" + userId + ">");
      Account acc = sc.getAccountById(userId);

      if (acc == null)
      {
         LOG.finer("No account for ID <" + userId + ">");
         return new AuthenticationResponse(null, AUTH_RESULT_INVALID_USERID);
      }

      // TODO: Why is this restriction needed? By doing this, one cannot
      //       use the custom auth method to authenticate processes, right?
      if (acc.getAccountType() != Account.ACC_USER)
      {
         LOG.finer("Wrong account type for ID <" + userId + ">");
         return new AuthenticationResponse(null, AUTH_RESULT_UNSPECIFIED_FAILURE);
      }

      UserAccount user = (UserAccount) acc;

      if (user.getAuthMode() == AUTH_MODE_X509)
      {
         LOG.finer("Account <" + userId + "> uses x509 auth mode, cancelling custom or id/pw auth");
         return new AuthenticationResponse(null, AUTH_RESULT_INVALID_USERID);
      }

      // verify password
      if (LOG.isLoggable(Level.FINEST))
         LOG.finest("Received password <" + pw + ">");
      else
         LOG.finer("Received password");

      if (checkPassword(new String(userPw), user))
      {
         agePassword(user);
         return new AuthenticationResponse(userId, AUTH_RESULT_SUCCESS);
      }

      return new AuthenticationResponse(null, AUTH_RESULT_INVALID_PASSWORD);
   }

   /**
    * Always returns <code>null</code>.
    *
    * @return  Always <code>null</code>.
    */
   @Override
   public SessionListener getSessionListener()
   {
      return null;
   }

   /**
    * Configures the Authenticator by setting the "option" parameter from directory.xml.
    *
    * @param   option
    *          The value of "option" entry for the auth plugin.
    */
   @Override
   public void configure(String option)
   {
      this.authOption = option;
   }
   
   /**
    * Returns a set of entities that this class handles. The list of entities / parameters will
    * be configured using other ways.
    *
    * @return  Always <code>null</code>.
    */
   @Override
   public Set<String> getAuthenticationEntities()
   {
      return null;
   }
   
   /**
    * Implements client side standard authentication logic.
    * <p>
    * Returns a byte array to be transmitted to the server as authentication
    * input.  If not contained in the given bootstrap configuration, the userid
    * and password values will be obtained by prompting the user using
    * <code>stdin</code> and <code>stdout</code>.
    * <p>
    * If this method is called, it simply provides a userID and password no
    * matter what authentication mode is. It works, because certificates are
    * verified before the call, if the authentication mode required that.
    *
    * @param    parameters
    *           Additional configuration parameters. Not used in this
    *           implementation.
    * @param    code
    *           The result of the most recent attempt to authenticate or
    *           <code>AUTH_RESULT_NONE</code> if this is the first attempt.
    * @param    config
    *           Configuration data upon which to base default processing.
    *
    * @return   Array of bytes that is taken as an authentication input.
    *           The array is suitable for passing to {@link #serverAuthHook}
    *           for authentication processing.
    */
   byte[] clientAuthHookWorker(Map<String, Object> parameters, int code, BootstrapConfig config)
   {
      if (config.isServer())
         return null;

      String id = null;
      String pw = null;
      boolean stored = true;

      try
      {
         id = config.getString("access", "subject", "id", null);

         if (id == null)
         {
            id = new String(Utils.prompt("User ID  :"));
            stored = false;
         }

         pw = config.getString("access", "password", "user", null);

         if (pw == null)
         {
            pw = new String(Utils.prompt("Password :"));
            stored = false;
         }

         config.setConfigItem("access", "password", "user", "password");
      }

      catch (Exception exc)
      {
         // pass through any failures
      }

      // if there was a failure and this input comes from the config, fail 
      if (stored && code > AUTH_RESULT_SUCCESS)
      {
         return null;
      }

      return SecurityUtil.packageIdPassword(id, pw);
   }
   
   /**
    * Verifies the plain text password against the one stored with 
    * the specified user account.
    *
    * @param   plain
    *          plain text password
    * @param   user
    *          <code>UserAccount</code> instance
    * @return  <code>true</code> if password is valid
    */
   private boolean checkPassword(String plain, UserAccount user)
   {
      if (user.getWebServiceToken() != null)
      {
         // this is an user used for web service requests, don't allow password authentication
         return false;
      }

      if (!user.isProtected())
      {
         // this user account is not protected with a password
         return true;
      }

      byte[] hashpw = null;

      synchronized (sm.pwchSync)
      {
         hashpw = user.getPassword();
      }

      if (hashpw == null)
      {
         // no password assigned - the account can't be used
         return false;
      }

      byte[] testpw = HashPassword.hashPassword(plain);

      if (testpw == null)
      {
         return false;
      }

      if (testpw.length != hashpw.length)
      {
         LOG.finer("Wrong password");
         return false;
      }

      for (int i = 0; i < hashpw.length; i++)
      {
         if (testpw[i] != hashpw[i])
         {
            LOG.finer("Wrong password");
            return false;
         }
      }

      return true;
   }
   
   /**
    * Calculates the age of the existing user account password and checks to
    * see whether it is too old, according to the maxAge parameter. 
    *
    * @param   user
    *          <code>UserAccount</code> instance
    */
   private void agePassword(UserAccount user)
   {
      // Pick up the generation of the security cache
      SecurityCache sc = sm.getCache();

      // get and check the password aging parameter
      int max = sc.getMaxAge();
      if (max == 0)
         return;

      // get the timestamp of the last password change, if any
      DateValue pdate = user.getPasswordDate();
      TimeValue ptime = user.getPasswordTime();

      // no or incomplete timestamp means the password is considered aged
      if (pdate == null || ptime == null)
      {
         user.setPasswordAged(true);
         return;
      }

      // calculate password age
      Date pwDate = pdate.getDate();
      Date now = new Date();

      long maxAge = ((long)max) * 86400 * 1000;
      long age = now.getTime() - pwDate.getTime();

      // flag the password as aged if its age exceeds the limit
      if (age > maxAge)
         user.setPasswordAged(true);
   }
}