/***********************************************************************
 * 
 *  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.authtoksvc;

import java.io.ByteArrayInputStream;

import org.apache.log4j.Logger;
import org.apache.xml.serialize.OutputFormat;
import org.apache.xml.serialize.XMLSerializer;
import org.apache.xml.security.utils.Constants;
import org.apache.xerces.parsers.DOMParser;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;

import java.io.*;


/**
 * AuthToken Class.
 * <p>
 * This class constructs authentication tokens that clients can present
 * to services for authentication. The authentication token consists of
 * a SOAP message secured with WSSecurity with the appropriate elements signed
 * and with a timestamp. The body of the SOAP message is as follows:
 * <p>
 *    <auth_token>
 *    <ident_token>
 *    <ident_token_provider>Identity Token type</ident_token_provider>
 *    <ident_token_data>identity token data</ident_token_datar>
 *    </ident_token>
 *    </auth_token>
 *
 */
public final class AuthToken
{
   private static final Logger m_log = Logger.getLogger(AuthToken.class);

   private String       m_token;
   private String       m_lifetime = "";
   private String       m_lifetimeShorter = "";
   private String       m_identityTokenType = null;
   private String       m_identityToken = null;

   static private SecureTokenUtil m_serverSecTokenUtil = null;
   static private SecureTokenUtil m_clientSecTokenUtil = null;


   /**
    * Constructor.
    *
    * @param identityId          Id of the authenticated identity.
    * @param realm               Realm where the identity id is valid.
    * @param realmUrl            Realm Url.
    * @param targetService       Name of the targeted service.
    * @param targetHost          Host where the targeted service resides.
    * @param svcConfig           Service configuration object.
    * @param enabledSvcsConfig   Enabled services configuration object.
    * @throws Exception
    */
   public AuthToken(String identityId,
                    String realm,
                    String realmUrl,
                    String targetService,
                    String targetHost,
                    SvcConfig svcConfig,
                    EnabledSvcsConfig enabledSvcsConfig) throws Exception
   {
      // Get access to the authentication token configuration for this service
      AuthTokenConfig authTokenConfig = enabledSvcsConfig.getAuthTokenConfig(targetHost, targetService);
      if (authTokenConfig != null)
      {
         // For now lets use the services of the only IdentityToken provider
         // that we have.
         //
         // tbd - Add code to allow for the consumption of tokens
         // from different providers.
         CasaIdentityToken identityToken = new CasaIdentityToken(enabledSvcsConfig.getIdenTokenConfig(targetHost, targetService));
         identityToken.initialize(identityId,
                                  realm,
                                  realmUrl,
                                  targetService,
                                  targetHost,
                                  svcConfig);

         m_identityToken = identityToken.getEncodedToken();
         m_identityTokenType = identityToken.getProviderType();

         m_lifetime = authTokenConfig.getSetting(AuthTokenConfig.TokenLifetime);
         m_lifetimeShorter = authTokenConfig.getSetting(AuthTokenConfig.LifetimeShorter);

         // Get secure token util object
         SecureTokenUtil secTokenUtil = getSecureTokenUtilObj(true);
         if (secTokenUtil != null)
         {
            // Obtain secure token template document
            Document tokenDoc = SecureTokenUtil.getSecureTokenTemplate();
            if (tokenDoc != null)
            {
               // Secure token template obtained, now get its top most element.
               Node tokenElement = tokenDoc.getDocumentElement();

               // Add authentication token payload to the token
               Node soapBodyElement = SecureTokenUtil.findChildNodeNS(tokenElement, "http://www.w3.org/2003/05/soap-envelope", "Body");
               Node authTokenElement = tokenDoc.createElement("auth_token");
               soapBodyElement.appendChild(authTokenElement);
               Node idenTokenElement = tokenDoc.createElement("ident_token");
               authTokenElement.appendChild(idenTokenElement);
               Node idenTokenProviderElement = tokenDoc.createElement("ident_token_provider");
               idenTokenProviderElement.setTextContent(m_identityTokenType);
               idenTokenElement.appendChild(idenTokenProviderElement);
               Node idenTokenDataElement = tokenDoc.createElement("ident_token_data");
               idenTokenDataElement.setTextContent(m_identityToken);
               idenTokenElement.appendChild(idenTokenDataElement);

               // Secure the token
               secTokenUtil.secure(tokenDoc, Integer.valueOf(m_lifetime).intValue());

               // Now save the token as a string
               ByteArrayOutputStream outStream = null;
               try
               {
                  outStream = new ByteArrayOutputStream();
                  OutputFormat format = new OutputFormat(tokenDoc);
                  XMLSerializer serializer = new XMLSerializer(outStream, format);
                  serializer.serialize(tokenDoc.getDocumentElement());
                  m_token = new String(outStream.toByteArray(), "UTF-8");
               }
               finally
               {
                  if (outStream != null)
                  {
                     try
                     {
                        outStream.flush();
                        outStream.close();
                     }
                     catch (Exception e)
                     {
                        // Do nothing
                     }
                  }
               }
            }
            else
            {
               // Failed to obtain secure token template
               m_log.warn("Constructor()- Failed to obtain secure token template");
               throw new Exception("AuthToken.Constructor()- Failed to obtain secure token template");
            }
         }
         else
         {
            // Failed to obtain secure token util object
            m_log.warn("Constructor()- Failed to obtain secure token util object");
            throw new Exception("SessionToken.Constructor()- Failed to obtain secure token util object");
         }
      }
      else
      {
         m_log.error("Constructor()- Error: Missing authentication token config for " + targetService);
         throw new Exception("Error: Missing authentication token config for " + targetService);
      }
   }

   /**
    * Constructor given an authentication token string. The constructor
    * validates the token as part of its processing.
    *
    * @param token         Authentication token string.
    * @param encodedToken  Indication of whether or not the token is Base64 encoded.
    * @throws Exception
    */
   public AuthToken(String token,
                    boolean encodedToken) throws Exception
   {
      Reader tokenReader = null;
      InputStream inStream = null;

      try
      {
         // Decode the token string if necessary
         if (encodedToken)
            m_token = Base64Coder.decodeUTF8(token);
         else
            m_token = token;

         // Now instantiate token document with the token string
         tokenReader = new StringReader(m_token);
         Constants.setSignatureSpecNSprefix("");

         DOMParser parser = new DOMParser();
         parser.setFeature("http://xml.org/sax/features/namespaces", true);
         parser.parse(new InputSource(tokenReader));
         Document tokenDoc = parser.getDocument();

         // Obtain secure token util object
         SecureTokenUtil secTokenUtil = getSecureTokenUtilObj(false);
         if (secTokenUtil != null)
         {
            // Use the secure token util to validate the token
            if (secTokenUtil.valid(tokenDoc))
            {
               // The authentication token is valid, now obtain the identity token
               // information from it.
               Node tokenElement = tokenDoc.getDocumentElement();
               if (tokenElement != null)
               {
                  Node soapBodyElement = SecureTokenUtil.findChildNodeNS(tokenElement,
                                                                         "http://www.w3.org/2003/05/soap-envelope",
                                                                         "Body");
                  if (soapBodyElement != null)
                  {
                     Node authTokenElement = SecureTokenUtil.findChildNode(soapBodyElement, "auth_token");
                     if (authTokenElement != null)
                     {
                        Node identTokenElement = SecureTokenUtil.findChildNode(authTokenElement, "ident_token");
                        if (identTokenElement != null)
                        {
                           Node identTokenProviderElement = SecureTokenUtil.findChildNode(identTokenElement,
                                                                                          "ident_token_provider");
                           Node identTokenDataElement = SecureTokenUtil.findChildNode(identTokenElement,
                                                                                      "ident_token_data");
                           if (identTokenProviderElement != null
                               && identTokenDataElement != null)
                           {
                              m_identityTokenType = identTokenProviderElement.getTextContent();
                              m_identityToken = identTokenDataElement.getTextContent();
                           }
                           else
                           {
                              // Missing identity token element child
                              m_log.warn("Constructor(String)- Unable to obtain identity token element child");
                              throw new Exception("AuthToken.Constructor(String)- Unable to obtain identity token element child");
                           }
                        }
                        else
                        {
                           // Missing ident token element
                           m_log.warn("Constructor(String)- Unable to obtain ident token element");
                           throw new Exception("AuthToken.Constructor(String)- Unable to obtain ident token element");
                        }
                     }
                     else
                     {
                        // Missing auth token element
                        m_log.warn("Constructor(String)- Unable to obtain auth token element");
                        throw new Exception("AuthToken.Constructor(String)- Unable to obtain auth token element");
                     }
                  }
                  else
                  {
                     // Invalid SOAP message
                     m_log.warn("Constructor(String)- Unable to obtain soap body element");
                     throw new Exception("AuthToken.Constructor(String)- Unable to obtain soap body element");
                  }
               }
               else
               {
                  // Invalid XML document
                  m_log.warn("Constructor(String)- Unable to obtain document element");
                  throw new Exception("AuthToken.Constructor(String)- Unable to obtain document element");
               }
            }
            else
            {
               // Message verification failed
               m_log.warn("Constructor(String)- Invalid Auth Token");
               throw new Exception("AuthToken.Constructor(String)- Invalid Auth Token");
            }
         }
         else
         {
            // Failed to obtain secure token util object
            m_log.warn("Constructor(String)- Failed to obtain secure token util object");
            throw new Exception("AuthToken.Constructor(String)- Failed to obtain secure token util object");
         }
      }
      finally
      {
         if (tokenReader != null)
         {
            try
            {
               tokenReader.close();
            }
            catch (IOException e)
            {
               // Do nothing
            }
         }
      }
   }

   /**
    * Gets object to be utilized for creating and verifying secure tokens.
    *
    * @param serverMode       True if to be used to secure tokens.
    * @return SecuteTokenUtil object or null if the method fails.
    */
   private static synchronized SecureTokenUtil getSecureTokenUtilObj(boolean serverMode)
   {
      // Proceed based on the mode spacified
      if (serverMode)
      {
         // Instantiate the server object if it has not been instantiated already
         if (m_serverSecTokenUtil == null)
         {
            try
            {
               // Instantiate secure token object to be utilized in server type operations
               m_serverSecTokenUtil = new SecureTokenUtil(true);
            }
            catch (Exception e)
            {
               // Exception caught
               m_log.warn("getSecureTokenUtilObj()- Exception caught, message = " + e.getMessage());
            }
         }

         // Return the object
         return m_serverSecTokenUtil;
      }
      else
      {
         // Instantiate the client object if it has not been instantiated already
         if (m_clientSecTokenUtil == null)
         {
            try
            {
               // Instantiate secure token object to be utilized in server type operations
               m_clientSecTokenUtil = new SecureTokenUtil(false);
            }
            catch (Exception e)
            {
               // Exception caught
               m_log.warn("getSecureTokenUtilObj()- Exception caught, message = " + e.getMessage());
            }
         }

         // Return the object
         return m_clientSecTokenUtil;
      }
   }

   /**
    * Returns a string representing the authentication token.
    *
    * @return  Authentication token string.
    * @throws Exception
    */
   public final String serialize() throws Exception
   {
      return Base64Coder.encodeUTF8(m_token);
   }

   /**
    * Returns the lifetime of the token.
    * <p>
    * Note: It is only valid to execute this procedure if its called on an object
    * instantiated via the constructor which takes a lifetime parameter.
    *
    * @return  Authentication token lifetime.
    * @throws Exception
    */
   public final String getLifetime() throws Exception
   {
      // Throw exeption if the lifetime parameter is not set
      if (m_lifetime.length() == 0)
      {
         m_log.error("getLifetime() - Called when lifetime is not set");
         throw new Exception("Error: Called getLifetime while not set");
      }

      return Integer.toString(Integer.valueOf(m_lifetime).intValue() - Integer.valueOf(m_lifetimeShorter).intValue());
   }

   /**
    * Returns the identity token.
    *
    * @return  Identity token string.
    */
   public final String getIdentityToken()
   {
      return m_identityToken;
   }

   /**
    * Returns the identity token type.
    *
    * @return  Identity token type.
    */
   public final String getIdentityTokenType()
   {
      return m_identityTokenType;
   }

   /**
    * Validates an authentication token. If successful it
    * returns a string containing the identity token associated
    * with the authentication token; otherwise it returns NULL;
    * <p>
    * Note, the routine assumes that the token is not encoded.
    *
    * @param authTokenString  Authentication token string.
    * @return  Identity token string.
    */
   public static String validate(String authTokenString)
   {
      m_log.debug("validate()- Start");

      // Instantiate the AuthToken, this validates the token itself.
      try
      {
         AuthToken authToken = new AuthToken(authTokenString, false);

         // If we are here is because the token validation succeeded,
         // return the identity token string.
         m_log.debug("validate()- Returning identity token");
         return authToken.getIdentityToken();

      }
      catch (Exception e)
      {
         // The validation of one of the tokens failed
         m_log.warn("validate()- Exception caught during token validation, msg: " + e.getMessage());
         return null;
      }
   }
}
