com.alfaariss.oa.profile.aselect.processor.handler.AbstractAPIHandler.java Source code

Java tutorial

Introduction

Here is the source code for com.alfaariss.oa.profile.aselect.processor.handler.AbstractAPIHandler.java

Source

/*
 * Asimba Server
 * 
 * Copyright (C) 2012 Asimba
 * Copyright (C) 2007-2010 Alfa & Ariss B.V.
 * 
 * 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 should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see www.gnu.org/licenses
 * 
 * Asimba - Serious Open Source SSO - More information on www.asimba.org
 * 
 */
package com.alfaariss.oa.profile.aselect.processor.handler;

import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.Signature;
import java.security.cert.Certificate;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;
import java.util.Vector;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.w3c.dom.Element;

import com.alfaariss.oa.OAException;
import com.alfaariss.oa.RequestorEvent;
import com.alfaariss.oa.SystemErrors;
import com.alfaariss.oa.api.attribute.IAttributes;
import com.alfaariss.oa.api.configuration.IConfigurationManager;
import com.alfaariss.oa.api.logging.IAuthority;
import com.alfaariss.oa.api.requestor.IRequestor;
import com.alfaariss.oa.api.tgt.ITGT;
import com.alfaariss.oa.api.tgt.TGTEventError;
import com.alfaariss.oa.api.tgt.TGTListenerException;
import com.alfaariss.oa.api.user.IUser;
import com.alfaariss.oa.authentication.remote.aselect.idp.storage.ASelectIDP;
import com.alfaariss.oa.engine.core.Engine;
import com.alfaariss.oa.engine.core.authentication.AuthenticationException;
import com.alfaariss.oa.engine.core.authentication.AuthenticationProfile;
import com.alfaariss.oa.engine.core.authentication.factory.IAuthenticationProfileFactory;
import com.alfaariss.oa.engine.core.crypto.CryptoException;
import com.alfaariss.oa.engine.core.crypto.CryptoManager;
import com.alfaariss.oa.engine.core.idp.IDPStorageManager;
import com.alfaariss.oa.engine.core.idp.storage.IIDP;
import com.alfaariss.oa.engine.core.requestor.RequestorPool;
import com.alfaariss.oa.engine.core.requestor.factory.IRequestorPoolFactory;
import com.alfaariss.oa.engine.core.server.Server;
import com.alfaariss.oa.engine.core.session.factory.ISessionFactory;
import com.alfaariss.oa.engine.core.tgt.factory.ITGTAliasStore;
import com.alfaariss.oa.engine.core.tgt.factory.ITGTFactory;
import com.alfaariss.oa.profile.aselect.ASelectErrors;
import com.alfaariss.oa.profile.aselect.ASelectException;
import com.alfaariss.oa.profile.aselect.binding.IBinding;
import com.alfaariss.oa.profile.aselect.binding.IRequest;
import com.alfaariss.oa.profile.aselect.binding.IResponse;
import com.alfaariss.oa.profile.aselect.processor.ASelectProcessor;
import com.alfaariss.oa.util.logging.AbstractEventLogItem;
import com.alfaariss.oa.util.logging.RequestorEventLogItem;

/**
 * Abstract API request handler.
 *
 * <br><br><i>Partitially based on sources from A-Select (www.a-select.org).</i>
 * 
 * @author MHO
 * @author Alfa & Ariss
 *
 */
public class AbstractAPIHandler implements IAuthority {
    /** system logger */
    protected Log _logger;
    /** event logger */
    protected Log _eventLogger;
    /** Server object */
    protected Server _OAServer;
    /** session factory */
    protected ISessionFactory _sessionFactory;
    /** requestor pool factory */
    protected IRequestorPoolFactory _requestorPoolFactory;
    /** authentication profile factory */
    protected IAuthenticationProfileFactory _authnProfileFactory;
    /** tgt factory */
    protected ITGTFactory _tgtFactory;
    /** redirect url */
    protected String _sRedirectURL;
    /** enabled */
    protected boolean _bEnabled;
    /** Hashtable containing the authsp_level per authenticationprofile */
    protected Hashtable<String, Integer> _htAuthSPLevels;
    /** Default authsp_level value */
    protected int _iDefaultAuthSPLevel;
    /** Hashtable containing the app_level per requestorpool */
    protected Hashtable<String, ASelectRequestorPool> _htASelectRequestorPools;
    /** Default app_level value*/
    protected int _iDefaultAppLevel;
    /** Requestors alias store */
    protected ITGTAliasStore _aliasStoreSPRole;
    /** IdP's alias store */
    protected ITGTAliasStore _aliasStoreIDPRole;
    /** IDP Storage manager */
    protected IDPStorageManager _idpStorageManager;

    private final static String PROPERTY_SIGN_REQUESTS = ".sign.requests";
    private final static String PROPERTY_APP_LEVEL = ".app_level";
    private final static String PROPERTY_UID_ATTRIBUTE = ".uid.attribute";
    private final static String PROPERTY_UID_OPAQUE_ENABLED = ".uid.opaque.enabled";
    private final static String PROPERTY_UID_OPAQUE_SALT = ".uid.opaque.salt";
    private final static String PROPERTY_AUTHSP_LEVEL = ".authsp_level";

    private String _sProfileID;
    /** crypto manager */
    private CryptoManager _cryptoManager;

    /**
     * Creates the object.
     *
     * The object can be disabled. This should be checked before the object is 
     * used, because the object won't be fully initialized if its disabled.
     * The object is disabled if no 'enabled' item is found in the supplied 
     * configuration section or the 'disabled' parameter has the value 'false'.
     *  
     * @param oConfigurationManager the configuration manager
     * @param eConfig the config section containing the configuration of this object
     * @param sRedirectURL the full URL to this profile or <code>null</code> (for loadbalanced environments)
     * @param htAuthSPLevels Hashtable containing authsp_levels per authentication profile
     * @param iDefaultAuthSPLevel Default authsp_level
     * @param sProfileID The ID of the OA profile
     * @throws OAException if the creation fails
     */
    public AbstractAPIHandler(IConfigurationManager oConfigurationManager, Element eConfig, String sRedirectURL,
            Hashtable<String, Integer> htAuthSPLevels, int iDefaultAuthSPLevel, String sProfileID)
            throws OAException {
        try {
            _logger = LogFactory.getLog(AbstractAPIHandler.class);
            _eventLogger = LogFactory.getLog(Engine.EVENT_LOGGER);

            _htAuthSPLevels = htAuthSPLevels;
            _iDefaultAuthSPLevel = iDefaultAuthSPLevel;
            _sProfileID = sProfileID;

            _bEnabled = false;
            if (eConfig != null)//if no config supplied, then component is disabled
            {
                String sEnabled = oConfigurationManager.getParam(eConfig, "enabled");
                if (sEnabled == null) {
                    _logger.info("No optional 'enabled' parameter found in handler section in configuration");
                    _bEnabled = true;
                } else if (sEnabled.equalsIgnoreCase("true")) {
                    _logger.info("Request handler is enabled");
                    _bEnabled = true;
                } else if (sEnabled.equalsIgnoreCase("false")) {
                    _logger.info("Request handler is disabled");
                    _bEnabled = false;
                } else {
                    _logger.error(
                            "Wrong 'enabled' parameter found in handler section in configuration: " + sEnabled);
                    throw new OAException(SystemErrors.ERROR_CONFIG_READ);
                }
            }

            if (_bEnabled) {
                Engine engine = Engine.getInstance();
                _OAServer = engine.getServer();
                _sessionFactory = engine.getSessionFactory();
                _requestorPoolFactory = engine.getRequestorPoolFactory();
                _tgtFactory = engine.getTGTFactory();
                _aliasStoreSPRole = _tgtFactory.getAliasStoreSP();
                _aliasStoreIDPRole = _tgtFactory.getAliasStoreIDP();
                _idpStorageManager = engine.getIDPStorageManager();
                _authnProfileFactory = engine.getAuthenticationProfileFactory();

                _cryptoManager = engine.getCryptoManager();
                if (_cryptoManager == null) {
                    _logger.error("No crypto manager available");
                    throw new OAException(SystemErrors.ERROR_INIT);
                }
                _sRedirectURL = sRedirectURL;

                String sDefaultAppLevel = oConfigurationManager.getParam(eConfig, "app_level");
                if (sDefaultAppLevel == null) {
                    _logger.error("No default 'app_level' item in handler section found in configuration");
                    throw new OAException(SystemErrors.ERROR_CONFIG_READ);
                }

                try {
                    _iDefaultAppLevel = Integer.valueOf(sDefaultAppLevel);
                } catch (NumberFormatException e) {
                    _logger.error(
                            "The configured default 'app_level' parameter isn't a number: " + sDefaultAppLevel, e);
                    throw new OAException(SystemErrors.ERROR_INIT);
                }
                _logger.info("Configured default 'app_level': " + sDefaultAppLevel);

                _htASelectRequestorPools = new Hashtable<String, ASelectRequestorPool>();
                Element eRequestorPool = oConfigurationManager.getSection(eConfig, "requestorpool");
                while (eRequestorPool != null) {
                    ASelectRequestorPool oASRequestorPool = new ASelectRequestorPool(oConfigurationManager,
                            eRequestorPool);

                    String sPoolId = oASRequestorPool.getID();
                    if (_htASelectRequestorPools.containsKey(sPoolId)) {
                        _logger.warn("The configured 'requestorpool' doesn't have a unique id: " + sPoolId);
                        throw new OAException(SystemErrors.ERROR_INIT);
                    }

                    if (!_requestorPoolFactory.isPool(sPoolId)) {
                        _logger.warn(
                                "The configured 'requestorpool' doesn't exist as a requestor pool: " + sPoolId);
                        throw new OAException(SystemErrors.ERROR_INIT);
                    }

                    _htASelectRequestorPools.put(sPoolId, oASRequestorPool);
                    _logger.info("Configured: " + oASRequestorPool);
                    eRequestorPool = oConfigurationManager.getNextSection(eRequestorPool);
                }
            }
        } catch (OAException e) {
            throw e;
        } catch (Exception e) {
            _logger.fatal("Internal error during object creation", e);
            throw new OAException(SystemErrors.ERROR_INTERNAL);
        }
    }

    /**
     * Verify if the object is enabled.
     * @return TRUE if this handler is enabled
     */
    public boolean isEnabled() {
        return _bEnabled;
    }

    /**
     * @see IAuthority#getAuthority()
     */
    public String getAuthority() {
        return ASelectProcessor.AUTHORITY_NAME;
    }

    /**
     * Process an asynchronous logout request revieved from a requestor.
     * 
     * @param oServletRequest HTTP servlet request object
     * @param oBinding The binding object
     * @param sRequestorID requestor id or NULL if app id supplied
     * @param sAppID app id or NULL if requestor id supplied 
     * @param credentials aselect_credentials
     * @throws ASelectException if request handling failed
     * @since 1.4
     */
    public void doRequestorSynchronousLogout(HttpServletRequest oServletRequest, IBinding oBinding,
            String sRequestorID, String sAppID, String credentials) throws ASelectException {
        AbstractEventLogItem oLogItem = null;
        try {
            if (sRequestorID == null && sAppID == null) {
                _logger.debug("No 'requestor' or 'app_id' found in request");
                throw new ASelectException(ASelectErrors.ERROR_ASELECT_SERVER_INVALID_REQUEST);
            }

            if (credentials == null) {
                StringBuffer sbError = new StringBuffer("No '");
                sbError.append(ASelectProcessor.PARAM_ASELECT_CREDENTIALS);
                sbError.append("' found in request");
                _logger.debug(sbError.toString());
                throw new ASelectException(ASelectErrors.ERROR_ASELECT_SERVER_INVALID_REQUEST);
            }

            IRequest oRequest = oBinding.getRequest();

            if (sRequestorID == null)
                sRequestorID = sAppID;

            IRequestor oRequestor = _requestorPoolFactory.getRequestor(sRequestorID);
            if (oRequestor == null) {
                _logger.debug("Unknown 'requestor' or 'app_id' found in request: " + sRequestorID);
                throw new ASelectException(ASelectErrors.ERROR_ASELECT_SERVER_UNKNOWN_APP);
            }

            String reason = (String) oRequest.getParameter(ASelectProcessor.PARAM_REASON);
            if (reason != null) {
                StringBuffer sbDebug = new StringBuffer("Received optional '");
                sbDebug.append(ASelectProcessor.PARAM_REASON);
                sbDebug.append("' in request from requestor: ");
                sbDebug.append(oRequestor.getID());
                _logger.debug(sbDebug.toString());
            }

            IResponse oResponse = oBinding.getResponse();
            if (oResponse == null) {
                _logger.error("No response for request");
                throw new ASelectException(ASelectErrors.ERROR_ASELECT_INTERNAL_ERROR);
            }

            String sResultCode = ASelectErrors.ERROR_ASELECT_INTERNAL_ERROR;
            try {
                if (!oRequestor.isEnabled()) {
                    StringBuffer sbError = new StringBuffer("Disabled '");
                    sbError.append(ASelectProcessor.PARAM_LOCAL_IDP);
                    sbError.append("' found in request: ");
                    sbError.append(oRequestor.getID());
                    _logger.debug(sbError.toString());
                    throw new ASelectException(ASelectErrors.ERROR_ASELECT_SERVER_UNKNOWN_APP);
                }

                RequestorPool oRequestorPool = _requestorPoolFactory.getRequestorPool(oRequestor.getID());
                if (oRequestorPool == null) {
                    _logger.warn("Requestor not available in a pool: " + oRequestor.getID());
                    throw new ASelectException(ASelectErrors.ERROR_ASELECT_INTERNAL_ERROR);
                }

                if (!oRequestorPool.isEnabled()) {
                    StringBuffer sbError = new StringBuffer("Requestor '");
                    sbError.append(oRequestor.getID());
                    sbError.append("' is found in a disabled requestor pool: ");
                    sbError.append(oRequestorPool.getID());
                    _logger.warn(sbError.toString());
                    throw new ASelectException(ASelectErrors.ERROR_ASELECT_INTERNAL_ERROR);
                }

                ASelectRequestorPool oASRequestorPool = _htASelectRequestorPools.get(oRequestorPool.getID());
                if (doSigning(oRequestorPool, oASRequestorPool, oRequestor)) {
                    String sSignature = (String) oRequest.getParameter(ASelectProcessor.PARAM_SIGNATURE);
                    if (sSignature == null) {
                        StringBuffer sbError = new StringBuffer("No '");
                        sbError.append(ASelectProcessor.PARAM_SIGNATURE);
                        sbError.append("' found in request");
                        _logger.debug(sbError.toString());

                        throw new ASelectException(ASelectErrors.ERROR_ASELECT_SERVER_INVALID_REQUEST);
                    }

                    Hashtable<String, String> htSignatureData = new Hashtable<String, String>();
                    htSignatureData.put(ASelectProcessor.PARAM_ASELECT_CREDENTIALS, credentials);
                    if (sRequestorID != null)
                        htSignatureData.put(ASelectProcessor.PARAM_REQUESTORID, sRequestorID);
                    if (sAppID != null)
                        htSignatureData.put(ASelectProcessor.PARAM_APPID, sAppID);
                    if (reason != null)
                        htSignatureData.put(ASelectProcessor.PARAM_REASON, reason);
                    if (!verifySignature(sSignature, oRequestor.getID(), htSignatureData)) {
                        _logger.error(
                                "Invalid signature for request from requestor with id: " + oRequestor.getID());
                        throw new ASelectException(ASelectErrors.ERROR_ASELECT_SERVER_INVALID_REQUEST);
                    }
                }

                if (reason != null && !ASelectProcessor.VALUE_REASON_TIMEOUT.equalsIgnoreCase(reason)) {
                    _logger.debug("Invalid reason in request from SP with id: " + oRequestor.getID());
                    throw new ASelectException(ASelectErrors.ERROR_ASELECT_SERVER_INVALID_REQUEST);
                }

                RequestorEvent logoutResult = RequestorEvent.LOGOUT_FAILED;
                if (_aliasStoreSPRole == null) {
                    _logger.debug("TGT Factory has no alias support");

                    sResultCode = ASelectErrors.ERROR_LOGOUT_FAILED;
                    logoutResult = RequestorEvent.LOGOUT_FAILED;
                } else {
                    sResultCode = ASelectErrors.ERROR_ASELECT_SUCCESS;
                    logoutResult = RequestorEvent.LOGOUT_SUCCESS;

                    String sTGTID = _aliasStoreSPRole.getTGTID(BrowserHandler.ALIAS_TYPE_CREDENTIALS,
                            oRequestor.getID(), credentials);
                    if (sTGTID != null) {
                        ITGT tgt = _tgtFactory.retrieve(sTGTID);
                        if (tgt != null && !tgt.isExpired()) {
                            //DD remove the credentials alias, so offline logout won't be triggered back to this requestor
                            _aliasStoreSPRole.removeAlias(BrowserHandler.ALIAS_TYPE_CREDENTIALS, oRequestor.getID(),
                                    credentials);

                            if (reason != null && tgt.getRequestorIDs().size() > 1) {//DD If reason == timeout then do not expire the tgt
                                tgt.removeRequestorID(oRequestor.getID());
                                tgt.persist();
                                sResultCode = ASelectErrors.ERROR_LOGOUT_PARTIALLY;
                                logoutResult = RequestorEvent.LOGOUT_PARTIALLY;
                            } else {
                                try {
                                    if (reason != null) {
                                        tgt.clean();//performs the expire event
                                    } else {
                                        tgt.expire();
                                        tgt.persist();//performs the remove event
                                    }
                                } catch (TGTListenerException e) {
                                    logoutResult = getLogoutResult(e.getErrors());
                                    switch (logoutResult) {
                                    case LOGOUT_PARTIALLY: {
                                        sResultCode = ASelectErrors.ERROR_LOGOUT_PARTIALLY;
                                        break;
                                    }
                                    case LOGOUT_FAILED:
                                    default: {
                                        sResultCode = ASelectErrors.ERROR_LOGOUT_FAILED;
                                        break;
                                    }
                                    }
                                }
                            }
                        }
                    }
                }

                oLogItem = new RequestorEventLogItem(null, null, null, logoutResult, null,
                        oServletRequest.getRemoteAddr(), null, this, "slogout SP role");
            } catch (ASelectException e) {
                sResultCode = e.getMessage();

                if (sResultCode.equals(ASelectErrors.ERROR_ASELECT_SERVER_INVALID_REQUEST))
                    throw e;

                oLogItem = new RequestorEventLogItem(null, null, null, RequestorEvent.REQUEST_INVALID, null,
                        oServletRequest.getRemoteAddr(), oRequestor.getID(), this,
                        "slogout SP role: " + sResultCode);
            }

            oResponse.setParameter(ASelectProcessor.PARAM_RESULT_CODE, sResultCode);

            _eventLogger.info(oLogItem);

            oResponse.send();
        } catch (ASelectException e) {
            throw e;
        } catch (OAException e) {
            oLogItem = new RequestorEventLogItem(null, null, null, RequestorEvent.REQUEST_INVALID, null,
                    oServletRequest.getRemoteAddr(), null, this, "request=logout: " + e.getMessage());
            _eventLogger.info(oLogItem);

            throw new ASelectException(e.getMessage());
        } catch (Exception e) {
            oLogItem = new RequestorEventLogItem(null, null, null, RequestorEvent.INTERNAL_ERROR, null,
                    oServletRequest.getRemoteAddr(), null, this, "request=logout");
            _eventLogger.info(oLogItem);

            _logger.fatal("Internal error during 'logout' process", e);
            throw new ASelectException(ASelectErrors.ERROR_ASELECT_INTERNAL_ERROR);
        }
    }

    /**
     * Process a synchronous logout request received from an organization.
     * 
     * @param oServletRequest HTTP servlet request object
     * @param oBinding The binding object
     * @param sLocalOrganization local_organization
     * @param credentials aselect_credentials
     * @throws ASelectException if request handling failed
     * @since 1.4
     */
    public void doOrganizationSynchronousLogout(HttpServletRequest oServletRequest, IBinding oBinding,
            String sLocalOrganization, String credentials) throws ASelectException {
        AbstractEventLogItem oLogItem = null;
        try {
            if (sLocalOrganization == null) {
                StringBuffer sbError = new StringBuffer("No '");
                sbError.append(ASelectProcessor.PARAM_LOCAL_IDP);
                sbError.append("' found in request");
                _logger.debug(sbError.toString());

                throw new ASelectException(ASelectErrors.ERROR_ASELECT_SERVER_INVALID_REQUEST);
            }

            if (credentials == null) {
                StringBuffer sbError = new StringBuffer("No '");
                sbError.append(ASelectProcessor.PARAM_ASELECT_CREDENTIALS);
                sbError.append("' found in request");
                _logger.debug(sbError.toString());
                throw new ASelectException(ASelectErrors.ERROR_ASELECT_SERVER_INVALID_REQUEST);
            }

            IRequest oRequest = oBinding.getRequest();

            IIDP idp = _idpStorageManager.getIDP(sLocalOrganization);
            if (idp == null) {
                _logger.debug("Unknown 'local_organization' found in request: " + sLocalOrganization);
                throw new ASelectException(ASelectErrors.ERROR_ASELECT_SERVER_UNKNOWN_APP);
            }

            String reason = (String) oRequest.getParameter(ASelectProcessor.PARAM_REASON);
            if (reason != null) {
                StringBuffer sbDebug = new StringBuffer("Received optional '");
                sbDebug.append(ASelectProcessor.PARAM_REASON);
                sbDebug.append("' in request from idp: ");
                sbDebug.append(idp.getID());
                _logger.debug(sbDebug.toString());
            }

            ASelectIDP aselectIDP = null;
            if (idp instanceof ASelectIDP) {
                aselectIDP = (ASelectIDP) idp;
            } else {
                _logger.debug("Supplied 'local_organization' is not of type ASelectIDP: " + idp.getID());
                throw new ASelectException(ASelectErrors.ERROR_ASELECT_SERVER_UNKNOWN_APP);
            }

            IResponse oResponse = oBinding.getResponse();
            if (oResponse == null) {
                _logger.error("No response for request");
                throw new ASelectException(ASelectErrors.ERROR_ASELECT_INTERNAL_ERROR);
            }

            String sResultCode = ASelectErrors.ERROR_ASELECT_INTERNAL_ERROR;
            try {
                if (aselectIDP.doSigning()) {
                    String sSignature = (String) oRequest.getParameter(ASelectProcessor.PARAM_SIGNATURE);
                    if (sSignature == null) {
                        StringBuffer sbError = new StringBuffer("No '");
                        sbError.append(ASelectProcessor.PARAM_SIGNATURE);
                        sbError.append("' found in request");
                        _logger.debug(sbError.toString());

                        throw new ASelectException(ASelectErrors.ERROR_ASELECT_SERVER_INVALID_REQUEST);
                    }

                    Hashtable<String, String> htSignatureData = new Hashtable<String, String>();
                    htSignatureData.put(ASelectProcessor.PARAM_ASELECT_CREDENTIALS, credentials);
                    if (sLocalOrganization != null)
                        htSignatureData.put(ASelectProcessor.PARAM_LOCAL_IDP, sLocalOrganization);
                    if (reason != null)
                        htSignatureData.put(ASelectProcessor.PARAM_REASON, reason);
                    if (!verifySignature(sSignature, idp.getID(), htSignatureData)) {
                        _logger.error("Invalid signature for request from IDP with id: " + idp.getID());
                        throw new ASelectException(ASelectErrors.ERROR_ASELECT_SERVER_INVALID_REQUEST);
                    }
                }

                if (reason != null && !ASelectProcessor.VALUE_REASON_TIMEOUT.equalsIgnoreCase(reason)) {
                    _logger.debug("Invalid reason in request from IDP with id: " + aselectIDP.getID());
                    throw new ASelectException(ASelectErrors.ERROR_ASELECT_SERVER_INVALID_REQUEST);
                }

                RequestorEvent logoutResult = RequestorEvent.LOGOUT_FAILED;
                if (_aliasStoreIDPRole == null) {
                    _logger.debug("TGT Factory has no alias support");

                    sResultCode = ASelectErrors.ERROR_LOGOUT_FAILED;
                    logoutResult = RequestorEvent.LOGOUT_FAILED;
                } else {
                    sResultCode = ASelectErrors.ERROR_ASELECT_SUCCESS;
                    logoutResult = RequestorEvent.LOGOUT_SUCCESS;

                    String sTGTID = _aliasStoreIDPRole.getTGTID(BrowserHandler.ALIAS_TYPE_CREDENTIALS,
                            aselectIDP.getID(), credentials);
                    if (sTGTID != null) {
                        ITGT tgt = _tgtFactory.retrieve(sTGTID);
                        if (tgt != null && !tgt.isExpired()) {
                            _aliasStoreIDPRole.removeAlias(BrowserHandler.ALIAS_TYPE_CREDENTIALS,
                                    aselectIDP.getID(), credentials);

                            if (reason != null) {//DD If reason == timeout then do not expire the tgt
                                sResultCode = ASelectErrors.ERROR_LOGOUT_PARTIALLY;
                                logoutResult = RequestorEvent.LOGOUT_PARTIALLY;
                            } else {
                                tgt.expire();

                                try {
                                    tgt.persist();
                                } catch (TGTListenerException e) {
                                    logoutResult = getLogoutResult(e.getErrors());
                                    switch (logoutResult) {
                                    case LOGOUT_PARTIALLY: {
                                        sResultCode = ASelectErrors.ERROR_LOGOUT_PARTIALLY;
                                        break;
                                    }
                                    case LOGOUT_FAILED:
                                    default: {
                                        sResultCode = ASelectErrors.ERROR_LOGOUT_FAILED;
                                        break;
                                    }
                                    }
                                }
                            }
                        }
                    }
                }

                oLogItem = new RequestorEventLogItem(null, null, null, logoutResult, null,
                        oServletRequest.getRemoteAddr(), null, this, "slogout IDP role");
            } catch (ASelectException e) {
                sResultCode = e.getMessage();

                if (sResultCode.equals(ASelectErrors.ERROR_ASELECT_SERVER_INVALID_REQUEST))
                    throw e;

                oLogItem = new RequestorEventLogItem(null, null, null, RequestorEvent.REQUEST_INVALID, null,
                        oServletRequest.getRemoteAddr(), aselectIDP.getID(), this,
                        "slogout IDP role: " + sResultCode);
            }

            oResponse.setParameter(ASelectProcessor.PARAM_RESULT_CODE, sResultCode);

            _eventLogger.info(oLogItem);

            oResponse.send();
        } catch (ASelectException e) {
            throw e;
        } catch (OAException e) {
            oLogItem = new RequestorEventLogItem(null, null, null, RequestorEvent.REQUEST_INVALID, null,
                    oServletRequest.getRemoteAddr(), null, this, "request=logout: " + e.getMessage());
            _eventLogger.info(oLogItem);

            throw new ASelectException(e.getMessage());
        } catch (Exception e) {
            oLogItem = new RequestorEventLogItem(null, null, null, RequestorEvent.INTERNAL_ERROR, null,
                    oServletRequest.getRemoteAddr(), null, this, "request=logout");
            _eventLogger.info(oLogItem);

            _logger.fatal("Internal error during 'logout' process", e);
            throw new ASelectException(ASelectErrors.ERROR_ASELECT_INTERNAL_ERROR);
        }
    }

    /**
     * Serialize attributes contained in a hashtable.
     * 
     * This method serializes attributes contained in a hashtable:
     * <ul>
     *  <li>They are formatted as attr1=value1&attr2=value2;...</li>
     *  <li>If a "&amp;" or a "=" appears in either the attribute name
     *  or value, they are transformed to %26 or %3d respectively.</li>
     *  <li>The end result is base64 encoded.</li>
     * </ul>
     * 
     * @param oAttributes IAttributes object containing all attributes
     * @return Serialized representation of the attributes
     * @throws ASelectException If serialization fails.
     */
    protected String serializeAttributes(IAttributes oAttributes) throws ASelectException {
        String sReturn = null;
        try {
            StringBuffer sbCGI = new StringBuffer();

            Enumeration enumGatheredAttributes = oAttributes.getNames();
            while (enumGatheredAttributes.hasMoreElements()) {
                StringBuffer sbPart = new StringBuffer();

                String sKey = (String) enumGatheredAttributes.nextElement();
                Object oValue = oAttributes.get(sKey);

                if (oValue instanceof Vector) {// it's a multivalue attribute
                    Vector vValue = (Vector) oValue;
                    Enumeration eEnum = vValue.elements();
                    while (eEnum.hasMoreElements()) {
                        String sValue = (String) eEnum.nextElement();
                        sbPart.append(URLEncoder.encode(sKey + "[]", ASelectProcessor.CHARSET));
                        sbPart.append("=");
                        sbPart.append(URLEncoder.encode(sValue, ASelectProcessor.CHARSET));

                        if (eEnum.hasMoreElements())
                            sbPart.append("&");
                    }
                } else if (oValue instanceof String) {// it's a single value attribute
                    String sValue = (String) oValue;

                    sbPart.append(URLEncoder.encode(sKey, ASelectProcessor.CHARSET));
                    sbPart.append("=");
                    sbPart.append(URLEncoder.encode(sValue, ASelectProcessor.CHARSET));
                } else {
                    StringBuffer sbDebug = new StringBuffer("Attribute '");
                    sbDebug.append(sKey);
                    sbDebug.append("' has an unsupported value; is not a String: ");
                    sbDebug.append(oValue);
                    _logger.debug(sbDebug.toString());
                }

                if (sbPart.length() > 0 && sbCGI.length() > 0)
                    sbCGI.append("&");

                sbCGI.append(sbPart);
            }

            if (sbCGI.length() > 0) {
                byte[] baCGI = Base64.encodeBase64(sbCGI.toString().getBytes(ASelectProcessor.CHARSET));
                sReturn = new String(baCGI, ASelectProcessor.CHARSET);
            }
        } catch (Exception e) {
            _logger.fatal("Could not serialize attributes: " + oAttributes.toString(), e);
            throw new ASelectException(ASelectErrors.ERROR_ASELECT_INTERNAL_ERROR);
        }

        return sReturn;
    }

    /**
     * Verifies signatures for requests and the signed parameters are supplied as map.
     * @param sSignature the signature that must be verified
     * @param sKeyAlias the key alias
     * @param mapRequest the data that is signed
     * @return TRUE if the signature is valid
     * @throws ASelectException if verification failed
     */
    protected boolean verifySignature(String sSignature, String sKeyAlias, Map<String, String> mapRequest)
            throws ASelectException {
        StringBuffer sbSignatureData = new StringBuffer();
        TreeSet<String> sortedSet = new TreeSet<String>(mapRequest.keySet());
        for (Iterator<String> iter = sortedSet.iterator(); iter.hasNext();) {
            String sKey = iter.next();
            sbSignatureData.append(mapRequest.get(sKey));
        }

        return verifySignature(sSignature, sKeyAlias, sbSignatureData.toString());
    }

    /**
     * Verifies signatures for requests retrieved from a requestor.
     * @param sSignature the signature that must be verified
     * @param sKeyAlias the key alias
     * @param sData the signed data
     * @return TRUE if the signature is valid
     * @throws ASelectException if verification failed
     */
    protected boolean verifySignature(String sSignature, String sKeyAlias, String sData) throws ASelectException {
        try {
            Certificate oCertificate = _cryptoManager.getCertificate(sKeyAlias);
            if (oCertificate == null) {
                _logger.warn("No certificate object found with alias: " + sKeyAlias);
                throw new ASelectException(ASelectErrors.ERROR_ASELECT_INTERNAL_ERROR);
            }

            Signature oSignature = _cryptoManager.getSignature();
            if (oSignature == null) {
                _logger.warn("No signature object found");
                throw new ASelectException(ASelectErrors.ERROR_ASELECT_INTERNAL_ERROR);
            }

            oSignature.initVerify(oCertificate);
            oSignature.update(sData.getBytes(ASelectProcessor.CHARSET));

            byte[] baData = Base64.decodeBase64(sSignature.getBytes(ASelectProcessor.CHARSET));
            boolean bVerified = oSignature.verify(baData);
            if (!bVerified) {
                StringBuffer sbDebug = new StringBuffer("Could not verify signature '");
                sbDebug.append(sSignature);
                sbDebug.append("' for key with alias '");
                sbDebug.append(sKeyAlias);
                sbDebug.append("' with data: ");
                sbDebug.append(sData);
                _logger.debug(sbDebug.toString());
            }
            return bVerified;
        } catch (CryptoException e) {
            _logger.warn("A crypto exception occurred", e);
            throw new ASelectException(e.getMessage());
        } catch (ASelectException e) {
            throw e;
        } catch (Exception e) {
            StringBuffer sbError = new StringBuffer("Could not verify signature '");
            sbError.append(sSignature);
            sbError.append("' for key with alias: ");
            sbError.append(sKeyAlias);
            _logger.fatal(sbError.toString(), e);
            throw new ASelectException(ASelectErrors.ERROR_ASELECT_INTERNAL_ERROR);
        }
    }

    /**
     * Returns the authN profile id with the highest authsp_level value.
     *
     * @param listAuthNProfileIDs a list with authN profile id's
     * @return authN profile id
     * @throws OAException if authsp_level could not be resolved from model
     */
    protected String getHighestAuthNProfile(List<String> listAuthNProfileIDs) throws OAException {
        String sHighestProfile = null;
        int iMaxLevel = -1;
        for (String sAuthNProfileID : listAuthNProfileIDs) {
            if (_htAuthSPLevels.containsKey(sAuthNProfileID)) {
                int iAuthNProfileID = _htAuthSPLevels.get(sAuthNProfileID);
                if (iAuthNProfileID > iMaxLevel) {
                    iMaxLevel = iAuthNProfileID;
                    sHighestProfile = sAuthNProfileID;
                }
            } else {
                AuthenticationProfile authnProfile = null;
                try {
                    authnProfile = _authnProfileFactory.getProfile(sAuthNProfileID);
                } catch (AuthenticationException e) {
                    _logger.error("Authentication profile not available: " + sAuthNProfileID);
                    throw new OAException(SystemErrors.ERROR_INTERNAL);
                }

                String sLevel = (String) authnProfile.getProperty(_sProfileID + PROPERTY_AUTHSP_LEVEL);
                if (sLevel != null) {
                    try {
                        int iAuthNProfileID = Integer.valueOf(sLevel);
                        if (iAuthNProfileID > iMaxLevel) {
                            iMaxLevel = iAuthNProfileID;
                            sHighestProfile = sAuthNProfileID;
                        }
                    } catch (NumberFormatException e) {
                        StringBuffer sbError = new StringBuffer("Invalid value of the '");
                        sbError.append(_sProfileID);
                        sbError.append(PROPERTY_AUTHSP_LEVEL);
                        sbError.append("' property available: ");
                        sbError.append(sLevel);
                        _logger.error(sbError.toString());
                        throw new OAException(SystemErrors.ERROR_INTERNAL);
                    }
                }
            }
        }
        return sHighestProfile;
    }

    /**
     * Resolves the authsp_level for the specified authentication profile.
     * 
     * @param sAuthNProfileID The profile id for which the authsp_level should be resolved
     * @return The authsp_level
     * @throws OAException if authsp_level could not be resolved from model
     * @since 1.1
     */
    protected Integer getAuthSPLevel(String sAuthNProfileID) throws OAException {
        Integer intAuthSPLevel = _iDefaultAuthSPLevel;
        if (_htAuthSPLevels.containsKey(sAuthNProfileID))
            intAuthSPLevel = _htAuthSPLevels.get(sAuthNProfileID);
        else {
            AuthenticationProfile authnProfile = null;
            try {
                authnProfile = _authnProfileFactory.getProfile(sAuthNProfileID);
            } catch (AuthenticationException e) {
                _logger.error("Authentication profile not available: " + sAuthNProfileID);
                throw new OAException(SystemErrors.ERROR_INTERNAL);
            }

            String sLevel = (String) authnProfile.getProperty(_sProfileID + PROPERTY_AUTHSP_LEVEL);
            if (sLevel != null) {
                try {
                    intAuthSPLevel = new Integer(sLevel);
                } catch (NumberFormatException e) {
                    StringBuffer sbError = new StringBuffer("Invalid value of the '");
                    sbError.append(_sProfileID);
                    sbError.append(PROPERTY_AUTHSP_LEVEL);
                    sbError.append("' property available: ");
                    sbError.append(sLevel);
                    _logger.error(sbError.toString());
                    throw new OAException(SystemErrors.ERROR_INTERNAL);
                }
            }
        }
        return intAuthSPLevel;
    }

    /**
     * Resolves the value for the uid parameter of the A-Select protocol.
     * 
     * @param oUser The User object
     * @param oASRequestorPool Requestor Pool object or <code>null</code>
     * @param oRequestorPool OA Requestorpool
     * @param oRequestor OA Requestor
     * @return the resolved uid value
     * @throws ASelectException if no uid can be resolved
     * @throws OAException if conversion to hexadecimal fails
     */
    protected String getUid(IUser oUser, ASelectRequestorPool oASRequestorPool, RequestorPool oRequestorPool,
            IRequestor oRequestor) throws ASelectException, OAException {
        String sUid = oUser.getID();

        String sUidAttribute = (String) oRequestor.getProperty(_sProfileID + PROPERTY_UID_ATTRIBUTE);
        if (sUidAttribute == null) {
            if (oASRequestorPool != null)
                sUidAttribute = oASRequestorPool.getUidAttribute();

            if (sUidAttribute == null)
                sUidAttribute = (String) oRequestorPool.getProperty(_sProfileID + PROPERTY_UID_ATTRIBUTE);
        }

        if (sUidAttribute != null) {
            IAttributes oAttributes = oUser.getAttributes();
            sUid = (String) oAttributes.get(sUidAttribute);
            if (sUid == null) {
                StringBuffer sbError = new StringBuffer("Missing required attribute (");
                sbError.append(sUidAttribute);
                sbError.append(") to resolve uid for user with ID: ");
                sbError.append(oUser.getID());
                _logger.warn(sbError.toString());
                throw new ASelectException(ASelectErrors.ERROR_MISSING_REQUIRED_ATTRIBUTE);
            }
            //DD Remove the used attribute from the user attributes, so it will not be released to the application
            oAttributes.remove(sUidAttribute);
        }

        boolean bOpaqueUID = false;

        String sUIDOpaque = (String) oRequestor.getProperty(_sProfileID + PROPERTY_UID_OPAQUE_ENABLED);
        if (sUIDOpaque != null) {
            if ("TRUE".equalsIgnoreCase(sUIDOpaque))
                bOpaqueUID = true;
            else if (!"FALSE".equalsIgnoreCase(sUIDOpaque)) {
                StringBuffer sbError = new StringBuffer("Invalid value for requestor property '");
                sbError.append(_sProfileID);
                sbError.append(PROPERTY_UID_OPAQUE_ENABLED);
                sbError.append("': ");
                sbError.append(sUIDOpaque);

                _logger.error(sbError.toString());
                throw new OAException(SystemErrors.ERROR_INTERNAL);
            }
        } else {
            if (oASRequestorPool != null)
                bOpaqueUID = oASRequestorPool.isUidOpaque();
            if (!bOpaqueUID) {
                sUIDOpaque = (String) oRequestorPool.getProperty(_sProfileID + PROPERTY_UID_OPAQUE_ENABLED);
                if (sUIDOpaque != null) {
                    if ("TRUE".equalsIgnoreCase(sUIDOpaque))
                        bOpaqueUID = true;
                    else if (!"FALSE".equalsIgnoreCase(sUIDOpaque)) {
                        StringBuffer sbError = new StringBuffer("Invalid value for requestorpool property '");
                        sbError.append(_sProfileID);
                        sbError.append(PROPERTY_UID_OPAQUE_ENABLED);
                        sbError.append("': ");
                        sbError.append(sUIDOpaque);

                        _logger.error(sbError.toString());
                        throw new OAException(SystemErrors.ERROR_INTERNAL);
                    }
                }
            }
        }

        if (bOpaqueUID) {
            String sSalt = (String) oRequestor.getProperty(_sProfileID + PROPERTY_UID_OPAQUE_SALT);
            if (sSalt == null) {
                if (oASRequestorPool != null)
                    sSalt = oASRequestorPool.getUidOpaqueSalt();
                if (sSalt == null)
                    sSalt = (String) oRequestorPool.getProperty(_sProfileID + PROPERTY_UID_OPAQUE_SALT);
            }

            if (sSalt != null)
                sUid = sUid + sSalt;

            // the returned user ID must contain an opaque value 
            MessageDigest oMessageDigest = _cryptoManager.getMessageDigest();
            try {
                oMessageDigest.update(sUid.getBytes(ASelectProcessor.CHARSET));
                sUid = toHexString(oMessageDigest.digest());
            } catch (Exception e) {
                _logger.warn(
                        "Unable to generate '" + oMessageDigest.getAlgorithm() + "' hash from user ID: " + sUid, e);
                throw new OAException(SystemErrors.ERROR_INTERNAL);
            }
        }

        return sUid;
    }

    /**
     * Resolves the app level from ASelectRequestorPool or RequestorPool.
     *
     * @param oRequestorPool OA Requestor pool
     * @param oASRequestorPool A-Select requestor pool
     * @param oRequestor OA Requestor
     * @return The app level
     * @throws OAException
     * @since 1.1
     */
    protected String getAppLevel(RequestorPool oRequestorPool, ASelectRequestorPool oASRequestorPool,
            IRequestor oRequestor) throws OAException {
        String sAppLevel = String.valueOf(_iDefaultAppLevel);

        int iAppLevel = -1;

        String appLevel = (String) oRequestor.getProperty(_sProfileID + PROPERTY_APP_LEVEL);
        if (appLevel != null) {
            try {
                iAppLevel = Integer.valueOf(appLevel);
            } catch (NumberFormatException e) {
                StringBuffer sbError = new StringBuffer("The configured requestor property (");
                sbError.append(_sProfileID);
                sbError.append(PROPERTY_APP_LEVEL);
                sbError.append(") value isn't a number: ");
                sbError.append(appLevel);

                _logger.error(sbError.toString(), e);
                throw new OAException(SystemErrors.ERROR_INTERNAL);
            }
        } else {
            if (oASRequestorPool != null)
                iAppLevel = oASRequestorPool.getAppLevel();

            if (iAppLevel == -1) {
                appLevel = (String) oRequestorPool.getProperty(_sProfileID + PROPERTY_APP_LEVEL);
                if (appLevel != null) {
                    try {
                        iAppLevel = Integer.valueOf(appLevel);
                    } catch (NumberFormatException e) {
                        StringBuffer sbError = new StringBuffer("The configured requestorpool property (");
                        sbError.append(_sProfileID);
                        sbError.append(PROPERTY_APP_LEVEL);
                        sbError.append(") value isn't a number: ");
                        sbError.append(appLevel);

                        _logger.error(sbError.toString(), e);
                        throw new OAException(SystemErrors.ERROR_INTERNAL);
                    }
                }
            }
        }

        if (iAppLevel > 0)
            sAppLevel = String.valueOf(iAppLevel);

        return sAppLevel;
    }

    /**
     * Returns TRUE if requests must be signed.
     * 
     * Resolves signing value from ASelectRequestorPool or RequestorPool.
     * @param oRequestorPool OA Requestor pool
     * @param oASRequestorPool A-Select Requestor pool
     * @param oRequestor OA Requestor
     * @return true if requests must be signed
     * @throws OAException
     * @since 1.1
     */
    protected boolean doSigning(RequestorPool oRequestorPool, ASelectRequestorPool oASRequestorPool,
            IRequestor oRequestor) throws OAException {
        String sEnabled = (String) oRequestor.getProperty(_sProfileID + PROPERTY_SIGN_REQUESTS);
        if (sEnabled != null) {
            if ("TRUE".equalsIgnoreCase(sEnabled))
                return true;
            else if (!"FALSE".equalsIgnoreCase(sEnabled)) {
                StringBuffer sbError = new StringBuffer("The configured requestor property (");
                sbError.append(_sProfileID);
                sbError.append(PROPERTY_SIGN_REQUESTS);
                sbError.append(") value isn't a boolean: ");
                sbError.append(sEnabled);
                _logger.error(sbError.toString());
                throw new OAException(SystemErrors.ERROR_INTERNAL);
            }
        }

        if (oASRequestorPool != null && oASRequestorPool.doSigning())
            return true;

        sEnabled = (String) oRequestorPool.getProperty(_sProfileID + PROPERTY_SIGN_REQUESTS);
        if (sEnabled != null) {
            if ("TRUE".equalsIgnoreCase(sEnabled))
                return true;
            else if (!"FALSE".equalsIgnoreCase(sEnabled)) {
                StringBuffer sbError = new StringBuffer("The configured requestorpool property (");
                sbError.append(_sProfileID);
                sbError.append(PROPERTY_SIGN_REQUESTS);
                sbError.append(") value isn't a boolean: ");
                sbError.append(sEnabled);
                _logger.error(sbError.toString());
                throw new OAException(SystemErrors.ERROR_INTERNAL);
            }
        }

        return false;
    }

    /**
     * Returns the logout result as requestor event.
     *
     * @param listErrors containing all TGT event errors to be verified
     * @return the resulting logout requestor event
     * @since 1.4
     */
    private RequestorEvent getLogoutResult(List<TGTEventError> listErrors) {
        RequestorEvent event = RequestorEvent.LOGOUT_FAILED;
        for (TGTEventError eventError : listErrors) {
            switch (eventError.getCode()) {
            case USER_LOGOUT_PARTIALLY: {
                event = RequestorEvent.LOGOUT_PARTIALLY;
                break;
            }
            case USER_LOGOUT_IN_PROGRESS:
            case USER_LOGOUT_FAILED:
            default: {
                //do not search further; logout failed already.
                return RequestorEvent.LOGOUT_FAILED;
            }
            }
        }
        return event;
    }

    /**
     * Hexstring encoding.
     * 
     * Outputs a hex-string representation of a byte array.
     * This method returns the hexadecimal String representation of a byte
     * array. 
     * 
     * Example: 
     * For input <code>[0x13, 0x2f, 0x98, 0x76]</code>, this method returns a
     * String object containing <code>"132F9876"</code>.
     * 
     * DD For backwards compatibly the hex presentation is converted to upper case.
     * @param baBytes Source byte array.
     * @return a String object representing <code>baBytes</code> in hexadecimal 
     *  format.
     *  @see Hex#encodeHex(byte[])
     */
    private static String toHexString(byte[] baBytes) {
        char[] ca = Hex.encodeHex(baBytes);
        String s = new String(ca).toUpperCase();
        return s;
    }
}