ch.bfh.ti.ictm.iam.stiam.aa.authority.AttributeService.java Source code

Java tutorial

Introduction

Here is the source code for ch.bfh.ti.ictm.iam.stiam.aa.authority.AttributeService.java

Source

/*
 * Copyright 2014 Pascal Mainini, Marc Kunz
 * Licensed under MIT license, see included file LICENSE or
 * http://opensource.org/licenses/MIT
 */
package ch.bfh.ti.ictm.iam.stiam.aa.authority;

import ch.bfh.ti.ictm.iam.stiam.aa.directory.Directory;
import ch.bfh.ti.ictm.iam.stiam.aa.directory.DirectoryException;
import ch.bfh.ti.ictm.iam.stiam.aa.directory.DirectoryFactory;
import ch.bfh.ti.ictm.iam.stiam.aa.directory.ldap.NameIDNotFoundException;
import ch.bfh.ti.ictm.iam.stiam.aa.eligibility.EligibilityChecker;
import ch.bfh.ti.ictm.iam.stiam.aa.eligibility.EligibilityCheckerFactory;
import ch.bfh.ti.ictm.iam.stiam.aa.util.StiamConfiguration;
import ch.bfh.ti.ictm.iam.stiam.aa.util.saml.Attribute;
import ch.bfh.ti.ictm.iam.stiam.aa.util.saml.AttributeResponseBuilder;
import ch.bfh.ti.ictm.iam.stiam.aa.util.saml.ResponseBuilder;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URLEncoder;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableEntryException;
import java.security.cert.CertificateException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.transform.TransformerException;
import org.joda.time.DateTime;
import org.opensaml.DefaultBootstrap;
import org.opensaml.common.binding.BasicSAMLMessageContext;
import org.opensaml.common.binding.decoding.BaseSAMLMessageDecoder;
import org.opensaml.saml2.binding.decoding.HTTPPostDecoder;
import org.opensaml.saml2.binding.decoding.HTTPSOAP11Decoder;
import org.opensaml.saml2.core.Assertion;
import org.opensaml.saml2.core.AttributeQuery;
import org.opensaml.saml2.core.AuthnStatement;
import org.opensaml.ws.message.MessageContext;
import org.opensaml.ws.message.decoder.MessageDecodingException;
import org.opensaml.ws.soap.soap11.Envelope;
import org.opensaml.ws.transport.http.HttpServletRequestAdapter;
import org.opensaml.xml.ConfigurationException;
import org.opensaml.xml.XMLObject;
import org.opensaml.xml.io.MarshallingException;
import org.opensaml.xml.parse.XMLParserException;
import org.opensaml.xml.security.SecurityException;
import org.opensaml.xml.signature.Signature;
import org.opensaml.xml.signature.SignatureException;
import org.opensaml.xml.signature.SignatureValidator;
import org.opensaml.xml.validation.ValidationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * AttributeService-servlet of the STIAM attribute authority. Receives extended
 * SAML attribute queries and handles them.
 *
 * @author Pascal Mainini
 * @author Marc Kunz
 */
@WebServlet(urlPatterns = { "/" })
@SuppressWarnings("serial")
public class AttributeService extends HttpServlet {

    //////////////////////////////////////// Fields
    private static final Logger logger = LoggerFactory.getLogger(AttributeService.class);
    private static final StiamConfiguration config = StiamConfiguration.getInstance();
    private static EligibilityChecker eligibilityChecker;
    private static Directory directory;

    //////////////////////////////////////// Methods
    /**
     * Inherited from HttpServlet, some basic initialization is performed here.
     *
     * @throws ServletException If initialisation fails for some reason.
     */
    @Override
    public void init() throws ServletException {
        try {
            DefaultBootstrap.bootstrap(); // initialise OpenSAML
            directory = DirectoryFactory.getInstance().createDirectory();
            eligibilityChecker = EligibilityCheckerFactory.getInstance().createEligibilityChecker();
        } catch (ConfigurationException ex) {
            logger.error("Error initializing attribute service: {}", ex.getMessage());
            throw new ServletException(ex);
        }
        logger.info("Sucessfully initialized Attribute Service (AS)!");
    }

    /**
     * Method inherited from HttpServlet. As all SAML-requests are received with
     * the POST-method, we show a short information page on GET-requests to
     * inform the user/operator that the AA is up and running.
     *
     * @param req The request-instance obtained from the container
     * @param res The response-instance obtained from the container
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
        res.setStatus(200);
        res.setContentType("text/html");
        try {
            final PrintWriter pw = res.getWriter();
            pw.println("<!DOCTYPE html>");
            pw.println("<head>");
            pw.println("<meta charset=\"utf-8\"/>");
            pw.println("<title>STIAM Attribute Authority</title>");
            pw.println("</head>");
            pw.println("<body>");
            pw.println("<h1>Welcome to the STIAM Attribute Authority!</h1>");
            pw.println("The authority is <b>up and running</b>, accepting attribute requests via HTTP POST.");
            pw.println("</body>\n</html>");
        } catch (IOException ex) {
            logger.error("Cannot send infopage, unable to write to response: {}", ex.getMessage());
        }
    }

    /**
     * Method inherited from HttpServlet. Handles POST-requests, tries to
     * extract extended SAML attribute queries from the request and return
     * appropriate responses.
     *
     * @param req The request-instance obtained from the container
     * @param res The response-instance obtained from the container
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
        logger.info("Request received!");
        final DateTime receptionTime = DateTime.now();

        //////////////////// Decode raw request
        logger.debug("Trying to decode raw request...");
        final MessageContext messageContext = new BasicSAMLMessageContext();
        messageContext.setInboundMessageTransport(new HttpServletRequestAdapter(req));
        final BaseSAMLMessageDecoder messageDecoder;
        if (config.getBinding() == StiamConfiguration.Binding.HTTP_POST) {
            logger.debug("Using HTTPPostDecoder for decoding...");
            messageDecoder = new HTTPPostDecoder();
        } else {
            logger.debug("Using HTTPSOAP11Decoder for decoding...");
            messageDecoder = new HTTPSOAP11Decoder();
        }

        try {
            messageDecoder.decode(messageContext);
        } catch (MessageDecodingException | SecurityException | IllegalArgumentException ex) {
            sendSAMLError(res, 400, "Decoding failed: " + ex.getMessage(), "", "", new String[] {
                    ResponseBuilder.STATUS_CODE_REQUESTER, ResponseBuilder.STATUS_CODE_REQUEST_DENIED });
            return;
        }
        logger.debug("Decoding succeeded!");

        //////////////////// Read out attribute-query
        logger.debug("Trying to read AttributeQuery...");
        AttributeQuery attributeQuery = null;
        try {
            if (config.getBinding() == StiamConfiguration.Binding.HTTP_POST) {
                attributeQuery = (AttributeQuery) messageContext.getInboundMessage();
            } else {
                final Envelope soapEnvelope = (Envelope) messageContext.getInboundMessage();
                final List<XMLObject> bodyElements = soapEnvelope.getBody().getUnknownXMLObjects();

                for (XMLObject element : bodyElements) {
                    if (element instanceof AttributeQuery) {
                        attributeQuery = (AttributeQuery) element;
                    }
                }
            }
        } catch (ClassCastException ex) {
            sendSAMLError(res, 400, "AttributeQuery could not be read!", "", "", new String[] {
                    ResponseBuilder.STATUS_CODE_REQUESTER, ResponseBuilder.STATUS_CODE_REQUEST_DENIED });
            return;
        }

        if (attributeQuery == null) {
            sendSAMLError(res, 400, "No AttributeQuery found!", "", "", new String[] {
                    ResponseBuilder.STATUS_CODE_REQUESTER, ResponseBuilder.STATUS_CODE_REQUEST_DENIED });
            return;
        }

        logger.debug("Sucessfully read AttributeQuery!");

        //////////////////// Try to read out QueryID, Issuer and NameID of the query
        logger.debug("Reading QueryID, Issuer and NameID of the query...");
        final String queryID;
        final String queryIssuer;
        final String nameID;
        try {
            queryID = attributeQuery.getID();
            queryIssuer = attributeQuery.getIssuer().getValue();
            nameID = attributeQuery.getSubject().getNameID().getValue();
        } catch (Exception ex) {
            sendSAMLError(res, 400, "Unable to read essential attributes of the query!", "", "", new String[] {
                    ResponseBuilder.STATUS_CODE_REQUESTER, ResponseBuilder.STATUS_CODE_REQUEST_DENIED });
            return;
        }
        logger.debug("Query with ID '{}' received from issuer '{}' for subject '{}'.", queryID, queryIssuer,
                nameID);

        //////////////////// Verify signature of the attribute query
        if (config.verifyQuerySignature()) {
            logger.debug("Trying to verify signature of the attribute query...");
            if (!verifySignature(attributeQuery.getSignature(), attributeQuery.getIssuer().getValue().toString())) {
                sendSAMLError(res, 400, "Signature validation failed!", queryIssuer, queryID, new String[] {
                        ResponseBuilder.STATUS_CODE_REQUESTER, ResponseBuilder.STATUS_CODE_REQUEST_DENIED });
                return;
            }
            logger.debug("Signature verified successfully!");
        }

        //////////////////// Check if subject is eligible...
        logger.debug("Checking subject eligibility...");
        if (!eligibilityChecker.isEligible(nameID)) {
            sendSAMLError(res, 400, "Subject not eligible!", queryIssuer, queryID, new String[] {
                    ResponseBuilder.STATUS_CODE_RESPONDER, ResponseBuilder.STATUS_CODE_UNKNOWN_PRINCIPAL });
            return;
        }
        logger.debug("Subject is eligible, continueing");

        //////////////////// Verify if we have extensions and if they contain an authentication statement
        if (config.verifyAuthnStatement()) {
            logger.debug("Trying to verify embedded Authn-Assertion...");

            logger.debug("Reading out assertion...");
            final AuthnStatement authnStatement;
            final Assertion assertion;
            final String assertionNameID;
            try {
                // Note: this code only works for the very limited assumption that we only have one
                // extension in the query (which is to be assumed to be an Assertion) and that this
                // in turn contains exactly one AuthnStatement. For other than PoC-code, this must be
                // implemented properly...
                assertion = (Assertion) attributeQuery.getExtensions().getUnknownXMLObjects().get(0);
                assertionNameID = assertion.getSubject().getNameID().getValue();
                authnStatement = (AuthnStatement) assertion.getStatements().get(0);
                if (authnStatement == null) {
                    sendSAMLError(res, 400, "Unable to read embedded authentication statement!", queryIssuer,
                            queryID, new String[] { ResponseBuilder.STATUS_CODE_REQUESTER,
                                    ResponseBuilder.STATUS_CODE_NO_AUTHN_CONTEXT });
                    return;
                }
            } catch (Exception ex) {
                sendSAMLError(res, 400, "Unable to read embedded authentication statement!", queryIssuer, queryID,
                        new String[] { ResponseBuilder.STATUS_CODE_REQUESTER,
                                ResponseBuilder.STATUS_CODE_NO_AUTHN_CONTEXT });
                return;
            }
            logger.debug("Successfully read assertion!");

            //////////////////// Compare NameID of authentication assertion to attribute query
            logger.debug("Ensuring that NameIDs match...");
            if (!assertionNameID.equals(nameID)) {
                sendSAMLError(res, 400, "NameID of assertion does not match NameID of attribute query!",
                        queryIssuer, queryID, new String[] { ResponseBuilder.STATUS_CODE_REQUESTER,
                                ResponseBuilder.STATUS_CODE_NO_AUTHN_CONTEXT });
                return;
            }
            logger.debug("NameIDs are equal!");

            //////////////////// Verify signature of the assertion
            if (config.verifyAuthnSignature()) {
                logger.debug("Trying to validate signature of authentication statement...");
                if (!verifySignature(assertion.getSignature(), assertion.getIssuer().getValue().toString())) {
                    sendSAMLError(res, 400, "Signature validation failed!", queryIssuer, queryID, new String[] {
                            ResponseBuilder.STATUS_CODE_REQUESTER, ResponseBuilder.STATUS_CODE_NO_AUTHN_CONTEXT });
                    return;
                }
                logger.debug("Signature verified successfully!");
            }

            //////////////////// Verify NotBefore / NotOnOrAfter in the Conditions of the authentication statement
            if (config.verifyAuthnTimespan()) {
                logger.debug("Trying to validate authentication-timespan...");
                if (receptionTime.isBefore(assertion.getConditions().getNotBefore())
                        || receptionTime.isEqual(assertion.getConditions().getNotOnOrAfter())
                        || receptionTime.isAfter(assertion.getConditions().getNotOnOrAfter())) {
                    sendSAMLError(res, 400, "Received statement is not in authentication-timespan!", queryIssuer,
                            queryID, new String[] { ResponseBuilder.STATUS_CODE_REQUESTER,
                                    ResponseBuilder.STATUS_CODE_NO_AUTHN_CONTEXT });
                    return;
                }
                logger.debug("Authentication-timespan verifed!");
            }

            logger.debug("Authn-Assertion found and validated!");
        }

        //////////////////// Read out attributes
        logger.debug("Reading Attributes...");
        final HashMap<String, Attribute> attributes = new HashMap<>(10);
        for (org.opensaml.saml2.core.Attribute attr : attributeQuery.getAttributes()) {
            attributes.put(attr.getName(), new Attribute(attr));
        }
        if (attributes.isEmpty()) {
            sendSAMLError(res, 400, "No attributes found in query!", queryIssuer, queryID, new String[] {
                    ResponseBuilder.STATUS_CODE_REQUESTER, ResponseBuilder.STATUS_CODE_REQUEST_DENIED });
            return;
        }
        logger.debug("Found {} attributes in query", attributes.size());

        for (Attribute attr : attributes.values()) {
            logger.debug("Attribute: {}", attr);
        }

        //////////////////// Query attributes in directory
        logger.debug("Retrieving attributes from directory...");
        try {
            final String[] attributeNames = new String[attributes.size()];
            int i = 0;
            for (String name : attributes.keySet()) {
                attributeNames[i] = name;
                i++;
            }
            final Map<String, String> fetchedAttributes = directory.fetchAttributes(nameID, attributeNames);

            for (String name : attributeNames) {
                attributes.get(name).setValue(fetchedAttributes.get(name));
                logger.debug("Got value: {}", attributes.get(name));
            }
        } catch (NameIDNotFoundException ex) {
            sendSAMLError(res, 400, "Subject not found!", queryIssuer, queryID, new String[] {
                    ResponseBuilder.STATUS_CODE_RESPONDER, ResponseBuilder.STATUS_CODE_UNKNOWN_PRINCIPAL });
            return;
        } catch (DirectoryException ex) {
            sendError(res, 500, "Error while fetching Attributes in directory: " + ex.getMessage());
            return;
        }
        logger.debug("Attributes retrieved!");

        //////////////////// Return attribute assertion
        logger.debug("Sending response...");
        try {
            logger.debug("Building attribute response...");
            final AttributeResponseBuilder builder = new AttributeResponseBuilder(queryIssuer, queryID, nameID,
                    attributes.values());
            if (config.getBinding() == StiamConfiguration.Binding.HTTP_POST) {
                res.setStatus(200);
                res.setContentType("text/html");

                final PrintWriter pw = res.getWriter();
                pw.println("<!DOCTYPE html>");
                pw.println("<html><head>\n<meta charset=\"utf-8\"/>\n<title>SAMLResponse</title>\n</head>");
                pw.println("<body onload=\"function () { document.forms[0].submit(); }\">");
                pw.println("<form method=\"post\" action=\"" + queryIssuer + "\">");
                pw.println("<input type=\"hidden\" name=\"SAMLResponse\" value=\""
                        + URLEncoder.encode(builder.buildBase64(), "UTF-8") + "\"/>");
                pw.println("</form>");
                pw.println("</body>\n</html>");
            } else {
                res.setStatus(200);
                res.setContentType("text/xml;charset=UTF-8");

                final PrintWriter pw = res.getWriter();
                pw.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
                pw.print(
                        "<soap11:Envelope xmlns:soap11=\"http://schemas.xmlsoap.org/soap/envelope/\"><soap11:Body>");
                pw.print(builder.build().substring(38)); // FIXME ugly substring-hack
                pw.print("</soap11:Body></soap11:Envelope>");
            }
        } catch (ConfigurationException | NoSuchAlgorithmException | KeyStoreException | CertificateException
                | UnrecoverableEntryException | SecurityException | MarshallingException | SignatureException
                | XMLParserException | TransformerException ex) {
            sendError(res, 500, "Error while building attribute response: " + ex.getMessage());
            return;
        }

        logger.info("Request handled!");
    }

    /**
     * Helper method to verify a SAML signature
     *
     * @param signature the signature to verify
     * @return true if verification was successful, false if not
     */
    private boolean verifySignature(Signature signature, String alias) {
        try {
            final SignatureValidator signatureValidator = new SignatureValidator(
                    config.getVerificationCredential(alias));
            signatureValidator.validate(signature);
        } catch (KeyStoreException | NoSuchAlgorithmException | CertificateException | UnrecoverableEntryException
                | IOException ex) {
            logger.error("Error while obtaining verification credential: {}", ex.getMessage());
            return false;
        } catch (ValidationException ex) {
            logger.error("Error while validating signature: {}", ex.getMessage());
            return false;
        }

        return true;
    }

    /**
     * Helper method for sending a SAML status-messages with the given
     * statuscodes.
     *
     * @param res The HttpServletResponse used for sending the message
     * @param httpStatusCode The HTTP-statuscode to set on the returned message
     * @param message A message used for logging the error on the AA
     * @param destination SAML-destination of the message
     * @param queryID ID of the SAML-query causing the message
     * @param statusCodes An array of statuscodes to include in the message
     */
    private void sendSAMLError(HttpServletResponse res, int httpStatusCode, String message, String destination,
            String queryID, String[] statusCodes) {
        logger.error(message);
        logger.debug("Sending error as SAML status response with the following status code(s): {}",
                (Object[]) statusCodes);

        res.setStatus(httpStatusCode);
        res.setContentType("text/plain");
        try {
            final ResponseBuilder builder = new ResponseBuilder(destination, queryID, statusCodes);

            final PrintWriter pw = res.getWriter();
            pw.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
            pw.print("<soap11:Envelope xmlns:soap11=\"http://schemas.xmlsoap.org/soap/envelope/\"><soap11:Body>");
            pw.print(builder.build().substring(38)); // FIXME ugly substring-hack
            pw.print("</soap11:Body></soap11:Envelope>");

        } catch (ConfigurationException | NoSuchAlgorithmException | KeyStoreException | CertificateException
                | UnrecoverableEntryException | SecurityException | MarshallingException | SignatureException
                | XMLParserException | TransformerException | IOException ex) {
            sendError(res, 500, "Error while sending error, cause: {}" + ex.getMessage());
        }
    }

    /**
     * Helper method for sending a textual error message.
     *
     * @param res The HttpServletResponse used for sending the message
     * @param httpStatusCode The HTTP-statuscode to set on the returned message
     * @param message The message which is logged and sent to the requester
     */
    private void sendError(HttpServletResponse res, int httpStatusCode, String message) {
        logger.error(message);

        res.setStatus(httpStatusCode);
        res.setContentType("text/plain");
        try {
            res.getWriter().println(message);
        } catch (IOException ex1) {
            logger.error("Cannot send error, unable to write to response: {}", ex1.getMessage());
        }
    }
}