/**
 * 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.io.IOException;
import java.util.Arrays;
import java.util.List;

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;

import org.w3c.dom.Element;

import org.apache.cxf.message.Message;
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.cxf.ws.security.trust.delegation.DelegationCallback;
import org.apache.wss4j.common.ext.WSSecurityException;
import org.apache.wss4j.common.principal.SAMLTokenPrincipalImpl;
import org.apache.wss4j.common.saml.SamlAssertionWrapper;
import org.apache.wss4j.dom.handler.RequestData;
import org.apache.wss4j.dom.validate.Credential;
import org.apache.wss4j.dom.validate.Validator;

/**
 * A WSS4J-based Validator to validate a received WS-Security credential by dispatching
 * it to a STS via WS-Trust. The default binding is "validate", but "issue" is also possible
 * by setting the "useIssueBinding" property. In this case, the credentials are sent via
 * "OnBehalfOf" unless the "useOnBehalfOf" property is set to "false", in which case the
 * credentials are used depending on the security policy of the STS endpoint (e.g. in a
 * UsernameToken if this is what the policy requires). Setting "useOnBehalfOf" to "false" +
 * "useIssueBinding" to "true" only works for validating UsernameTokens.
 */
public class STSTokenValidator implements Validator {
    private STSSamlAssertionValidator samlValidator = new STSSamlAssertionValidator();
    private boolean alwaysValidateToSts;
    private boolean useIssueBinding;
    private boolean useOnBehalfOf = true;
    private STSClient stsClient;
    private TokenStore tokenStore;
    private boolean disableCaching;

    public STSTokenValidator() {
    }

    /**
     * Construct a new instance.
     * @param alwaysValidateToSts whether to always validate the token to the STS
     */
    public STSTokenValidator(boolean alwaysValidateToSts) {
        this.alwaysValidateToSts = alwaysValidateToSts;
    }

    public Credential validate(Credential credential, RequestData data) throws WSSecurityException {

        if (isValidatedLocally(credential, data)) {
            return credential;
        }

        return validateWithSTS(credential, (Message)data.getMsgContext());
    }

    public Credential validateWithSTS(Credential credential, Message message) throws WSSecurityException {

        try {
            SecurityToken token = new SecurityToken();
            Element tokenElement = null;
            int hash = 0;
            if (credential.getSamlAssertion() != null) {
                SamlAssertionWrapper assertion = credential.getSamlAssertion();
                byte[] signatureValue = assertion.getSignatureValue();
                if (signatureValue != null && signatureValue.length > 0) {
                    hash = Arrays.hashCode(signatureValue);
                }
                tokenElement = credential.getSamlAssertion().getElement();
            } else if (credential.getUsernametoken() != null) {
                tokenElement = credential.getUsernametoken().getElement();
                hash = credential.getUsernametoken().hashCode();
            } else if (credential.getBinarySecurityToken() != null) {
                tokenElement = credential.getBinarySecurityToken().getElement();
                hash = credential.getBinarySecurityToken().hashCode();
            } else if (credential.getSecurityContextToken() != null) {
                tokenElement = credential.getSecurityContextToken().getElement();
                hash = credential.getSecurityContextToken().hashCode();
            }
            token.setToken(tokenElement);

            TokenStore ts = null;
            if (!disableCaching) {
                ts = getTokenStore(message);
                if (ts == null) {
                    ts = tokenStore;
                }
                if (ts != null && hash != 0) {
                    SecurityToken transformedToken = getTransformedToken(ts, hash);
                    if (transformedToken != null && !transformedToken.isExpired()) {
                        SamlAssertionWrapper assertion = new SamlAssertionWrapper(transformedToken.getToken());
                        credential.setPrincipal(new SAMLTokenPrincipalImpl(assertion));
                        credential.setTransformedToken(assertion);
                        return credential;
                    }
                }
            }
            token.setTokenHash(hash);

            STSClient c = stsClient;
            if (c == null) {
                c = STSUtils.getClient(message, "sts");
            }

            synchronized (c) {
                System.setProperty("noprint", "true");

                SecurityToken returnedToken = null;

                if (useIssueBinding && useOnBehalfOf) {
                    ElementCallbackHandler callbackHandler = new ElementCallbackHandler(tokenElement);
                    c.setOnBehalfOf(callbackHandler);
                    returnedToken = c.requestSecurityToken();
                    c.setOnBehalfOf(null);
                } else if (useIssueBinding && !useOnBehalfOf && credential.getUsernametoken() != null) {
                    c.getProperties().put(SecurityConstants.USERNAME,
                                          credential.getUsernametoken().getName());
                    c.getProperties().put(SecurityConstants.PASSWORD,
                                          credential.getUsernametoken().getPassword());
                    returnedToken = c.requestSecurityToken();
                    c.getProperties().remove(SecurityConstants.USERNAME);
                    c.getProperties().remove(SecurityConstants.PASSWORD);
                } else {
                    List<SecurityToken> tokens = c.validateSecurityToken(token);
                    returnedToken = tokens.get(0);
                }

                if (returnedToken != token) {
                    SamlAssertionWrapper assertion = new SamlAssertionWrapper(returnedToken.getToken());
                    credential.setTransformedToken(assertion);
                    credential.setPrincipal(new SAMLTokenPrincipalImpl(assertion));
                    if (!disableCaching && hash != 0 && ts != null) {
                        ts.add(returnedToken);
                        token.setTransformedTokenIdentifier(returnedToken.getId());
                        ts.add(Integer.toString(hash), token);
                    }
                }
                return credential;
            }
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, e, "invalidSAMLsecurity");
        }
    }

    static final TokenStore getTokenStore(Message message) {
        if (message == null) {
            return null;
        }

        return TokenStoreUtils.getTokenStore(message);
    }

    protected boolean isValidatedLocally(Credential credential, RequestData data)
        throws WSSecurityException {

        if (!alwaysValidateToSts && credential.getSamlAssertion() != null) {
            try {
                samlValidator.validate(credential, data);
                return samlValidator.isTrustVerificationSucceeded();
            } catch (RuntimeException e) {
                throw e;
            } catch (Exception e) {
                throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, e, "invalidSAMLsecurity");
            }
        }
        return false;
    }

    private SecurityToken getTransformedToken(TokenStore ts, int hash) {
        SecurityToken recoveredToken = ts.getToken(Integer.toString(hash));
        if (recoveredToken != null && recoveredToken.getTokenHash() == hash) {
            String transformedTokenId = recoveredToken.getTransformedTokenIdentifier();
            if (transformedTokenId != null) {
                return ts.getToken(transformedTokenId);
            }
        }
        return null;
    }

    public boolean isUseIssueBinding() {
        return useIssueBinding;
    }

    public void setUseIssueBinding(boolean useIssueBinding) {
        this.useIssueBinding = useIssueBinding;
    }

    public boolean isUseOnBehalfOf() {
        return useOnBehalfOf;
    }

    public void setUseOnBehalfOf(boolean useOnBehalfOf) {
        this.useOnBehalfOf = useOnBehalfOf;
    }

    public STSClient getStsClient() {
        return stsClient;
    }

    public void setStsClient(STSClient stsClient) {
        this.stsClient = stsClient;
    }

    public TokenStore getTokenStore() {
        return tokenStore;
    }

    public void setTokenStore(TokenStore tokenStore) {
        this.tokenStore = tokenStore;
    }

    public boolean isDisableCaching() {
        return disableCaching;
    }

    public void setDisableCaching(boolean disableCaching) {
        this.disableCaching = disableCaching;
    }

    private static class ElementCallbackHandler implements CallbackHandler {

        private final Element tokenElement;

        ElementCallbackHandler(Element tokenElement) {
            this.tokenElement = tokenElement;
        }

        public void handle(Callback[] callbacks)
            throws IOException, UnsupportedCallbackException {
            for (int i = 0; i < callbacks.length; i++) {
                if (callbacks[i] instanceof DelegationCallback) {
                    DelegationCallback callback = (DelegationCallback) callbacks[i];

                    callback.setToken(tokenElement);
                } else {
                    throw new UnsupportedCallbackException(callbacks[i], "Unrecognized Callback");
                }
            }
        }
    }

}
