/***********************************************************************
 * 
 *  Copyright (C) 2006 Novell, Inc. All Rights Reserved.
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; version 2.1
 *  of the License.
 *
 *  This library 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
 *  Library Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, Novell, Inc.
 * 
 *  To contact Novell about this file by physical or electronic mail, 
 *  you may find current contact information at www.novell.com.
 * 
 *  Author: Juan Carlos Luciani <jluciani@novell.com>
 *  
 ***********************************************************************/

package com.novell.casa.jaas;

import java.util.Map;
import java.util.Set;

import org.apache.log4j.Logger;

import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.login.FailedLoginException;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;

import com.novell.casa.authtoksvc.AuthToken;
import com.novell.casa.authtoksvc.CasaIdentityToken;

/**
 * CasaLoginModule Class.
 * <p>
 * This class implements a LoginModule which performs
 * authentication via the Casa Authentication Token
 * infrastructure.
 * 
 */
public final class CasaLoginModule implements LoginModule
{
   private static final Logger m_log = Logger.getLogger(CasaLoginModule.class);

   private final static String casaUsername = "CasaIdentityUser";
   
   private Subject            m_subject = null;
   private CasaPrincipal      m_principal = null;
   private CallbackHandler    m_callbackHandler = null;
   private Map                m_sharedState = null;
   private Map                m_options = null;
   
   /**
    * Aborts the subject authentication operation.
    *
    * @throws LoginException
    * @see javax.security.auth.spi.LoginModule#abort()
    */
   public boolean abort() throws LoginException
   {
      // Clear out all of our state
      m_subject = null;
      m_principal = null;
      m_callbackHandler = null;
      m_sharedState = null;
      m_options = null;
      
      return true;
   }

   /**
    * Commit the results of the subject authentication operation.
    *
    * @throws LoginException
    * @see javax.security.auth.spi.LoginModule#commit()
    */
   public boolean commit() throws LoginException
   {
      // Check if we instantiated a principal to associate
      // with the subject.
      if (m_principal != null)
      {
         try
         {
            // Add our principal to the set associated with
            // the subject.
            m_subject.getPrincipals().add(m_principal);
            return true;
         }
         catch (Exception e)
         {
            m_log.error("commit()- Exception caught associating principal, msg: " + e.getMessage());
            throw new LoginException("Error encountered");
         }
      }
      else
      {
         // Allways return since authentication failed or was not
         // performed by us.
         return false;
      }
   }

   /**
    * Authenticate subject.
    *
    * @throws LoginException
    * @see javax.security.auth.spi.LoginModule#login()
    */
   public boolean login() throws LoginException
   {
      // Verify that a CallbackHandler was specified
      if (m_callbackHandler == null)
      {
         m_log.error("login()- Null CallbackHandler");
         throw new LoginException("Null CallbackHandler");
      }
      
      // Do not perform the username check unless configured to do it.
      boolean performUsernameCheck = false;
      if (m_options != null
          && m_options.containsKey("PerformUsernameCheck"))
      {
         String keyVal = (String) m_options.get("PerformUsernameCheck");
         if (keyVal != null && keyVal.equalsIgnoreCase("true"))
            performUsernameCheck = true;
      }
      
      if (performUsernameCheck)
      {
         // Verify that the username is CasaIdentityUser, for this
         // we first need to obtain it.
         //
         // Try to obtain the user name from the shared state
         String username = (String) m_sharedState.get("javax.security.auth.login.name");
         if (username == null)
         {
            // The usename was not stored in the shared state, request it.
            try
            {
               NameCallback nameCallback = new NameCallback("Enter username:");
               Callback[] callbacks = new Callback[1];
               callbacks[0] = nameCallback;
               m_callbackHandler.handle(callbacks);
               username = nameCallback.getName();
            }
            catch (Exception e)
            {
               m_log.warn("login()- Exception caught during nameCallback, msg: " + e.getMessage());
            }
            
            // Check the username
            if (username == null)
               return false;
            else
            {
               // Save the retrieved username in the shared state and then check it.
               m_sharedState.put("javax.security.auth.login.name", username);
               if (username.equalsIgnoreCase(casaUsername) == false)
                  return false;
            }
         }
         else
         {
            // Check the username
            if (username.equalsIgnoreCase(casaUsername) == false)
               return false;
         }
      }
         
      // Obtain the CasaAuthenticationToken
      char[] authTokenChars = null;
      try
      {
         PasswordCallback passwordCallback = new PasswordCallback("Enter CasaAuthenticationToken:", false);
         Callback[] callbacks = new Callback[1];
         callbacks[0] = passwordCallback;
         m_callbackHandler.handle(callbacks);
         authTokenChars = passwordCallback.getPassword();
      }
      catch (Exception e)
      {
         m_log.warn("login()- Exception caught during passwordCallback, msg: " + e.getMessage());
      }
      
      // Check the CasaAuthenticationToken
      if (authTokenChars != null)
      {
         // Instantiate the AuthToken, this validates the token itself.
         try
         {
            AuthToken authToken = new AuthToken(new String(authTokenChars), true);
            
            // Instantiate the appropriate IdentityToken based on the IdentityTokenProvider type
            // tbd - For now use the CasaIdentityToken
            CasaIdentityToken identityToken = new CasaIdentityToken();
            identityToken.initialize(authToken.getIdentityToken());
            
            // Now instantiate the CasaPrincipal
            m_principal = new CasaPrincipal(identityToken);
         }
         catch (Exception e)
         {
            // The validation of one of the tokens failed
            m_log.info("login()- Exception caught during token processing, msg: " + e.getMessage());
            throw new FailedLoginException("Token validation failed");
         }
      }
      else
      {
         // Token not provided
         m_log.warn("login()- Token not provided");
         throw new FailedLoginException("CasaAuthenticationToken not obtained");
      }

      // User validated
      return true;
   }

   /**
    * Logout subject.
    *
    * @throws LoginException
    * @see javax.security.auth.spi.LoginModule#logout()
    */
   public boolean logout() throws LoginException
   {
      // Check if we must try to remove our principal
      // from the associated subject.
      if (m_principal != null
          && m_subject.isReadOnly() == false)
      {
         Set principalSet = m_subject.getPrincipals();
         principalSet.remove(m_principal);
      }
      return true;
   }

   /**
    * Initialize CasaLoginModule instance.
    *
    * @param subject          The subject to be authenticated.
    * @param callbackHandler  Callback handler for communicating with end-user.
    * @param sharedState      State shared between login modules.
    * @param options          Module configuration options.
    * @see javax.security.auth.spi.LoginModule#initialize(javax.security.auth.Subject, javax.security.auth.callback.CallbackHandler, java.util.Map, java.util.Map)
    */
   public void initialize(Subject subject,
                          CallbackHandler callbackHandler,
                          Map sharedState,
                          Map options)
   {
      // Save the input parameters for later use
      m_subject = subject;
      m_callbackHandler = callbackHandler;
      m_sharedState = sharedState;
      m_options = options;
   }
}
