/**
 * 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 org.w3c.dom.Document;
import org.w3c.dom.Element;

import org.apache.commons.codec.binary.Base64;
import org.apache.cxf.binding.soap.SoapMessage;
import org.apache.cxf.helpers.DOMUtils;
import org.apache.cxf.ws.security.tokenstore.SecurityToken;
import org.apache.wss4j.binding.wss10.AttributedString;
import org.apache.wss4j.binding.wss10.BinarySecurityTokenType;
import org.apache.wss4j.binding.wss10.EncodedString;
import org.apache.wss4j.binding.wss10.PasswordString;
import org.apache.wss4j.binding.wss10.UsernameTokenType;
import org.apache.wss4j.binding.wsu10.AttributedDateTime;
import org.apache.wss4j.common.crypto.Crypto;
import org.apache.wss4j.common.ext.WSPasswordCallback;
import org.apache.wss4j.common.ext.WSSecurityException;
import org.apache.wss4j.common.saml.SamlAssertionWrapper;
import org.apache.wss4j.common.token.BinarySecurity;
import org.apache.wss4j.common.token.PKIPathSecurity;
import org.apache.wss4j.common.token.X509Security;
import org.apache.wss4j.dom.message.token.KerberosSecurity;
import org.apache.wss4j.dom.message.token.UsernameToken;
import org.apache.wss4j.stax.ext.WSSConstants;
import org.apache.wss4j.stax.impl.securityToken.KerberosServiceSecurityTokenImpl;
import org.apache.wss4j.stax.impl.securityToken.SamlSecurityTokenImpl;
import org.apache.wss4j.stax.impl.securityToken.UsernameSecurityTokenImpl;
import org.apache.wss4j.stax.impl.securityToken.X509PKIPathv1SecurityTokenImpl;
import org.apache.wss4j.stax.impl.securityToken.X509V3SecurityTokenImpl;
import org.apache.wss4j.stax.securityToken.SamlSecurityToken;
import org.apache.wss4j.stax.securityToken.UsernameSecurityToken;
import org.apache.wss4j.stax.securityToken.WSSecurityTokenConstants;
import org.apache.wss4j.stax.utils.WSSUtils;
import org.apache.wss4j.stax.validate.BinarySecurityTokenValidator;
import org.apache.wss4j.stax.validate.BinarySecurityTokenValidatorImpl;
import org.apache.wss4j.stax.validate.SamlTokenValidatorImpl;
import org.apache.wss4j.stax.validate.TokenContext;
import org.apache.wss4j.stax.validate.UsernameTokenValidator;
import org.apache.xml.security.exceptions.XMLSecurityException;
import org.apache.xml.security.stax.ext.XMLSecurityUtils;
import org.apache.xml.security.stax.securityToken.InboundSecurityToken;

/**
 * A Streaming SAML Token Validator implementation to validate a received Token to a
 * SecurityTokenService (STS).
 *
 * TODO Refactor this class a bit better...
 */
public class STSStaxTokenValidator
    extends SamlTokenValidatorImpl implements BinarySecurityTokenValidator, UsernameTokenValidator {

    private boolean alwaysValidateToSts;

    public STSStaxTokenValidator() {
        //
    }

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

    @SuppressWarnings("unchecked")
    @Override
    public <T extends SamlSecurityToken & InboundSecurityToken> T validate(
                                                 final SamlAssertionWrapper samlAssertionWrapper,
                                                 final InboundSecurityToken subjectSecurityToken,
                                                 final TokenContext tokenContext) throws WSSecurityException {
        // Check conditions
        checkConditions(samlAssertionWrapper);

        // Check OneTimeUse Condition
        checkOneTimeUse(samlAssertionWrapper,
                        tokenContext.getWssSecurityProperties().getSamlOneTimeUseReplayCache());

        // Validate the assertion against schemas/profiles
        validateAssertion(samlAssertionWrapper);

        Crypto sigVerCrypto = null;
        if (samlAssertionWrapper.isSigned()) {
            sigVerCrypto = tokenContext.getWssSecurityProperties().getSignatureVerificationCrypto();
        }

        final SoapMessage message =
            (SoapMessage)tokenContext.getWssSecurityProperties().getMsgContext();

        // Validate to STS if required
        boolean valid = false;
        if (alwaysValidateToSts) {
            Element tokenElement = samlAssertionWrapper.getElement();
            validateTokenToSTS(tokenElement, message);
            valid = true;
        }
        final boolean stsValidated = valid;

        SamlSecurityTokenImpl securityToken = new SamlSecurityTokenImpl(
                samlAssertionWrapper, subjectSecurityToken,
                tokenContext.getWsSecurityContext(),
                sigVerCrypto,
                WSSecurityTokenConstants.KeyIdentifier_NoKeyInfo,
                tokenContext.getWssSecurityProperties()) {

            @Override
            public void verify() throws XMLSecurityException {
                if (stsValidated) {
                    // Already validated
                    return;
                }
                try {
                    super.verify();
                } catch (XMLSecurityException ex) {
                    SamlAssertionWrapper assertion = super.getSamlAssertionWrapper();
                    Element tokenElement = assertion.getElement();
                    validateTokenToSTS(tokenElement, message);
                }
            }

        };

        securityToken.setElementPath(tokenContext.getElementPath());
        securityToken.setXMLSecEvent(tokenContext.getFirstXMLSecEvent());

        return (T)securityToken;
    }

    @Override
    public InboundSecurityToken validate(final BinarySecurityTokenType binarySecurityTokenType,
                                         final TokenContext tokenContext)
        throws WSSecurityException {
        STSStaxBSTValidator validator = new STSStaxBSTValidator(alwaysValidateToSts);
        return validator.validate(binarySecurityTokenType, tokenContext);
    }

    @SuppressWarnings("unchecked")
    @Override
    public <T extends UsernameSecurityToken & InboundSecurityToken> T validate(UsernameTokenType usernameTokenType,
                                                                               TokenContext tokenContext)
        throws WSSecurityException {
        // If the UsernameToken is to be used for key derivation, the (1.1)
        // spec says that it cannot contain a password, and it must contain
        // an Iteration element
        final byte[] salt = XMLSecurityUtils.getQNameType(usernameTokenType.getAny(), WSSConstants.TAG_WSSE11_SALT);
        PasswordString passwordType =
            XMLSecurityUtils.getQNameType(usernameTokenType.getAny(), WSSConstants.TAG_WSSE_PASSWORD);
        final Long iteration =
            XMLSecurityUtils.getQNameType(usernameTokenType.getAny(), WSSConstants.TAG_WSSE11_ITERATION);
        if (salt != null && (passwordType != null || iteration == null)) {
            throw new WSSecurityException(WSSecurityException.ErrorCode.INVALID_SECURITY_TOKEN, "badTokenType01");
        }

        boolean handleCustomPasswordTypes =
            tokenContext.getWssSecurityProperties().getHandleCustomPasswordTypes();
        boolean allowUsernameTokenNoPassword =
            tokenContext.getWssSecurityProperties().isAllowUsernameTokenNoPassword()
                || Boolean.parseBoolean((String)tokenContext.getWsSecurityContext().get(
                    WSSConstants.PROP_ALLOW_USERNAMETOKEN_NOPASSWORD));

        // Check received password type against required type
        WSSConstants.UsernameTokenPasswordType requiredPasswordType =
            tokenContext.getWssSecurityProperties().getUsernameTokenPasswordType();
        if (requiredPasswordType != null) {
            if (passwordType == null || passwordType.getType() == null) {
                throw new WSSecurityException(WSSecurityException.ErrorCode.FAILED_AUTHENTICATION);
            }
            WSSConstants.UsernameTokenPasswordType usernameTokenPasswordType =
                WSSConstants.UsernameTokenPasswordType.getUsernameTokenPasswordType(passwordType.getType());
            if (requiredPasswordType != usernameTokenPasswordType) {
                throw new WSSecurityException(WSSecurityException.ErrorCode.FAILED_AUTHENTICATION);
            }
        }

        WSSConstants.UsernameTokenPasswordType usernameTokenPasswordType =
            WSSConstants.UsernameTokenPasswordType.PASSWORD_NONE;
        if (passwordType != null && passwordType.getType() != null) {
            usernameTokenPasswordType =
                WSSConstants.UsernameTokenPasswordType.getUsernameTokenPasswordType(
                    passwordType.getType());
        }

        final AttributedString username = usernameTokenType.getUsername();
        if (username == null) {
            throw new WSSecurityException(WSSecurityException.ErrorCode.INVALID_SECURITY_TOKEN,
                                          "badTokenType01");
        }

        final EncodedString encodedNonce =
                XMLSecurityUtils.getQNameType(usernameTokenType.getAny(),
                                              WSSConstants.TAG_WSSE_NONCE);
        byte[] nonceVal = null;
        if (encodedNonce != null && encodedNonce.getValue() != null) {
            nonceVal = Base64.decodeBase64(encodedNonce.getValue());
        }

        final AttributedDateTime attributedDateTimeCreated =
                XMLSecurityUtils.getQNameType(usernameTokenType.getAny(),
                                              WSSConstants.TAG_WSU_CREATED);

        String created = null;
        if (attributedDateTimeCreated != null) {
            created = attributedDateTimeCreated.getValue();
        }

        // Validate to STS if required
        boolean valid = false;
        final SoapMessage message =
            (SoapMessage)tokenContext.getWssSecurityProperties().getMsgContext();
        if (alwaysValidateToSts) {
            Element tokenElement =
                convertToDOM(username.getValue(), passwordType.getValue(),
                             passwordType.getType(), usernameTokenType.getId());
            validateTokenToSTS(tokenElement, message);
            valid = true;
        }

        if (!valid) {
            try {
                if (usernameTokenPasswordType == WSSConstants.UsernameTokenPasswordType.PASSWORD_DIGEST) {
                    if (encodedNonce == null || attributedDateTimeCreated == null) {
                        throw new WSSecurityException(WSSecurityException.ErrorCode.INVALID_SECURITY_TOKEN,
                                                      "badTokenType01");
                    }

                    if (!WSSConstants.SOAPMESSAGE_NS10_BASE64_ENCODING.equals(encodedNonce.getEncodingType())) {
                        throw new WSSecurityException(WSSecurityException.ErrorCode.UNSUPPORTED_SECURITY_TOKEN,
                                                      "badTokenType01");
                    }

                    verifyDigestPassword(username.getValue(), passwordType, nonceVal, created, tokenContext);
                } else if (usernameTokenPasswordType == WSSConstants.UsernameTokenPasswordType.PASSWORD_TEXT
                        || passwordType != null && passwordType.getValue() != null
                        && usernameTokenPasswordType == WSSConstants.UsernameTokenPasswordType.PASSWORD_NONE) {

                    verifyPlaintextPassword(username.getValue(), passwordType, tokenContext);
                } else if (passwordType != null && passwordType.getValue() != null) {
                    if (!handleCustomPasswordTypes) {
                        throw new WSSecurityException(WSSecurityException.ErrorCode.FAILED_AUTHENTICATION);
                    }
                    verifyPlaintextPassword(username.getValue(), passwordType, tokenContext);
                } else {
                    if (!allowUsernameTokenNoPassword) {
                        throw new WSSecurityException(WSSecurityException.ErrorCode.FAILED_AUTHENTICATION);
                    }
                }
            } catch (WSSecurityException ex) {
                Element tokenElement =
                    convertToDOM(username.getValue(), passwordType.getValue(),
                                 passwordType.getType(), usernameTokenType.getId());
                validateTokenToSTS(tokenElement, message);
            }
        }

        final String password;
        if (passwordType != null) {
            password = passwordType.getValue();
        } else if (salt != null) {
            WSPasswordCallback pwCb = new WSPasswordCallback(username.getValue(),
                   WSPasswordCallback.USERNAME_TOKEN);
            try {
                WSSUtils.doPasswordCallback(tokenContext.getWssSecurityProperties().getCallbackHandler(), pwCb);
            } catch (WSSecurityException e) {
                throw new WSSecurityException(WSSecurityException.ErrorCode.FAILED_AUTHENTICATION, e);
            }
            password = pwCb.getPassword();
        } else {
            password = null;
        }

        UsernameSecurityTokenImpl usernameSecurityToken = new UsernameSecurityTokenImpl(
                usernameTokenPasswordType, username.getValue(), password, created,
                nonceVal, salt, iteration,
                tokenContext.getWsSecurityContext(), usernameTokenType.getId(),
                WSSecurityTokenConstants.KEYIDENTIFIER_SECURITY_TOKEN_DIRECT_REFERENCE);
        usernameSecurityToken.setElementPath(tokenContext.getElementPath());
        usernameSecurityToken.setXMLSecEvent(tokenContext.getFirstXMLSecEvent());

        return (T)usernameSecurityToken;
    }

    /**
     * Verify a UsernameToken containing a password digest.
     */
    private void verifyDigestPassword(
        String username,
        PasswordString passwordType,
        byte[] nonceVal,
        String created,
        TokenContext tokenContext
    ) throws WSSecurityException {
        WSPasswordCallback pwCb = new WSPasswordCallback(username,
                null,
                passwordType.getType(),
                WSPasswordCallback.USERNAME_TOKEN);
        try {
            WSSUtils.doPasswordCallback(tokenContext.getWssSecurityProperties().getCallbackHandler(), pwCb);
        } catch (WSSecurityException e) {
            throw new WSSecurityException(WSSecurityException.ErrorCode.FAILED_AUTHENTICATION, e);
        }

        if (pwCb.getPassword() == null) {
            throw new WSSecurityException(WSSecurityException.ErrorCode.FAILED_AUTHENTICATION);
        }

        String passDigest = WSSUtils.doPasswordDigest(nonceVal, created, pwCb.getPassword());
        if (!passwordType.getValue().equals(passDigest)) {
            throw new WSSecurityException(WSSecurityException.ErrorCode.FAILED_AUTHENTICATION);
        }
        passwordType.setValue(pwCb.getPassword());
    }

    /**
     * Verify a UsernameToken containing a plaintext password.
     */
    private void verifyPlaintextPassword(
        String username,
        PasswordString passwordType,
        TokenContext tokenContext
    ) throws WSSecurityException {
        WSPasswordCallback pwCb = new WSPasswordCallback(username,
                null,
                passwordType.getType(),
                WSPasswordCallback.USERNAME_TOKEN);
        try {
            WSSUtils.doPasswordCallback(tokenContext.getWssSecurityProperties().getCallbackHandler(), pwCb);
        } catch (WSSecurityException e) {
            throw new WSSecurityException(WSSecurityException.ErrorCode.FAILED_AUTHENTICATION, e);
        }

        if (pwCb.getPassword() == null) {
            throw new WSSecurityException(WSSecurityException.ErrorCode.FAILED_AUTHENTICATION);
        }

        if (!passwordType.getValue().equals(pwCb.getPassword())) {
            throw new WSSecurityException(WSSecurityException.ErrorCode.FAILED_AUTHENTICATION);
        }
        passwordType.setValue(pwCb.getPassword());
    }

    // Convert to DOM to send the token to the STS - it does not copy Nonce/Created/Iteration
    // values
    private Element convertToDOM(
        String username, String password, String passwordType, String id
    ) {
        Document doc = DOMUtils.getEmptyDocument();

        UsernameToken usernameToken = new UsernameToken(true, doc, passwordType);
        usernameToken.setName(username);
        usernameToken.setPassword(password);
        usernameToken.setID(id);

        usernameToken.addWSSENamespace();
        usernameToken.addWSUNamespace();

        return usernameToken.getElement();
    }

    private static void validateTokenToSTS(Element tokenElement, SoapMessage message)
        throws WSSecurityException {
        SecurityToken token = new SecurityToken();
        token.setToken(tokenElement);

        STSClient c = STSUtils.getClient(message, "sts");
        synchronized (c) {
            System.setProperty("noprint", "true");
            try {
                c.validateSecurityToken(token);
            } catch (Exception e) {
                throw new WSSecurityException(WSSecurityException.ErrorCode.FAILED_AUTHENTICATION, e);
            }
        }
    }

    /**
     * A Streaming SAML Token Validator implementation to validate a BinarySecurityToken to a
     * SecurityTokenService (STS).
     */
    private static class STSStaxBSTValidator extends BinarySecurityTokenValidatorImpl {

        private boolean alwaysValidateToSts;

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

        @Override
        public InboundSecurityToken validate(final BinarySecurityTokenType binarySecurityTokenType,
                                             final TokenContext tokenContext)
            throws WSSecurityException {

            //only Base64Encoding is supported
            if (!WSSConstants.SOAPMESSAGE_NS10_BASE64_ENCODING.equals(
                binarySecurityTokenType.getEncodingType())
            ) {
                throw new WSSecurityException(
                        WSSecurityException.ErrorCode.INVALID_SECURITY_TOKEN, "badEncoding",
                        new Object[]{binarySecurityTokenType.getEncodingType()});
            }

            final byte[] securityTokenData = Base64.decodeBase64(binarySecurityTokenType.getValue());
            final SoapMessage message =
                (SoapMessage)tokenContext.getWssSecurityProperties().getMsgContext();

            // Validate to STS if required
            boolean valid = false;
            if (alwaysValidateToSts) {
                Element tokenElement =
                    convertToDOM(binarySecurityTokenType, securityTokenData);
                validateTokenToSTS(tokenElement, message);
                valid = true;
            }
            final boolean stsValidated = valid;

            try {
                if (WSSConstants.NS_X509_V3_TYPE.equals(binarySecurityTokenType.getValueType())) {
                    Crypto crypto = getCrypto(tokenContext.getWssSecurityProperties());
                    X509V3SecurityTokenImpl x509V3SecurityToken = new X509V3SecurityTokenImpl(
                            tokenContext.getWsSecurityContext(),
                            crypto,
                            tokenContext.getWssSecurityProperties().getCallbackHandler(),
                            securityTokenData, binarySecurityTokenType.getId(),
                            tokenContext.getWssSecurityProperties()
                    ) {

                        @Override
                        public void verify() throws XMLSecurityException {
                            if (stsValidated) {
                                // Already validated
                                return;
                            }
                            try {
                                super.verify();
                            } catch (XMLSecurityException ex) {
                                Element tokenElement =
                                    convertToDOM(binarySecurityTokenType, securityTokenData);
                                validateTokenToSTS(tokenElement, message);
                            }
                        }
                    };
                    x509V3SecurityToken.setElementPath(tokenContext.getElementPath());
                    x509V3SecurityToken.setXMLSecEvent(tokenContext.getFirstXMLSecEvent());
                    return x509V3SecurityToken;
                } else if (WSSConstants.NS_X509_PKIPATH_V1.equals(binarySecurityTokenType.getValueType())) {
                    Crypto crypto = getCrypto(tokenContext.getWssSecurityProperties());
                    X509PKIPathv1SecurityTokenImpl x509PKIPathv1SecurityToken =
                        new X509PKIPathv1SecurityTokenImpl(
                            tokenContext.getWsSecurityContext(),
                            crypto,
                            tokenContext.getWssSecurityProperties().getCallbackHandler(),
                            securityTokenData, binarySecurityTokenType.getId(),
                            WSSecurityTokenConstants.KEYIDENTIFIER_SECURITY_TOKEN_DIRECT_REFERENCE,
                            tokenContext.getWssSecurityProperties()
                        ) {
                            @Override
                            public void verify() throws XMLSecurityException {
                                if (stsValidated) {
                                    // Already validated
                                    return;
                                }
                                try {
                                    super.verify();
                                } catch (XMLSecurityException ex) {
                                    Element tokenElement =
                                        convertToDOM(binarySecurityTokenType, securityTokenData);
                                    validateTokenToSTS(tokenElement, message);
                                }
                            }
                        };
                    x509PKIPathv1SecurityToken.setElementPath(tokenContext.getElementPath());
                    x509PKIPathv1SecurityToken.setXMLSecEvent(tokenContext.getFirstXMLSecEvent());
                    return x509PKIPathv1SecurityToken;
                } else if (WSSConstants.NS_GSS_KERBEROS5_AP_REQ.equals(binarySecurityTokenType.getValueType())) {
                    KerberosServiceSecurityTokenImpl kerberosServiceSecurityToken =
                        new KerberosServiceSecurityTokenImpl(
                            tokenContext.getWsSecurityContext(),
                            tokenContext.getWssSecurityProperties().getCallbackHandler(),
                            securityTokenData, binarySecurityTokenType.getValueType(),
                            binarySecurityTokenType.getId(),
                            WSSecurityTokenConstants.KEYIDENTIFIER_SECURITY_TOKEN_DIRECT_REFERENCE
                        ) {
                            @Override
                            public void verify() throws XMLSecurityException {
                                if (stsValidated) {
                                    // Already validated
                                    return;
                                }
                                try {
                                    super.verify();
                                } catch (XMLSecurityException ex) {
                                    Element tokenElement =
                                        convertToDOM(binarySecurityTokenType, securityTokenData);
                                    validateTokenToSTS(tokenElement, message);
                                }
                            }
                        };
                    kerberosServiceSecurityToken.setElementPath(tokenContext.getElementPath());
                    kerberosServiceSecurityToken.setXMLSecEvent(tokenContext.getFirstXMLSecEvent());
                    return kerberosServiceSecurityToken;
                } else {
                    throw new WSSecurityException(
                            WSSecurityException.ErrorCode.INVALID_SECURITY_TOKEN, "invalidValueType",
                            new Object[]{binarySecurityTokenType.getValueType()});
                }
            } catch (XMLSecurityException e) {
                throw new WSSecurityException(WSSecurityException.ErrorCode.INVALID_SECURITY_TOKEN, e);
            }
        }

        // Convert to DOM to send the token to the STS
        private Element convertToDOM(
            BinarySecurityTokenType binarySecurityTokenType,
            byte[] securityTokenData
        ) throws WSSecurityException {
            Document doc = DOMUtils.getEmptyDocument();
            BinarySecurity binarySecurity = null;
            if (WSSConstants.NS_X509_V3_TYPE.equals(binarySecurityTokenType.getValueType())) {
                binarySecurity = new X509Security(doc);
            } else if (WSSConstants.NS_X509_PKIPATH_V1.equals(binarySecurityTokenType.getValueType())) {
                binarySecurity = new PKIPathSecurity(doc);
            } else if (WSSConstants.NS_GSS_KERBEROS5_AP_REQ.equals(binarySecurityTokenType.getValueType())) {
                binarySecurity = new KerberosSecurity(doc);
            } else {
                throw new WSSecurityException(WSSecurityException.ErrorCode.INVALID_SECURITY_TOKEN);
            }

            binarySecurity.addWSSENamespace();
            binarySecurity.addWSUNamespace();
            binarySecurity.setEncodingType(binarySecurityTokenType.getEncodingType());
            binarySecurity.setValueType(binarySecurityTokenType.getValueType());
            binarySecurity.setID(binarySecurityTokenType.getId());
            binarySecurity.setToken(securityTokenData);

            return binarySecurity.getElement();
        }
    }


}
