/**
 * 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.cxf.ws.security.trust;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;

import org.w3c.dom.Element;

import org.apache.cxf.interceptor.Fault;
import org.apache.cxf.message.Message;
import org.apache.cxf.rt.security.utils.SecurityUtils;
import org.apache.cxf.ws.security.SecurityConstants;
import org.apache.cxf.ws.security.tokenstore.SecurityToken;
import org.apache.cxf.ws.security.tokenstore.TokenStore;
import org.apache.cxf.ws.security.tokenstore.TokenStoreUtils;
import org.apache.wss4j.common.ext.WSSecurityException;
import org.apache.wss4j.common.saml.SamlAssertionWrapper;
import org.apache.wss4j.common.util.XMLUtils;
import org.apache.wss4j.dom.WSConstants;
import org.apache.xml.security.utils.Base64;

public class DefaultSTSTokenCacher implements STSTokenCacher {

    public SecurityToken retrieveToken(Message message) {
        boolean cacheIssuedToken =
            SecurityUtils.getSecurityPropertyBoolean(SecurityConstants.CACHE_ISSUED_TOKEN_IN_ENDPOINT,
                                              message,
                                              true);
        SecurityToken tok = null;
        if (cacheIssuedToken) {
            tok = (SecurityToken)message.getContextualProperty(SecurityConstants.TOKEN);
            if (tok == null) {
                String tokId = (String)message.getContextualProperty(SecurityConstants.TOKEN_ID);
                if (tokId != null) {
                    tok = TokenStoreUtils.getTokenStore(message).getToken(tokId);
                }
            }
        } else {
            tok = (SecurityToken)message.get(SecurityConstants.TOKEN);
            if (tok == null) {
                String tokId = (String)message.get(SecurityConstants.TOKEN_ID);
                if (tokId != null) {
                    tok = TokenStoreUtils.getTokenStore(message).getToken(tokId);
                }
            }
        }
        return tok;
    }

    public SecurityToken retrieveToken(Message message, Element delegationToken, String cacheKey) {
        if (delegationToken == null) {
            return null;
        }
        TokenStore tokenStore = TokenStoreUtils.getTokenStore(message);

        // See if the token corresponding to the delegation Token is stored in the cache
        // and if it points to an issued token
        String id = getIdFromToken(delegationToken);
        SecurityToken cachedToken = tokenStore.getToken(id);
        if (cachedToken != null) {
            Map<String, Object> properties = cachedToken.getProperties();
            if (properties != null && properties.containsKey(cacheKey)) {
                String associatedToken = (String)properties.get(cacheKey);
                SecurityToken issuedToken = tokenStore.getToken(associatedToken);
                if (issuedToken != null) {
                    return issuedToken;
                }
            }
        }

        return null;
    }

    public void storeToken(Message message, SecurityToken securityToken) {
        boolean cacheIssuedToken =
            SecurityUtils.getSecurityPropertyBoolean(SecurityConstants.CACHE_ISSUED_TOKEN_IN_ENDPOINT,
                                              message,
                                              true)
                && !isOneTimeUse(securityToken);
        if (cacheIssuedToken) {
            message.getExchange().getEndpoint().put(SecurityConstants.TOKEN, securityToken);
            message.getExchange().put(SecurityConstants.TOKEN, securityToken);
            message.put(SecurityConstants.TOKEN_ELEMENT, securityToken.getToken());
            message.getExchange().put(SecurityConstants.TOKEN_ID, securityToken.getId());
            message.getExchange().getEndpoint().put(SecurityConstants.TOKEN_ID,
                                                    securityToken.getId());
        } else {
            message.put(SecurityConstants.TOKEN, securityToken);
            message.put(SecurityConstants.TOKEN_ID, securityToken.getId());
            message.put(SecurityConstants.TOKEN_ELEMENT, securityToken.getToken());
        }
        // ?
        TokenStoreUtils.getTokenStore(message).add(securityToken);
    }

    public void storeToken(Message message, Element delegationToken, String secTokenId, String cacheKey) {
        if (secTokenId == null || delegationToken == null) {
            return;
        }

        TokenStore tokenStore = TokenStoreUtils.getTokenStore(message);

        String id = getIdFromToken(delegationToken);
        SecurityToken cachedToken = tokenStore.getToken(id);
        if (cachedToken == null) {
            cachedToken = new SecurityToken(id);
            cachedToken.setToken(delegationToken);
        }
        Map<String, Object> properties = cachedToken.getProperties();
        if (properties == null) {
            properties = new HashMap<>();
            cachedToken.setProperties(properties);
        }
        properties.put(cacheKey, secTokenId);
        tokenStore.add(cachedToken);
    }

    public void removeToken(Message message, SecurityToken securityToken) {
        // Remove token from cache
        message.getExchange().getEndpoint().remove(SecurityConstants.TOKEN);
        message.getExchange().getEndpoint().remove(SecurityConstants.TOKEN_ID);
        message.getExchange().remove(SecurityConstants.TOKEN_ID);
        message.getExchange().remove(SecurityConstants.TOKEN);
        if (securityToken != null) {
            TokenStoreUtils.getTokenStore(message).remove(securityToken.getId());
        }
    }

    // Check to see if the received token is a SAML2 Token with "OneTimeUse" set. If so,
    // it should not be cached on the endpoint, but only on the message.
    private static boolean isOneTimeUse(SecurityToken issuedToken) {
        Element token = issuedToken.getToken();
        if (token != null && "Assertion".equals(token.getLocalName())
            && WSConstants.SAML2_NS.equals(token.getNamespaceURI())) {
            try {
                SamlAssertionWrapper assertion = new SamlAssertionWrapper(token);

                if (assertion.getSaml2().getConditions() != null
                    && assertion.getSaml2().getConditions().getOneTimeUse() != null) {
                    return true;
                }
            } catch (WSSecurityException ex) {
                throw new Fault(ex);
            }
        }

        return false;
    }

    // Get an id from the token that is unique to that token
    private static String getIdFromToken(Element token) {
        if (token != null) {
            // For SAML tokens get the ID/AssertionID
            if ("Assertion".equals(token.getLocalName())
                && WSConstants.SAML2_NS.equals(token.getNamespaceURI())) {
                return token.getAttributeNS(null, "ID");
            } else if ("Assertion".equals(token.getLocalName())
                && WSConstants.SAML_NS.equals(token.getNamespaceURI())) {
                return token.getAttributeNS(null, "AssertionID");
            }

            // For UsernameTokens get the username
            if (WSConstants.USERNAME_TOKEN_LN.equals(token.getLocalName())
                && WSConstants.WSSE_NS.equals(token.getNamespaceURI())) {
                Element usernameElement =
                    XMLUtils.getDirectChildElement(token, WSConstants.USERNAME_LN, WSConstants.WSSE_NS);
                if (usernameElement != null) {
                    return XMLUtils.getElementText(usernameElement);
                }
            }

            // For BinarySecurityTokens take the hash of the value
            if (WSConstants.BINARY_TOKEN_LN.equals(token.getLocalName())
                && WSConstants.WSSE_NS.equals(token.getNamespaceURI())) {
                String text = XMLUtils.getElementText(token);
                if (text != null && !"".equals(text)) {
                    try {
                        MessageDigest digest = MessageDigest.getInstance("SHA-256");
                        byte[] bytes = digest.digest(text.getBytes());
                        return Base64.encode(bytes);
                    } catch (NoSuchAlgorithmException e) {
                        // SHA-256 must be supported so not going to happen...
                    }
                }
            }
        }
        return "";
    }

}