/**
 * 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.sts.token.validator.jwt;

import java.security.KeyStore;
import java.security.Principal;
import java.util.Properties;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.xml.soap.Node;

import org.w3c.dom.Element;

import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.common.security.SimplePrincipal;
import org.apache.cxf.rs.security.jose.common.JoseConstants;
import org.apache.cxf.rs.security.jose.jwa.SignatureAlgorithm;
import org.apache.cxf.rs.security.jose.jws.JwsJwtCompactConsumer;
import org.apache.cxf.rs.security.jose.jws.JwsSignatureVerifier;
import org.apache.cxf.rs.security.jose.jws.JwsUtils;
import org.apache.cxf.rs.security.jose.jwt.JwtToken;
import org.apache.cxf.rs.security.jose.jwt.JwtUtils;
import org.apache.cxf.sts.STSPropertiesMBean;
import org.apache.cxf.sts.request.ReceivedToken;
import org.apache.cxf.sts.request.ReceivedToken.STATE;
import org.apache.cxf.sts.token.realm.JWTRealmCodec;
import org.apache.cxf.sts.token.validator.TokenValidator;
import org.apache.cxf.sts.token.validator.TokenValidatorParameters;
import org.apache.cxf.sts.token.validator.TokenValidatorResponse;
import org.apache.cxf.ws.security.sts.provider.STSException;
import org.apache.wss4j.common.crypto.Crypto;
import org.apache.wss4j.common.crypto.Merlin;

/**
 * Validate a SAML Assertion. It is valid if it was issued and signed by this STS.
 */
public class JWTTokenValidator implements TokenValidator {
    
    private static final Logger LOG = LogUtils.getL7dLogger(JWTTokenValidator.class);
    private int clockOffset;
    private int ttl;
    private JWTRoleParser roleParser;
    private JWTRealmCodec realmCodec;
    
    /**
     * Return true if this TokenValidator implementation is capable of validating the
     * ReceivedToken argument.
     */
    public boolean canHandleToken(ReceivedToken validateTarget) {
        return canHandleToken(validateTarget, null);
    }
    
    /**
     * Return true if this TokenValidator implementation is capable of validating the
     * ReceivedToken argument. The realm is ignored in this Validator.
     */
    public boolean canHandleToken(ReceivedToken validateTarget, String realm) {
        Object token = validateTarget.getToken();
        if (token instanceof Element) {
            Element tokenEl = (Element)token;
            if (tokenEl.getFirstChild().getNodeType() == Node.TEXT_NODE) {
                try {
                    JwsJwtCompactConsumer jwtConsumer = new JwsJwtCompactConsumer(tokenEl.getTextContent());
                    if (jwtConsumer.getJwtToken() != null) {
                        return true;
                    }
                } catch (RuntimeException ex) {
                    return false;
                }
            }
        }
        return false;
    }
    
    /**
     * Validate a Token using the given TokenValidatorParameters.
     */
    public TokenValidatorResponse validateToken(TokenValidatorParameters tokenParameters) {
        LOG.fine("Validating JWT Token");
        STSPropertiesMBean stsProperties = tokenParameters.getStsProperties();
        
        TokenValidatorResponse response = new TokenValidatorResponse();
        ReceivedToken validateTarget = tokenParameters.getToken();
        validateTarget.setState(STATE.INVALID);
        response.setToken(validateTarget);
        
        String token = ((Element)validateTarget.getToken()).getTextContent();
        if (token == null || "".equals(token)) {
            return response;
        }
        
        if (token.split("\\.").length != 3) {
            LOG.log(Level.WARNING, "JWT Token appears not to be signed. Validation has failed");
            return response;
        }
        
        JwsJwtCompactConsumer jwtConsumer = new JwsJwtCompactConsumer(token);
        JwtToken jwt = jwtConsumer.getJwtToken();
        
        // Verify the signature
        Properties verificationProperties = new Properties();
        
        Crypto signatureCrypto = stsProperties.getSignatureCrypto();
        String alias = stsProperties.getSignatureUsername();
        
        if (alias != null) {
            verificationProperties.put(JoseConstants.RSSEC_KEY_STORE_ALIAS, alias);
        }
        
        if (!(signatureCrypto instanceof Merlin)) {
            throw new STSException("Can't get the keystore", STSException.REQUEST_FAILED);
        }
        KeyStore keystore = ((Merlin)signatureCrypto).getKeyStore();
        verificationProperties.put(JoseConstants.RSSEC_KEY_STORE, keystore);
        
        JwsSignatureVerifier signatureVerifier = 
            JwsUtils.loadSignatureVerifier(verificationProperties, jwt.getJwsHeaders());
        
        if (!jwtConsumer.verifySignatureWith(signatureVerifier)) {
            return response;
        }
        
        try {
            validateToken(jwt);
        } catch (RuntimeException ex) {
            LOG.log(Level.WARNING, "JWT token validation failed", ex);
            return response;
        }
        
        
        // Get the realm of the JWT Token
        if (realmCodec != null) {
            String tokenRealm = realmCodec.getRealmFromToken(jwt);
            response.setTokenRealm(tokenRealm);
        }

        if (isVerifiedWithAPublicKey(jwt)) {
            Principal principal = new SimplePrincipal(jwt.getClaims().getSubject());
            response.setPrincipal(principal);
            
            // Parse roles from the validated token
            if (roleParser != null) {
                Set<Principal> roles = 
                    roleParser.parseRolesFromToken(principal, null, jwt);
                response.setRoles(roles);
            }
        }

        validateTarget.setState(STATE.VALID);
        LOG.fine("JWT Token successfully validated");

        return response;
    }
    
    private boolean isVerifiedWithAPublicKey(JwtToken jwt) {
        String alg = (String)jwt.getJwsHeader(JoseConstants.HEADER_ALGORITHM);
        SignatureAlgorithm sigAlg = SignatureAlgorithm.getAlgorithm(alg);
        return SignatureAlgorithm.isPublicKeyAlgorithm(sigAlg);
    }
    
    protected void validateToken(JwtToken jwt) {
        JwtUtils.validateTokenClaims(jwt.getClaims(), ttl, clockOffset, false);
    }

    public int getClockOffset() {
        return clockOffset;
    }

    public void setClockOffset(int clockOffset) {
        this.clockOffset = clockOffset;
    }
    
    public int getTtl() {
        return ttl;
    }

    public void setTtl(int ttl) {
        this.ttl = ttl;
    }
    
    public JWTRoleParser getRoleParser() {
        return roleParser;
    }

    public void setRoleParser(JWTRoleParser roleParser) {
        this.roleParser = roleParser;
    }

    public JWTRealmCodec getRealmCodec() {
        return realmCodec;
    }

    public void setRealmCodec(JWTRealmCodec realmCodec) {
        this.realmCodec = realmCodec;
    }
}
