/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.syncope.core.logic;

import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.lang.reflect.Method;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.annotation.Resource;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.cxf.rs.security.jose.jws.JwsJwtCompactConsumer;
import org.apache.cxf.rs.security.jose.jws.JwsSignatureVerifier;
import org.apache.cxf.rs.security.saml.sso.SSOValidatorResponse;
import org.apache.syncope.common.lib.SyncopeClientException;
import org.apache.syncope.common.lib.to.AttrTO;
import org.apache.syncope.common.lib.to.EntityTO;
import org.apache.syncope.common.lib.to.SAML2RequestTO;
import org.apache.syncope.common.lib.to.SAML2LoginResponseTO;
import org.apache.syncope.common.lib.to.SAML2ReceivedResponseTO;
import org.apache.syncope.common.lib.to.UserTO;
import org.apache.syncope.common.lib.types.CipherAlgorithm;
import org.apache.syncope.common.lib.types.ClientExceptionType;
import org.apache.syncope.common.lib.types.SAML2BindingType;
import org.apache.syncope.common.lib.types.StandardEntitlement;
import org.apache.syncope.core.logic.saml2.SAML2ReaderWriter;
import org.apache.syncope.core.logic.saml2.SAML2IdPCache;
import org.apache.syncope.core.logic.saml2.SAML2IdPEntity;
import org.apache.syncope.core.logic.saml2.SAML2UserManager;
import org.apache.syncope.core.persistence.api.dao.AccessTokenDAO;
import org.apache.syncope.core.persistence.api.dao.NotFoundException;
import org.apache.syncope.core.persistence.api.dao.SAML2IdPDAO;
import org.apache.syncope.core.persistence.api.entity.SAML2IdP;
import org.apache.syncope.core.provisioning.api.RequestedAuthnContextProvider;
import org.apache.syncope.core.provisioning.api.data.AccessTokenDataBinder;
import org.joda.time.DateTime;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.core.xml.schema.XSString;
import org.opensaml.saml.common.SAMLVersion;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.saml2.core.Assertion;
import org.opensaml.saml.saml2.core.Attribute;
import org.opensaml.saml.saml2.core.AttributeStatement;
import org.opensaml.saml.saml2.core.AuthnRequest;
import org.opensaml.saml.saml2.core.Issuer;
import org.opensaml.saml.saml2.core.LogoutRequest;
import org.opensaml.saml.saml2.core.LogoutResponse;
import org.opensaml.saml.saml2.core.NameID;
import org.opensaml.saml.saml2.core.NameIDPolicy;
import org.opensaml.saml.saml2.core.NameIDType;
import org.opensaml.saml.saml2.core.Response;
import org.opensaml.saml.saml2.core.SessionIndex;
import org.opensaml.saml.saml2.core.StatusCode;
import org.opensaml.saml.saml2.core.impl.AuthnRequestBuilder;
import org.opensaml.saml.saml2.core.impl.IssuerBuilder;
import org.opensaml.saml.saml2.core.impl.LogoutRequestBuilder;
import org.opensaml.saml.saml2.core.impl.NameIDBuilder;
import org.opensaml.saml.saml2.core.impl.NameIDPolicyBuilder;
import org.opensaml.saml.saml2.core.impl.SessionIndexBuilder;
import org.opensaml.saml.saml2.metadata.AssertionConsumerService;
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
import org.opensaml.saml.saml2.metadata.KeyDescriptor;
import org.opensaml.saml.saml2.metadata.NameIDFormat;
import org.opensaml.saml.saml2.metadata.SPSSODescriptor;
import org.opensaml.saml.saml2.metadata.SingleLogoutService;
import org.opensaml.saml.saml2.metadata.impl.AssertionConsumerServiceBuilder;
import org.opensaml.saml.saml2.metadata.impl.EntityDescriptorBuilder;
import org.opensaml.saml.saml2.metadata.impl.KeyDescriptorBuilder;
import org.opensaml.saml.saml2.metadata.impl.NameIDFormatBuilder;
import org.opensaml.saml.saml2.metadata.impl.SPSSODescriptorBuilder;
import org.opensaml.saml.saml2.metadata.impl.SingleLogoutServiceBuilder;
import org.opensaml.xmlsec.keyinfo.KeyInfoGenerator;
import org.opensaml.xmlsec.keyinfo.impl.X509KeyInfoGeneratorFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Component;
import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
import org.apache.syncope.core.provisioning.java.DefaultRequestedAuthnContextProvider;
import org.apache.syncope.core.spring.ApplicationContextProvider;
import org.apache.syncope.core.spring.security.AuthContextUtils;
import org.apache.syncope.core.spring.security.AuthDataAccessor;
import org.apache.syncope.core.spring.security.Encryptor;
import org.apache.syncope.core.spring.security.SecureRandomUtils;
import org.opensaml.core.xml.schema.XSAny;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.util.ResourceUtils;

@Component
public class SAML2SPLogic extends AbstractSAML2Logic<EntityTO> {

    private static final String IDP_INITIATED_RELAY_STATE = "idpInitiated";

    private static final long JWT_RELAY_STATE_DURATION = 60L;

    private static final String JWT_CLAIM_IDP_DEFLATE = "IDP_DEFLATE";

    private static final String JWT_CLAIM_IDP_ENTITYID = "IDP_ENTITYID";

    private static final String JWT_CLAIM_NAMEID_FORMAT = "NAMEID_FORMAT";

    private static final String JWT_CLAIM_NAMEID_VALUE = "NAMEID_VALUE";

    private static final String JWT_CLAIM_SESSIONINDEX = "SESSIONINDEX";

    private static final Encryptor ENCRYPTOR = Encryptor.getInstance();

    @Autowired
    private AccessTokenDataBinder accessTokenDataBinder;

    @Autowired
    private SAML2IdPCache cache;

    @Autowired
    private SAML2UserManager userManager;

    @Autowired
    private SAML2IdPDAO saml2IdPDAO;

    @Autowired
    private AccessTokenDAO accessTokenDAO;

    @Autowired
    private AuthDataAccessor authDataAccessor;

    @Autowired
    private SAML2ReaderWriter saml2rw;

    @Resource(name = "accessTokenJwsSignatureVerifier")
    private JwsSignatureVerifier jwsSignatureVerifier;

    private void validateUrl(final String url) {
        boolean isValid = true;
        if (url.contains("..")) {
            isValid = false;
        }
        if (isValid) {
            isValid = ResourceUtils.isUrl(url);
        }

        if (!isValid) {
            SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown);
            sce.getElements().add("Invalid URL: " + url);
            throw sce;
        }
    }

    private String getAssertionConsumerURL(final String spEntityID, final String urlContext) {
        String assertionConsumerUrl = spEntityID + urlContext + "/assertion-consumer";
        validateUrl(assertionConsumerUrl);
        return assertionConsumerUrl;
    }

    @PreAuthorize("isAuthenticated()")
    public void getMetadata(final String spEntityID, final String urlContext, final OutputStream os) {
        check();

        try {
            EntityDescriptor spEntityDescriptor = new EntityDescriptorBuilder().buildObject();
            spEntityDescriptor.setEntityID(spEntityID);

            SPSSODescriptor spSSODescriptor = new SPSSODescriptorBuilder().buildObject();
            spSSODescriptor.setWantAssertionsSigned(true);
            spSSODescriptor.setAuthnRequestsSigned(true);
            spSSODescriptor.addSupportedProtocol(SAMLConstants.SAML20P_NS);

            X509KeyInfoGeneratorFactory keyInfoGeneratorFactory = new X509KeyInfoGeneratorFactory();
            keyInfoGeneratorFactory.setEmitEntityCertificate(true);
            KeyInfoGenerator keyInfoGenerator = keyInfoGeneratorFactory.newInstance();
            keyInfoGenerator.generate(loader.getCredential());

            KeyDescriptor keyDescriptor = new KeyDescriptorBuilder().buildObject();
            keyDescriptor.setKeyInfo(keyInfoGenerator.generate(loader.getCredential()));
            spSSODescriptor.getKeyDescriptors().add(keyDescriptor);

            NameIDFormat nameIDFormat = new NameIDFormatBuilder().buildObject();
            nameIDFormat.setFormat(NameIDType.PERSISTENT);
            spSSODescriptor.getNameIDFormats().add(nameIDFormat);
            nameIDFormat = new NameIDFormatBuilder().buildObject();
            nameIDFormat.setFormat(NameIDType.TRANSIENT);
            spSSODescriptor.getNameIDFormats().add(nameIDFormat);

            for (SAML2BindingType bindingType : SAML2BindingType.values()) {
                AssertionConsumerService assertionConsumerService = new AssertionConsumerServiceBuilder().buildObject();
                assertionConsumerService.setIndex(bindingType.ordinal());
                assertionConsumerService.setBinding(bindingType.getUri());
                assertionConsumerService.setLocation(getAssertionConsumerURL(spEntityID, urlContext));
                spSSODescriptor.getAssertionConsumerServices().add(assertionConsumerService);
                spEntityDescriptor.getRoleDescriptors().add(spSSODescriptor);

                String sloUrl = spEntityID + urlContext + "/logout";
                validateUrl(sloUrl);

                SingleLogoutService singleLogoutService = new SingleLogoutServiceBuilder().buildObject();
                singleLogoutService.setBinding(bindingType.getUri());
                singleLogoutService.setLocation(sloUrl);
                singleLogoutService.setResponseLocation(sloUrl);
                spSSODescriptor.getSingleLogoutServices().add(singleLogoutService);
            }

            spEntityDescriptor.getRoleDescriptors().add(spSSODescriptor);
            saml2rw.sign(spEntityDescriptor);

            saml2rw.write(new OutputStreamWriter(os), spEntityDescriptor, true);
        } catch (Exception e) {
            LOG.error("While getting SP metadata", e);
            SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown);
            sce.getElements().add(e.getMessage());
            throw sce;
        }
    }

    private SAML2IdPEntity getIdP(final String entityID) {
        SAML2IdPEntity idp = null;

        SAML2IdP saml2IdP = saml2IdPDAO.findByEntityID(entityID);
        if (saml2IdP != null) {
            try {
                idp = cache.put(saml2IdP);
            } catch (Exception e) {
                LOG.error("Could not build SAML 2.0 IdP with key ", entityID, e);
                SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown);
                sce.getElements().add(e.getMessage());
                throw sce;
            }
        }

        if (idp == null) {
            throw new NotFoundException("SAML 2.0 IdP '" + entityID + "'");
        }
        return idp;
    }

    @PreAuthorize("hasRole('" + StandardEntitlement.ANONYMOUS + "')")
    public SAML2RequestTO createLoginRequest(final String spEntityID, final String idpEntityID) {
        check();

        // 1. look for IdP
        SAML2IdPEntity idp = StringUtils.isBlank(idpEntityID) ? cache.getFirst() : cache.get(idpEntityID);
        if (idp == null) {
            if (StringUtils.isBlank(idpEntityID)) {
                List<SAML2IdP> all = saml2IdPDAO.findAll();
                if (!all.isEmpty()) {
                    idp = getIdP(all.get(0).getKey());
                }
            } else {
                idp = getIdP(idpEntityID);
            }
        }
        if (idp == null) {
            throw new NotFoundException(StringUtils.isBlank(idpEntityID)
                    ? "Any SAML 2.0 IdP"
                    : "SAML 2.0 IdP '" + idpEntityID + "'");
        }

        if (idp.getSSOLocation(idp.getBindingType()) == null) {
            throw new IllegalArgumentException("No SingleSignOnService available for " + idp.getId());
        }

        // 2. create AuthnRequest
        Issuer issuer = new IssuerBuilder().buildObject();
        issuer.setValue(spEntityID);

        NameIDPolicy nameIDPolicy = new NameIDPolicyBuilder().buildObject();
        if (idp.supportsNameIDFormat(NameIDType.TRANSIENT)) {
            nameIDPolicy.setFormat(NameIDType.TRANSIENT);
        } else if (idp.supportsNameIDFormat(NameIDType.PERSISTENT)) {
            nameIDPolicy.setFormat(NameIDType.PERSISTENT);
        } else {
            throw new IllegalArgumentException("Could not find supported NameIDFormat for IdP " + idpEntityID);
        }
        nameIDPolicy.setAllowCreate(true);
        nameIDPolicy.setSPNameQualifier(spEntityID);

        RequestedAuthnContextProvider requestedAuthnContextProvider = new DefaultRequestedAuthnContextProvider();
        if (idp.getRequestedAuthnContextProviderClassName() != null) {
            try {
                Class<?> actionsClass = Class.forName(idp.getRequestedAuthnContextProviderClassName());
                requestedAuthnContextProvider = (RequestedAuthnContextProvider) ApplicationContextProvider.
                        getBeanFactory().createBean(actionsClass, AbstractBeanDefinition.AUTOWIRE_BY_TYPE, true);
            } catch (Exception e) {
                LOG.warn("Cannot instantiate '{}', reverting to {}",
                        idp.getRequestedAuthnContextProviderClassName(),
                        DefaultRequestedAuthnContextProvider.class.getName(), e);
            }
        }

        AuthnRequest authnRequest = new AuthnRequestBuilder().buildObject();
        authnRequest.setID("_" + SecureRandomUtils.generateRandomUUID().toString());
        authnRequest.setForceAuthn(false);
        authnRequest.setIsPassive(false);
        authnRequest.setVersion(SAMLVersion.VERSION_20);
        authnRequest.setProtocolBinding(idp.getBindingType().getUri());
        authnRequest.setIssueInstant(new DateTime());
        authnRequest.setIssuer(issuer);
        authnRequest.setNameIDPolicy(nameIDPolicy);
        authnRequest.setRequestedAuthnContext(requestedAuthnContextProvider.provide());
        authnRequest.setDestination(idp.getSSOLocation(idp.getBindingType()).getLocation());

        SAML2RequestTO requestTO = new SAML2RequestTO();
        requestTO.setIdpServiceAddress(authnRequest.getDestination());
        requestTO.setBindingType(idp.getBindingType());
        try {
            // 3. generate relay state as JWT
            Map<String, Object> claims = new HashMap<>();
            claims.put(JWT_CLAIM_IDP_DEFLATE, idp.isUseDeflateEncoding());
            Pair<String, Date> relayState = accessTokenDataBinder.generateJWT(
                    SecureRandomUtils.generateRandomUUID().toString(),
                    authnRequest.getID(), JWT_RELAY_STATE_DURATION, claims);

            // 4. sign and encode AuthnRequest
            switch (idp.getBindingType()) {
                case REDIRECT:
                    requestTO.setRelayState(URLEncoder.encode(relayState.getLeft(), StandardCharsets.UTF_8.name()));
                    requestTO.setContent(URLEncoder.encode(
                            saml2rw.encode(authnRequest, true), StandardCharsets.UTF_8.name()));
                    requestTO.setSignAlg(URLEncoder.encode(saml2rw.getSigAlgo(), StandardCharsets.UTF_8.name()));
                    requestTO.setSignature(URLEncoder.encode(
                            saml2rw.sign(requestTO.getContent(), requestTO.getRelayState()),
                            StandardCharsets.UTF_8.name()));
                    break;

                case POST:
                default:
                    requestTO.setRelayState(relayState.getLeft());
                    saml2rw.sign(authnRequest);
                    requestTO.setContent(saml2rw.encode(authnRequest, idp.isUseDeflateEncoding()));
            }
        } catch (Exception e) {
            LOG.error("While generating AuthnRequest", e);
            SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown);
            sce.getElements().add(e.getMessage());
            throw sce;
        }

        return requestTO;
    }

    @PreAuthorize("hasRole('" + StandardEntitlement.ANONYMOUS + "')")
    public SAML2LoginResponseTO validateLoginResponse(final SAML2ReceivedResponseTO response) {
        check();

        // 1. first checks for the provided relay state
        if (response.getRelayState() == null) {
            throw new IllegalArgumentException("No Relay State was provided");
        }

        Boolean useDeflateEncoding = false;
        String requestId = null;
        if (!IDP_INITIATED_RELAY_STATE.equals(response.getRelayState())) {
            JwsJwtCompactConsumer relayState = new JwsJwtCompactConsumer(response.getRelayState());
            if (!relayState.verifySignatureWith(jwsSignatureVerifier)) {
                throw new IllegalArgumentException("Invalid signature found in Relay State");
            }
            useDeflateEncoding = Boolean.valueOf(
                    relayState.getJwtClaims().getClaim(JWT_CLAIM_IDP_DEFLATE).toString());
            requestId = relayState.getJwtClaims().getSubject();

            Long expiryTime = relayState.getJwtClaims().getExpiryTime();
            if (expiryTime == null || (expiryTime * 1000L) < new Date().getTime()) {
                throw new IllegalArgumentException("Relay State is expired");
            }
        }

        // 2. parse the provided SAML response
        if (response.getSamlResponse() == null) {
            throw new IllegalArgumentException("No SAML Response was provided");
        }
        Response samlResponse;
        try {
            XMLObject responseObject = saml2rw.read(useDeflateEncoding, response.getSamlResponse());
            if (!(responseObject instanceof Response)) {
                throw new IllegalArgumentException("Expected " + Response.class.getName()
                        + ", got " + responseObject.getClass().getName());
            }
            samlResponse = (Response) responseObject;
        } catch (Exception e) {
            LOG.error("While parsing AuthnResponse", e);
            SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown);
            sce.getElements().add(e.getMessage());
            throw sce;
        }

        // 3. validate the SAML response and, if needed, decrypt the provided assertion(s)
        if (samlResponse.getIssuer() == null || samlResponse.getIssuer().getValue() == null) {
            throw new IllegalArgumentException("The SAML Response must contain an Issuer");
        }
        final SAML2IdPEntity idp = getIdP(samlResponse.getIssuer().getValue());
        if (idp.getConnObjectKeyItem() == null) {
            throw new IllegalArgumentException("No mapping provided for SAML 2.0 IdP '" + idp.getId() + "'");
        }

        if (IDP_INITIATED_RELAY_STATE.equals(response.getRelayState()) && !idp.isSupportUnsolicited()) {
            throw new IllegalArgumentException("An unsolicited request is not allowed for idp: " + idp.getId());
        }

        SSOValidatorResponse validatorResponse = null;
        try {
            validatorResponse = saml2rw.validate(
                    samlResponse,
                    idp,
                    getAssertionConsumerURL(response.getSpEntityID(), response.getUrlContext()),
                    requestId,
                    response.getSpEntityID());
        } catch (Exception e) {
            LOG.error("While validating AuthnResponse", e);
            SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown);
            sce.getElements().add(e.getMessage());
            throw sce;
        }

        // 4. prepare the result: find matching user (if any) and return the received attributes
        final SAML2LoginResponseTO responseTO = new SAML2LoginResponseTO();
        responseTO.setIdp(idp.getId());
        responseTO.setSloSupported(idp.getSLOLocation(idp.getBindingType()) != null);

        Assertion assertion = validatorResponse.getOpensamlAssertion();
        NameID nameID = assertion.getSubject().getNameID();
        if (nameID == null) {
            throw new IllegalArgumentException(NameID.DEFAULT_ELEMENT_LOCAL_NAME + " not found");
        }

        String keyValue = null;
        if (StringUtils.isNotBlank(nameID.getValue())
                && idp.getConnObjectKeyItem().getExtAttrName().equals(NameID.DEFAULT_ELEMENT_LOCAL_NAME)) {

            keyValue = nameID.getValue();
        }

        if (assertion.getConditions().getNotOnOrAfter() != null) {
            responseTO.setNotOnOrAfter(assertion.getConditions().getNotOnOrAfter().toDate());
        }
        assertion.getAuthnStatements().forEach(authnStmt -> {
            responseTO.setSessionIndex(authnStmt.getSessionIndex());

            responseTO.setAuthInstant(authnStmt.getAuthnInstant().toDate());
            if (authnStmt.getSessionNotOnOrAfter() != null) {
                responseTO.setNotOnOrAfter(authnStmt.getSessionNotOnOrAfter().toDate());
            }
        });

        for (AttributeStatement attrStmt : assertion.getAttributeStatements()) {
            for (Attribute attr : attrStmt.getAttributes()) {
                if (!attr.getAttributeValues().isEmpty()) {
                    String attrName = attr.getFriendlyName() == null ? attr.getName() : attr.getFriendlyName();
                    if (attrName.equals(idp.getConnObjectKeyItem().getExtAttrName())) {
                        if (attr.getAttributeValues().get(0) instanceof XSString) {
                            keyValue = ((XSString) attr.getAttributeValues().get(0)).getValue();
                        } else if (attr.getAttributeValues().get(0) instanceof XSAny) {
                            keyValue = ((XSAny) attr.getAttributeValues().get(0)).getTextContent();
                        }
                    }

                    AttrTO attrTO = new AttrTO.Builder().schema(attrName).
                            values(attr.getAttributeValues().stream().
                                    filter(value -> value.getDOM() != null).
                                    map(value -> value.getDOM().getTextContent()).
                                    collect(Collectors.toList())).build();
                    responseTO.getAttrs().add(attrTO);
                }
            }
        }

        List<String> matchingUsers = keyValue == null
                ? Collections.emptyList()
                : userManager.findMatchingUser(keyValue, idp.getKey());
        LOG.debug("Found {} matching users for {}", matchingUsers.size(), keyValue);

        String username;
        if (matchingUsers.isEmpty()) {
            if (idp.isCreateUnmatching()) {
                LOG.debug("No user matching {}, about to create", keyValue);

                username = AuthContextUtils.execWithAuthContext(AuthContextUtils.getDomain(),
                        () -> userManager.create(idp, responseTO, nameID.getValue()));
            } else if (idp.isSelfRegUnmatching()) {
                responseTO.setNameID(nameID.getValue());
                UserTO userTO = new UserTO();

                userManager.fill(idp.getKey(), responseTO, userTO);

                responseTO.getAttrs().clear();
                responseTO.getAttrs().addAll(userTO.getPlainAttrs());
                responseTO.getAttrs().addAll(userTO.getVirAttrs());
                if (StringUtils.isNotBlank(userTO.getUsername())) {
                    responseTO.setUsername(userTO.getUsername());
                }

                responseTO.setSelfReg(true);

                return responseTO;
            } else {
                throw new NotFoundException("User matching the provided value " + keyValue);
            }
        } else if (matchingUsers.size() > 1) {
            throw new IllegalArgumentException("Several users match the provided value " + keyValue);
        } else {
            if (idp.isUpdateMatching()) {
                LOG.debug("About to update {} for {}", matchingUsers.get(0), keyValue);

                username = AuthContextUtils.execWithAuthContext(AuthContextUtils.getDomain(),
                        () -> userManager.update(matchingUsers.get(0), idp, responseTO));
            } else {
                username = matchingUsers.get(0);
            }
        }

        responseTO.setUsername(username);
        responseTO.setNameID(nameID.getValue());

        // 5. generate JWT for further access
        Map<String, Object> claims = new HashMap<>();
        claims.put(JWT_CLAIM_IDP_ENTITYID, idp.getId());
        claims.put(JWT_CLAIM_NAMEID_FORMAT, nameID.getFormat());
        claims.put(JWT_CLAIM_NAMEID_VALUE, nameID.getValue());
        claims.put(JWT_CLAIM_SESSIONINDEX, responseTO.getSessionIndex());

        byte[] authorities = null;
        try {
            authorities = ENCRYPTOR.encode(POJOHelper.serialize(
                    authDataAccessor.getAuthorities(responseTO.getUsername(), null)), CipherAlgorithm.AES).getBytes();
        } catch (Exception e) {
            LOG.error("Could not fetch authorities", e);
        }

        Pair<String, Date> accessTokenInfo =
                accessTokenDataBinder.create(responseTO.getUsername(), claims, authorities, true);
        responseTO.setAccessToken(accessTokenInfo.getLeft());
        responseTO.setAccessTokenExpiryTime(accessTokenInfo.getRight());

        return responseTO;
    }

    @PreAuthorize("isAuthenticated() and not(hasRole('" + StandardEntitlement.ANONYMOUS + "'))")
    public SAML2RequestTO createLogoutRequest(final String accessToken, final String spEntityID) {
        check();

        // 1. fetch the current JWT used for Syncope authentication
        JwsJwtCompactConsumer consumer = new JwsJwtCompactConsumer(accessToken);
        if (!consumer.verifySignatureWith(jwsSignatureVerifier)) {
            throw new IllegalArgumentException("Invalid signature found in Access Token");
        }

        // 2. look for IdP
        String idpEntityID = (String) consumer.getJwtClaims().getClaim(JWT_CLAIM_IDP_ENTITYID);
        if (idpEntityID == null) {
            throw new NotFoundException("No SAML 2.0 IdP information found in the access token");
        }
        SAML2IdPEntity idp = cache.get(idpEntityID);
        if (idp == null) {
            throw new NotFoundException("SAML 2.0 IdP '" + idpEntityID + "'");
        }
        if (idp.getSLOLocation(idp.getBindingType()) == null) {
            throw new IllegalArgumentException("No SingleLogoutService available for " + idp.getId());
        }

        // 3. create LogoutRequest
        LogoutRequest logoutRequest = new LogoutRequestBuilder().buildObject();
        logoutRequest.setID("_" + SecureRandomUtils.generateRandomUUID().toString());
        logoutRequest.setDestination(idp.getSLOLocation(idp.getBindingType()).getLocation());

        DateTime now = new DateTime();
        logoutRequest.setIssueInstant(now);
        logoutRequest.setNotOnOrAfter(now.plusMinutes(5));

        Issuer issuer = new IssuerBuilder().buildObject();
        issuer.setValue(spEntityID);
        logoutRequest.setIssuer(issuer);

        NameID nameID = new NameIDBuilder().buildObject();
        nameID.setFormat((String) consumer.getJwtClaims().getClaim(JWT_CLAIM_NAMEID_FORMAT));
        nameID.setValue((String) consumer.getJwtClaims().getClaim(JWT_CLAIM_NAMEID_VALUE));
        logoutRequest.setNameID(nameID);

        SessionIndex sessionIndex = new SessionIndexBuilder().buildObject();
        sessionIndex.setSessionIndex((String) consumer.getJwtClaims().getClaim(JWT_CLAIM_SESSIONINDEX));
        logoutRequest.getSessionIndexes().add(sessionIndex);

        SAML2RequestTO requestTO = new SAML2RequestTO();
        requestTO.setIdpServiceAddress(logoutRequest.getDestination());
        requestTO.setBindingType(idp.getBindingType());
        try {
            // 3. generate relay state as JWT
            Map<String, Object> claims = new HashMap<>();
            claims.put(JWT_CLAIM_IDP_DEFLATE,
                    idp.getBindingType() == SAML2BindingType.REDIRECT ? true : idp.isUseDeflateEncoding());
            Pair<String, Date> relayState = accessTokenDataBinder.generateJWT(
                    SecureRandomUtils.generateRandomUUID().toString(),
                    logoutRequest.getID(), JWT_RELAY_STATE_DURATION, claims);
            requestTO.setRelayState(relayState.getLeft());

            // 4. sign and encode AuthnRequest
            switch (idp.getBindingType()) {
                case REDIRECT:
                    requestTO.setContent(saml2rw.encode(logoutRequest, true));
                    requestTO.setSignAlg(saml2rw.getSigAlgo());
                    requestTO.setSignature(saml2rw.sign(requestTO.getContent(), requestTO.getRelayState()));
                    break;

                case POST:
                default:
                    saml2rw.sign(logoutRequest);
                    requestTO.setContent(saml2rw.encode(logoutRequest, idp.isUseDeflateEncoding()));
            }
        } catch (Exception e) {
            LOG.error("While generating LogoutRequest", e);
            SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown);
            sce.getElements().add(e.getMessage());
            throw sce;
        }

        return requestTO;
    }

    @PreAuthorize("isAuthenticated() and not(hasRole('" + StandardEntitlement.ANONYMOUS + "'))")
    public void validateLogoutResponse(final String accessToken, final SAML2ReceivedResponseTO response) {
        check();

        // 1. fetch the current JWT used for Syncope authentication
        JwsJwtCompactConsumer consumer = new JwsJwtCompactConsumer(accessToken);
        if (!consumer.verifySignatureWith(jwsSignatureVerifier)) {
            throw new IllegalArgumentException("Invalid signature found in Access Token");
        }

        // 2. extract raw SAML response and relay state
        JwsJwtCompactConsumer relayState = null;
        Boolean useDeflateEncoding = false;
        if (StringUtils.isNotBlank(response.getRelayState())) {
            // first checks for the provided relay state, if available
            relayState = new JwsJwtCompactConsumer(response.getRelayState());
            if (!relayState.verifySignatureWith(jwsSignatureVerifier)) {
                throw new IllegalArgumentException("Invalid signature found in Relay State");
            }
            Long expiryTime = relayState.getJwtClaims().getExpiryTime();
            if (expiryTime == null || (expiryTime * 1000L) < new Date().getTime()) {
                throw new IllegalArgumentException("Relay State is expired");
            }

            useDeflateEncoding = Boolean.valueOf(
                    relayState.getJwtClaims().getClaim(JWT_CLAIM_IDP_DEFLATE).toString());
        }

        // 3. parse the provided SAML response
        LogoutResponse logoutResponse;
        try {
            XMLObject responseObject = saml2rw.read(useDeflateEncoding, response.getSamlResponse());
            if (!(responseObject instanceof LogoutResponse)) {
                throw new IllegalArgumentException("Expected " + LogoutResponse.class.getName()
                        + ", got " + responseObject.getClass().getName());
            }
            logoutResponse = (LogoutResponse) responseObject;
        } catch (Exception e) {
            LOG.error("While parsing LogoutResponse", e);
            SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown);
            sce.getElements().add(e.getMessage());
            throw sce;
        }

        // 4. if relay state was available, check the SAML Reponse's InResponseTo
        if (relayState != null && !relayState.getJwtClaims().getSubject().equals(logoutResponse.getInResponseTo())) {
            throw new IllegalArgumentException("Unmatching request ID: " + logoutResponse.getInResponseTo());
        }

        // 5. finally check for the logout status
        if (StatusCode.SUCCESS.equals(logoutResponse.getStatus().getStatusCode().getValue())) {
            accessTokenDAO.delete(consumer.getJwtClaims().getTokenId());
        } else {
            SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown);
            if (logoutResponse.getStatus().getStatusMessage() == null) {
                sce.getElements().add(logoutResponse.getStatus().getStatusCode().getValue());
            } else {
                sce.getElements().add(logoutResponse.getStatus().getStatusMessage().getMessage());
            }
            throw sce;
        }
    }

    @Override
    protected EntityTO resolveReference(
            final Method method, final Object... args) throws UnresolvedReferenceException {

        throw new UnresolvedReferenceException();
    }
}
