/**
 * 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.fediz.samlsso.example;


import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.zip.DataFormatException;

import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.UriBuilder;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.stream.XMLStreamException;

import org.w3c.dom.Document;
import org.w3c.dom.Element;

import org.apache.cxf.common.util.Base64Exception;
import org.apache.cxf.common.util.Base64Utility;
import org.apache.cxf.helpers.IOUtils;
import org.apache.cxf.jaxrs.ext.MessageContext;
import org.apache.cxf.rs.security.saml.DeflateEncoderDecoder;
import org.apache.cxf.staxutils.StaxUtils;
import org.apache.wss4j.common.crypto.Crypto;
import org.apache.wss4j.common.crypto.CryptoFactory;
import org.apache.wss4j.common.ext.WSSecurityException;
import org.apache.wss4j.common.saml.OpenSAMLUtil;
import org.apache.wss4j.common.saml.SAMLCallback;
import org.apache.wss4j.common.saml.SAMLUtil;
import org.apache.wss4j.common.saml.SamlAssertionWrapper;
import org.apache.wss4j.common.saml.bean.AudienceRestrictionBean;
import org.apache.wss4j.common.saml.bean.ConditionsBean;
import org.apache.wss4j.common.saml.bean.SubjectConfirmationDataBean;
import org.apache.wss4j.common.util.DOM2Writer;
import org.joda.time.DateTime;
import org.opensaml.saml.saml2.core.AuthnRequest;
import org.opensaml.saml.saml2.core.Response;
import org.opensaml.saml.saml2.core.Status;

/**
 * A mock IdP for SAML SSO. The user is already authenticated via HTTP/BA.
 */
@Path("/samlsso")
public class SamlSso {

    static {
        OpenSAMLUtil.initSamlEngine();
    }

    private final DocumentBuilderFactory docBuilderFactory;
    private MessageContext messageContext;

    public SamlSso() {
        docBuilderFactory = DocumentBuilderFactory.newInstance();
        docBuilderFactory.setNamespaceAware(true);
    }

    @POST
    public javax.ws.rs.core.Response login(@FormParam("SAMLRequest") String samlRequest,
        @FormParam("RelayState") String relayState) throws Exception {

        return login(samlRequest, relayState, "POST");
    }

    @GET
    public javax.ws.rs.core.Response login(@QueryParam("SAMLRequest") String samlRequest,
            @QueryParam("RelayState") String relayState, @QueryParam("binding") String binding) throws Exception {

        AuthnRequest request = extractRequest(samlRequest);

        String racs = request.getAssertionConsumerServiceURL();
        String requestIssuer = request.getIssuer().getValue();

        // Create the response
        Element response = createResponse(request.getID(), racs, requestIssuer);
        boolean redirect = "REDIRECT".equals(binding);
        String responseStr = encodeResponse(response, redirect);

        if (redirect) {
            return redirectResponse(relayState, racs, responseStr);
        } else {
            return postBindingResponse(relayState, racs, responseStr);
        }
    }

    @Context
    public void setMessageContext(MessageContext mc) {
        this.messageContext = mc;
    }

    protected Element createResponse(String requestID, String racs, String requestIssuer) throws Exception {
        DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
        Document doc = docBuilder.newDocument();

        Status status =
            SAML2PResponseComponentBuilder.createStatus(
                "urn:oasis:names:tc:SAML:2.0:status:Success", null
            );
        String issuer = messageContext.getUriInfo().getAbsolutePath().toString();
        Response response =
            SAML2PResponseComponentBuilder.createSAMLResponse(requestID, issuer, status);

        // Create an AuthenticationAssertion
        SAML2CallbackHandler callbackHandler = new SAML2CallbackHandler();
        callbackHandler.setIssuer(issuer);
        String user = messageContext.getSecurityContext().getUserPrincipal().getName();
        callbackHandler.setSubjectName(user);

        // Subject Confirmation Data
        SubjectConfirmationDataBean subjectConfirmationData = new SubjectConfirmationDataBean();
        subjectConfirmationData.setAddress(messageContext.getHttpServletRequest().getRemoteAddr());
        subjectConfirmationData.setInResponseTo(requestID);
        subjectConfirmationData.setNotAfter(new DateTime().plusMinutes(5));
        subjectConfirmationData.setRecipient(racs);
        callbackHandler.setSubjectConfirmationData(subjectConfirmationData);

        // Audience Restriction
        ConditionsBean conditions = new ConditionsBean();
        conditions.setTokenPeriodMinutes(5);

        AudienceRestrictionBean audienceRestriction = new AudienceRestrictionBean();
        audienceRestriction.setAudienceURIs(Collections.singletonList(requestIssuer));
        conditions.setAudienceRestrictions(Collections.singletonList(audienceRestriction));
        callbackHandler.setConditions(conditions);

        SAMLCallback samlCallback = new SAMLCallback();
        SAMLUtil.doSAMLCallback(callbackHandler, samlCallback);
        SamlAssertionWrapper assertion = new SamlAssertionWrapper(samlCallback);

        Crypto issuerCrypto = CryptoFactory.getInstance("stsKeystoreB.properties");
        assertion.signAssertion("realmb", "realmb", issuerCrypto, false);

        response.getAssertions().add(assertion.getSaml2());

        Element policyElement = OpenSAMLUtil.toDom(response, doc);
        doc.appendChild(policyElement);

        return policyElement;
    }

    protected String encodeResponse(Element response, boolean redirect) throws IOException {
        String responseMessage = DOM2Writer.nodeToString(response);
        System.out.println("RESP: " + responseMessage);

        final byte[] deflatedBytes;
        if (redirect) {
            DeflateEncoderDecoder encoder = new DeflateEncoderDecoder();
            deflatedBytes = encoder.deflateToken(responseMessage.getBytes(StandardCharsets.UTF_8));
        } else {
            deflatedBytes = responseMessage.getBytes(StandardCharsets.UTF_8);
        }

        return Base64Utility.encode(deflatedBytes);
    }

    protected AuthnRequest extractRequest(String samlRequest) throws Base64Exception,
        DataFormatException, XMLStreamException, IOException, WSSecurityException {
        byte[] deflatedToken = Base64Utility.decode(samlRequest);

        final Document responseDoc;
        try (InputStream tokenStream = new DeflateEncoderDecoder().inflateToken(deflatedToken)) {
            responseDoc = StaxUtils.read(new InputStreamReader(tokenStream, StandardCharsets.UTF_8));
        }
        AuthnRequest request =
            (AuthnRequest)OpenSAMLUtil.fromDom(responseDoc.getDocumentElement());
        System.out.println(DOM2Writer.nodeToString(responseDoc));
        return request;
    }

    protected javax.ws.rs.core.Response postBindingResponse(String relayState, String racs, String responseStr)
        throws IOException {
        String responseTemplate;
        try (InputStream inputStream = this.getClass().getResourceAsStream("/TemplateSAMLResponse.xml")) {
            responseTemplate = IOUtils.toString(inputStream, StandardCharsets.UTF_8.name());
        }

        // Perform Redirect to RACS
        responseTemplate = responseTemplate.replace("%RESPONSE_URL%", racs);
        responseTemplate = responseTemplate.replace("%SAMLResponse%", responseStr);
        responseTemplate = responseTemplate.replace("%RelayState%", relayState);

        return javax.ws.rs.core.Response.ok(responseTemplate).type(MediaType.TEXT_HTML).build();
    }

    protected javax.ws.rs.core.Response redirectResponse(String relayState, String racs, String responseStr) {
        // Perform Redirect to RACS
        UriBuilder ub = UriBuilder.fromUri(racs);
        ub.queryParam("SAMLResponse", responseStr);
        ub.queryParam("RelayState", relayState);

        return javax.ws.rs.core.Response.seeOther(ub.build()).build();
    }

}
