org.codice.ddf.security.idp.client.LogoutRequestService.java Source code

Java tutorial

Introduction

Here is the source code for org.codice.ddf.security.idp.client.LogoutRequestService.java

Source

/**
 * Copyright (c) Codice Foundation
 * <p>
 * This is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser
 * General Public License as published by the Free Software Foundation, either version 3 of the
 * License, or any later version.
 * <p>
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details. A copy of the GNU Lesser General Public License
 * is distributed along with this program and can be found at
 * <http://www.gnu.org/licenses/lgpl.html>.
 */
package org.codice.ddf.security.idp.client;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.xml.stream.XMLStreamException;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.cxf.helpers.DOMUtils;
import org.apache.karaf.jaas.boot.principal.RolePrincipal;
import org.apache.wss4j.common.ext.WSSecurityException;
import org.apache.wss4j.common.saml.OpenSAMLUtil;
import org.apache.wss4j.common.util.DOM2Writer;
import org.codice.ddf.configuration.SystemBaseUrl;
import org.codice.ddf.security.common.jaxrs.RestSecurity;
import org.opensaml.saml.saml2.core.LogoutRequest;
import org.opensaml.saml.saml2.core.LogoutResponse;
import org.opensaml.saml.saml2.core.StatusCode;
import org.opensaml.xmlsec.signature.SignableXMLObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;

import ddf.security.SecurityConstants;
import ddf.security.assertion.SecurityAssertion;
import ddf.security.assertion.impl.SecurityAssertionImpl;
import ddf.security.common.SecurityTokenHolder;
import ddf.security.common.audit.SecurityLogger;
import ddf.security.encryption.EncryptionService;
import ddf.security.http.SessionFactory;
import ddf.security.samlp.LogoutMessage;
import ddf.security.samlp.SamlProtocol;
import ddf.security.samlp.SimpleSign;
import ddf.security.samlp.ValidationException;
import ddf.security.samlp.impl.HtmlResponseTemplate;
import ddf.security.samlp.impl.RelayStates;
import ddf.security.samlp.impl.SamlValidator;

@Path("logout")
public class LogoutRequestService {

    private static final Logger LOGGER = LoggerFactory.getLogger(LogoutRequestService.class);

    private static final String SAML_REQUEST = "SAMLRequest";

    private static final String SAML_RESPONSE = "SAMLResponse";

    private static final String RELAY_STATE = "RelayState";

    private static final String SIG_ALG = "SigAlg";

    private static final String SIGNATURE = "Signature";

    static {
        OpenSAMLUtil.initSamlEngine();
    }

    private final RelayStates<String> relayStates;

    private SimpleSign simpleSign;

    private IdpMetadata idpMetadata;

    @Context
    private HttpServletRequest request;

    private LogoutMessage logoutMessage;

    private String submitForm;

    private String redirectPage;

    private EncryptionService encryptionService;

    private SessionFactory sessionFactory;

    private long logOutPageTimeOut = 3600000;

    public LogoutRequestService(SimpleSign simpleSign, IdpMetadata idpMetadata, RelayStates<String> relayStates) {
        this.simpleSign = simpleSign;
        this.idpMetadata = idpMetadata;
        this.relayStates = relayStates;

    }

    public void init() {
        try (InputStream submitStream = LogoutRequestService.class
                .getResourceAsStream("/templates/submitForm.handlebars");
                InputStream redirectStream = LogoutRequestService.class
                        .getResourceAsStream("/templates/redirect.handlebars")) {
            submitForm = IOUtils.toString(submitStream);
            redirectPage = IOUtils.toString(redirectStream);
        } catch (Exception e) {
            LOGGER.error("Unable to load index page for SP.", e);
        }
    }

    @GET
    @Path("/request")
    public Response sendLogoutRequest(@QueryParam("EncryptedNameIdTime") String encryptedNameIdTime) {
        String nameIdTime = encryptionService.decrypt(encryptedNameIdTime);
        String[] nameIdTimeArray = StringUtils.split(nameIdTime, "\n");
        if (nameIdTimeArray.length == 2) {
            try {
                String name = nameIdTimeArray[0];
                Long time = Long.parseLong(nameIdTimeArray[1]);
                if (System.currentTimeMillis() - time > logOutPageTimeOut) {
                    String msg = String.format(
                            "Logout request was older than %sms old so it was rejected. Please refresh page and request again.",
                            logOutPageTimeOut);
                    LOGGER.error(msg);
                    return buildLogoutResponse(msg);
                }
                logout();
                LogoutRequest logoutRequest = logoutMessage.buildLogoutRequest(name, getEntityId());

                String relayState = relayStates.encode(name);

                return getLogoutRequest(relayState, logoutRequest);
            } catch (Exception e) {
                String msg = "Failed to create logout request.";
                LOGGER.error(msg, e);
                return buildLogoutResponse(msg);
            }

        } else {
            String msg = "Failed to decrypt logout request params. Invalid number of params.";
            LOGGER.error(msg);
            return buildLogoutResponse(msg);
        }

    }

    private Response getLogoutRequest(String relayState, LogoutRequest logoutRequest) {
        try {

            String binding = idpMetadata.getSingleLogoutBinding();
            if (SamlProtocol.POST_BINDING.equals(binding)) {
                return getSamlpPostLogoutRequest(relayState, logoutRequest);
            } else if (SamlProtocol.REDIRECT_BINDING.equals(binding)) {
                return getSamlpRedirectLogoutRequest(relayState, logoutRequest);
            } else {
                return buildLogoutResponse(
                        "The identity provider does not support either POST or Redirect bindings.");
            }
        } catch (Exception e) {
            String msg = "Failed to create logout request";
            LOGGER.error(msg, e);
            return buildLogoutResponse(msg);
        }
    }

    private Response getSamlpPostLogoutRequest(String relayState, LogoutRequest logoutRequest)
            throws SimpleSign.SignatureException, WSSecurityException {
        LOGGER.debug("Configuring SAML LogoutRequest for POST.");
        Document doc = DOMUtils.createDocument();
        doc.appendChild(doc.createElement("root"));
        LOGGER.debug("Signing SAML POST LogoutRequest.");
        simpleSign.signSamlObject(logoutRequest);
        LOGGER.debug("Converting SAML Request to DOM");
        String assertionResponse = DOM2Writer.nodeToString(OpenSAMLUtil.toDom(logoutRequest, doc));
        String encodedSamlRequest = Base64.getEncoder()
                .encodeToString(assertionResponse.getBytes(StandardCharsets.UTF_8));
        String singleLogoutLocation = idpMetadata.getSingleLogoutLocation();
        String submitFormUpdated = String.format(submitForm, singleLogoutLocation, SAML_REQUEST, encodedSamlRequest,
                relayState);
        Response.ResponseBuilder ok = Response.ok(submitFormUpdated);
        return ok.build();
    }

    private Response getSamlpRedirectLogoutRequest(String relayState, LogoutRequest logoutRequest)
            throws IOException, SimpleSign.SignatureException, WSSecurityException, URISyntaxException {
        LOGGER.debug("Configuring SAML Response for Redirect.");
        Document doc = DOMUtils.createDocument();
        doc.appendChild(doc.createElement("root"));
        URI location = logoutMessage.signSamlGetRequest(logoutRequest,
                new URI(idpMetadata.getSingleLogoutLocation()), relayState);
        String redirectUpdated = String.format(redirectPage, location.toString());
        Response.ResponseBuilder ok = Response.ok(redirectUpdated);
        return ok.build();
    }

    @POST
    @Produces(MediaType.APPLICATION_FORM_URLENCODED)
    public Response postLogoutRequest(@FormParam(SAML_REQUEST) String encodedSamlRequest,
            @FormParam(SAML_REQUEST) String encodedSamlResponse, @FormParam(RELAY_STATE) String relayState) {

        if (encodedSamlRequest != null) {
            try {
                LogoutRequest logoutRequest = logoutMessage
                        .extractSamlLogoutRequest(decodeBase64(encodedSamlRequest));
                if (logoutRequest == null) {
                    String msg = "Unable to parse logout request.";
                    LOGGER.error(msg);
                    return buildLogoutResponse(msg);
                }

                new SamlValidator.Builder(simpleSign).buildAndValidate(request.getRequestURL().toString(),
                        SamlProtocol.Binding.HTTP_POST, logoutRequest);
                logout();
                LogoutResponse logoutResponse = logoutMessage.buildLogoutResponse(
                        logoutRequest.getIssuer().getValue(), StatusCode.SUCCESS, logoutRequest.getID());

                return getLogoutResponse(relayState, logoutResponse);
            } catch (WSSecurityException e) {
                String msg = "Failed to sign logout response.";
                LOGGER.error(msg, e);
                return buildLogoutResponse(msg);
            } catch (ValidationException e) {
                String msg = "Unable to validate";
                LOGGER.error(msg, e);
                return buildLogoutResponse(msg);
            } catch (XMLStreamException e) {
                String msg = "Unable to parse logout request.";
                LOGGER.error(msg, e);
                return buildLogoutResponse(msg);
            }
        } else {
            try {
                LogoutResponse logoutResponse = logoutMessage
                        .extractSamlLogoutResponse(decodeBase64(encodedSamlResponse));
                if (logoutResponse == null) {
                    String msg = "Unable to parse logout response.";
                    LOGGER.error(msg);
                    return buildLogoutResponse(msg);
                }
                new SamlValidator.Builder(simpleSign).buildAndValidate(request.getRequestURL().toString(),
                        SamlProtocol.Binding.HTTP_POST, logoutResponse);
            } catch (ValidationException e) {
                String msg = "Unable to validate";
                LOGGER.error(msg, e);
                return buildLogoutResponse(msg);
            } catch (WSSecurityException | XMLStreamException e) {
                String msg = "Unable to parse logout response.";
                LOGGER.warn(msg, e);
                return buildLogoutResponse(msg);
            }
            String nameId = "You";
            String decodedValue;
            if (relayState != null && (decodedValue = relayStates.decode(relayState)) != null) {
                nameId = decodedValue;
            }
            return buildLogoutResponse(nameId + " logged out successfully.");

        }
    }

    @GET
    public Response getLogoutRequest(@QueryParam(SAML_REQUEST) String deflatedSamlRequest,
            @QueryParam(SAML_RESPONSE) String deflatedSamlResponse, @QueryParam(RELAY_STATE) String relayState,
            @QueryParam(SIG_ALG) String signatureAlgorithm, @QueryParam(SIGNATURE) String signature) {

        if (deflatedSamlRequest != null) {
            try {
                LogoutRequest logoutRequest = logoutMessage
                        .extractSamlLogoutRequest(RestSecurity.inflateBase64(deflatedSamlRequest));
                if (logoutRequest == null) {
                    String msg = "Unable to parse logout request.";
                    return buildLogoutResponse(msg);
                }
                buildAndValidateSaml(deflatedSamlRequest, relayState, signatureAlgorithm, signature, logoutRequest);
                logout();
                String entityId = getEntityId();
                LogoutResponse logoutResponse = logoutMessage.buildLogoutResponse(entityId, StatusCode.SUCCESS,
                        logoutRequest.getID());
                return getLogoutResponse(relayState, logoutResponse);
            } catch (IOException e) {
                String msg = "Unable to decode and inflate logout request.";
                LOGGER.warn(msg, e);
                return buildLogoutResponse(msg);
            } catch (ValidationException e) {
                String msg = "Unable to validate";
                LOGGER.warn(msg, e);
                return buildLogoutResponse(msg);
            } catch (WSSecurityException | XMLStreamException e) {
                String msg = "Unable to parse logout request.";
                LOGGER.warn(msg, e);
                return buildLogoutResponse(msg);
            }
        } else {
            try {

                LogoutResponse logoutResponse = logoutMessage
                        .extractSamlLogoutResponse(RestSecurity.inflateBase64(deflatedSamlResponse));
                if (logoutResponse == null) {
                    String msg = "Unable to parse logout response.";
                    LOGGER.error(msg);
                    return buildLogoutResponse(msg);
                }
                buildAndValidateSaml(deflatedSamlResponse, relayState, signatureAlgorithm, signature,
                        logoutResponse);
                String nameId = "You";
                String decodedValue;
                if (relayState != null && (decodedValue = relayStates.decode(relayState)) != null) {
                    nameId = decodedValue;
                }
                return buildLogoutResponse(nameId + " logged out successfully.");
            } catch (IOException e) {
                String msg = "Unable to decode and inflate logout response.";
                LOGGER.warn(msg, e);
                return buildLogoutResponse(msg);
            } catch (ValidationException e) {
                String msg = "Unable to validate";
                LOGGER.warn(msg, e);
                return buildLogoutResponse(msg);
            } catch (WSSecurityException | XMLStreamException e) {
                String msg = "Unable to parse logout response.";
                LOGGER.warn(msg, e);
                return buildLogoutResponse(msg);
            }
        }
    }

    protected void buildAndValidateSaml(String samlRequest, String relayState, String signatureAlgorithm,
            String signature, SignableXMLObject xmlObject) throws ValidationException {
        new SamlValidator.Builder(simpleSign).setRedirectParams(relayState, signature, signatureAlgorithm,
                samlRequest, idpMetadata.getSigningCertificate()).buildAndValidate(
                        request.getRequestURL().toString(), SamlProtocol.Binding.HTTP_REDIRECT, xmlObject);
    }

    private String getEntityId() {
        String hostname = SystemBaseUrl.getHost();
        String port = SystemBaseUrl.getPort();
        String rootContext = SystemBaseUrl.getRootContext();

        return String.format("https://%s:%s%s/saml", hostname, port, rootContext);
    }

    private void logout() {
        HttpSession session = sessionFactory.getOrCreateSession(request);

        SecurityTokenHolder tokenHolder = ((SecurityTokenHolder) session
                .getAttribute(SecurityConstants.SAML_ASSERTION));

        SecurityAssertion securityAssertion = new SecurityAssertionImpl(tokenHolder.getSecurityToken("idp"));

        boolean hasSecurityAuditRole = Arrays.stream(System.getProperty("security.audit.roles").split(","))
                .filter(role -> securityAssertion.getPrincipals().contains(new RolePrincipal(role))).findFirst()
                .isPresent();

        if (hasSecurityAuditRole) {
            SecurityLogger.audit("Subject with admin privileges has logged out: {}",
                    securityAssertion.getPrincipal().getName());
        }

        tokenHolder.remove("idp");
    }

    private Response getLogoutResponse(String relayState, LogoutResponse samlResponse) {
        try {

            String binding = idpMetadata.getSingleLogoutBinding();
            if (SamlProtocol.POST_BINDING.equals(binding)) {
                return getSamlpPostLogoutResponse(relayState, samlResponse);
            } else if (SamlProtocol.REDIRECT_BINDING.equals(binding)) {
                return getSamlpRedirectLogoutResponse(relayState, samlResponse);
            } else {
                return buildLogoutResponse(
                        "The identity provider does not support either POST or Redirect bindings.");
            }
        } catch (Exception e) {
            String msg = "Failed to create logout response";
            LOGGER.error(msg, e);
            return buildLogoutResponse(msg);
        }

    }

    private Response getSamlpPostLogoutResponse(String relayState, LogoutResponse samlResponse)
            throws WSSecurityException, SimpleSign.SignatureException {
        LOGGER.debug("Configuring SAML Response for POST.");
        Document doc = DOMUtils.createDocument();
        doc.appendChild(doc.createElement("root"));
        LOGGER.debug("Signing SAML POST Response.");
        simpleSign.signSamlObject(samlResponse);
        LOGGER.debug("Converting SAML Response to DOM");
        String assertionResponse = DOM2Writer.nodeToString(OpenSAMLUtil.toDom(samlResponse, doc));
        String encodedSamlResponse = Base64.getEncoder()
                .encodeToString(assertionResponse.getBytes(StandardCharsets.UTF_8));

        return Response.ok(HtmlResponseTemplate.getPostPage(idpMetadata.getSingleLogoutLocation(),
                SamlProtocol.Type.RESPONSE, encodedSamlResponse, relayState)).build();
    }

    private Response getSamlpRedirectLogoutResponse(String relayState, LogoutResponse samlResponse)
            throws IOException, SimpleSign.SignatureException, WSSecurityException, URISyntaxException {
        LOGGER.debug("Configuring SAML Response for Redirect.");
        Document doc = DOMUtils.createDocument();
        doc.appendChild(doc.createElement("root"));
        URI location = logoutMessage.signSamlGetResponse(samlResponse,
                new URI(idpMetadata.getSingleLogoutLocation()), relayState);

        return Response.ok(HtmlResponseTemplate.getRedirectPage(location.toString())).build();
    }

    private String decodeBase64(String encoded) {
        return Base64.getMimeEncoder().encodeToString(encoded.getBytes(StandardCharsets.UTF_8));
    }

    private Response buildLogoutResponse(String message) {
        UriBuilder uriBuilder = UriBuilder.fromUri(SystemBaseUrl.getBaseUrl());
        uriBuilder.path("logout/logout-response.html");
        uriBuilder.queryParam("msg", message);
        return Response.seeOther(uriBuilder.build()).build();
    }

    public void setRequest(HttpServletRequest request) {
        this.request = request;
    }

    public void setLogoutMessage(LogoutMessage logoutMessage) {
        this.logoutMessage = logoutMessage;
    }

    public void setEncryptionService(EncryptionService encryptionService) {
        this.encryptionService = encryptionService;
    }

    public void setSessionFactory(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    public void setLogOutPageTimeOut(long logOutPageTimeOut) {
        this.logOutPageTimeOut = logOutPageTimeOut;
    }

}