org.entcore.auth.controllers.SamlController.java Source code

Java tutorial

Introduction

Here is the source code for org.entcore.auth.controllers.SamlController.java

Source

/*
 * Copyright  "Open Digital Education", 2015
 *
 * This program is published by "Open Digital Education".
 * You must indicate the name of the software and the company in any production /contribution
 * using the software and indicate on the home page of the software industry in question,
 * "powered by Open Digital Education" with a reference to the website: https://opendigitaleducation.com/.
 *
 * This program is free software, licensed under the terms of the GNU Affero General Public License
 * as published by the Free Software Foundation, version 3 of the License.
 *
 * You can redistribute this application and/or modify it since you respect the terms of the GNU Affero General Public License.
 * If you modify the source code and then use this modified source code in your creation, you must make available the source code of your modifications.
 *
 * You should have received a copy of the GNU Affero General Public License along with the software.
 * If not, please see : <http://www.gnu.org/licenses/>. Full compliance requires reading the terms of this license and following its directives.
    
 */

package org.entcore.auth.controllers;

import fr.wseduc.rs.Get;
import fr.wseduc.rs.Post;
import fr.wseduc.security.ActionType;
import fr.wseduc.security.SecuredAction;
import fr.wseduc.webutils.Either;
import fr.wseduc.webutils.I18n;
import fr.wseduc.webutils.request.CookieHelper;
import fr.wseduc.webutils.request.RequestUtils;
import fr.wseduc.webutils.security.HmacSha1;
import io.vertx.core.buffer.Buffer;
import org.entcore.auth.security.SamlUtils;
import org.entcore.auth.services.FederationService;
import org.entcore.auth.services.SamlServiceProvider;
import org.entcore.auth.services.SamlServiceProviderFactory;
import org.entcore.auth.services.impl.FederationServiceImpl;
import org.entcore.common.http.response.DefaultPages;
import org.entcore.common.user.UserInfos;
import org.entcore.common.user.UserUtils;
import org.opensaml.DefaultBootstrap;
import org.opensaml.saml2.core.Assertion;
import org.opensaml.saml2.core.AuthnStatement;
import org.opensaml.saml2.core.Response;
import org.opensaml.xml.ConfigurationException;
import org.opensaml.xml.io.MarshallingException;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.eventbus.Message;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import org.vertx.java.core.http.RouteMatcher;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

import javax.xml.bind.Element;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathFactory;
import java.io.File;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static fr.wseduc.webutils.Utils.handlerToAsyncHandler;
import static fr.wseduc.webutils.Utils.isEmpty;
import static fr.wseduc.webutils.Utils.isNotEmpty;
import static org.entcore.common.http.response.DefaultResponseHandler.arrayResponseHandler;

public class SamlController extends AbstractFederateController {

    private SamlServiceProviderFactory spFactory;
    private FederationService federationService;
    private JsonObject samlWayfParams;
    private JsonObject samlWayfMustacheFormat;
    private String ignoreCallBackPattern;
    private boolean softSlo;
    private boolean federatedAuthenticateError = false;

    // regex used to find namequalifier in session nameid (Mongo)
    private String NAME_QUALIFIER_REGEXP = ".*\\sNameQualifier=\"([^\"]*)\".*";
    private final Pattern NAME_QUALIFIER_PATTERN = Pattern.compile(NAME_QUALIFIER_REGEXP);

    private static final String SESSIONS_COLLECTION = "sessions";

    @Override
    public void init(Vertx vertx, JsonObject config, RouteMatcher rm,
            Map<String, fr.wseduc.webutils.security.SecuredAction> securedActions) {
        super.init(vertx, config, rm, securedActions);

        // load soft-slo property : true = normal slo, false = redirect instead of slo
        softSlo = config.getBoolean("soft-slo", false);

        federatedAuthenticateError = config.getBoolean("federated-authenticate-error", false);

        // load nameQualifierRegex (in-case mongoDb NameId format change)
        String nameQualifierRegex = config.getString("nameQualifierRegex");
        if (nameQualifierRegex != null && !nameQualifierRegex.trim().isEmpty()) {
            if (log.isDebugEnabled()) {
                log.debug("Using nameQualifierRegex specified : " + nameQualifierRegex);
            }
            this.NAME_QUALIFIER_REGEXP = nameQualifierRegex;
        } else {
            if (log.isDebugEnabled()) {
                log.debug("Using default nameQualifierRegex : " + NAME_QUALIFIER_REGEXP);
            }
        }
    }

    public SamlController() throws ConfigurationException {
        DefaultBootstrap.bootstrap();
        this.federationService = new FederationServiceImpl();
    }

    @Get("/saml/wayf")
    public void wayf(HttpServerRequest request) {
        if (samlWayfParams != null) {
            if (samlWayfMustacheFormat == null) {
                final JsonArray wmf = new fr.wseduc.webutils.collections.JsonArray();
                for (String attr : samlWayfParams.fieldNames()) {
                    JsonObject i = samlWayfParams.getJsonObject(attr);
                    if (i == null)
                        continue;
                    final String acs = i.getString("acs");
                    if (isEmpty(acs))
                        continue;
                    URI uri;
                    try {
                        uri = new URI(acs);
                    } catch (URISyntaxException e) {
                        log.error("Invalid acs URI", e);
                        continue;
                    }
                    JsonObject o = new JsonObject().put("name", attr).put("uri",
                            uri.getScheme() + "://" + uri.getHost()
                                    + (attr.startsWith("login") ? "/auth/login" : "/auth/saml/authn/" + attr));
                    wmf.add(o);
                }
                samlWayfMustacheFormat = new JsonObject().put("providers", wmf);
            }
            String callBack = request.params().get("callBack");
            final JsonObject swmf;
            if (isNotEmpty(callBack)
                    && (ignoreCallBackPattern == null || !callBack.matches(ignoreCallBackPattern))) {
                try {
                    callBack = URLEncoder.encode(callBack, "UTF-8");
                } catch (UnsupportedEncodingException e) {
                    log.error("Error encode wayf callback.", e);
                }
                swmf = samlWayfMustacheFormat.copy();
                for (Object o : swmf.getJsonArray("providers")) {
                    if (!(o instanceof JsonObject))
                        continue;
                    final String uri = ((JsonObject) o).getString("uri");
                    if (isNotEmpty(uri) && !uri.contains("callBack")) {
                        ((JsonObject) o).put("uri", uri + (uri.contains("?") ? "&" : "?") + "callBack=" + callBack);
                    }
                }
            } else {
                swmf = samlWayfMustacheFormat;
            }
            renderView(request, swmf, "wayf.html", null);
        } else {
            request.response().setStatusCode(401).setStatusMessage("Unauthorized")
                    .putHeader("content-type", "text/html").end(DefaultPages.UNAUTHORIZED.getPage());
        }
    }

    @Get("/saml/authn/:providerId")
    public void auth(final HttpServerRequest request) {
        final JsonObject item = samlWayfParams.getJsonObject(request.params().get("providerId"));
        if (item == null) {
            forbidden(request, "invalid.provider");
            return;
        }
        final JsonObject event = item.copy().put("action", "generate-authn-request");
        vertx.eventBus().send("saml", event, handlerToAsyncHandler(new Handler<Message<JsonObject>>() {
            @Override
            public void handle(Message<JsonObject> event) {
                if (log.isDebugEnabled()) {
                    log.debug("authn request : " + event.body().encodePrettily());
                }
                final String authnRequest = event.body().getString("authn-request");
                if (isNotEmpty(authnRequest)) {
                    CookieHelper.getInstance().setSigned("relaystate", event.body().getString("relay-state"), 900,
                            request);
                    final String callBack = request.params().get("callBack");
                    if (isNotEmpty(callBack)) {
                        CookieHelper.getInstance().setSigned("callback", callBack, 900, request);
                    }
                    redirect(request, authnRequest, "");
                } else {
                    badRequest(request, "empty.authn.request");
                }
            }
        }));
    }

    /**
     * Generate base64 SAMLResponse to the service provider specified.
     * Then HTML auto-submit FORM is created with the samlResponse and render the page.
     *
     * Finally if user is recognnized and authenticated ine th SP side, he will access to the service.
     *
     * @param request
     */
    @Get("/saml/sso/:providerId")
    @SecuredAction(value = "", type = ActionType.AUTHENTICATED)
    public void sso(final HttpServerRequest request) {
        UserUtils.getUserInfos(eb, request, new Handler<UserInfos>() {
            @Override
            public void handle(final UserInfos user) {
                final String serviceProviderId = request.params().get("providerId");
                ssoGenerateSAML(user, serviceProviderId, request);
            }
        });
    }

    private void ssoGenerateSAML(UserInfos user, String serviceProviderId, HttpServerRequest request) {
        if (serviceProviderId == null || serviceProviderId.trim().isEmpty()) {
            forbidden(request, "invalid.provider");
            return;
        }

        // Get connected user sessionId
        final String sessionId = CookieHelper.getInstance().getSigned("oneSessionId", request);

        // Send to the bus to generate the SAMLResponse
        JsonObject event = new JsonObject().put("action", "generate-saml-response").put("SP", serviceProviderId)
                .put("userId", user.getUserId()).put("nameid", sessionId)
                .put("host", getScheme(request) + "://" + getHost(request));
        vertx.eventBus().send("saml", event, handlerToAsyncHandler(new Handler<Message<JsonObject>>() {
            @Override
            public void handle(Message<JsonObject> event) {

                // Get base64 SAMLResponse generated
                String samlResponse = event.body().getString("SAMLResponse64");
                if (log.isDebugEnabled()) {
                    log.debug("base64 samlResponse get from controller : " + samlResponse);
                }

                // Assertion Consumer Service location
                String destination = event.body().getString("destination");
                // If generation succeed, an HTML auto-submit FORM is created
                String error = event.body().getString("error");
                if (isNotEmpty(samlResponse) && isNotEmpty(destination) && (error == null || error.isEmpty())) {
                    renderSamlResponse(user, samlResponse, serviceProviderId, destination, request);
                } else {
                    // Else redirect to the login page
                    redirect(request, "");
                }

            }
        }));
    }

    @Get("/saml/redirect/sso/")
    public void ssoredirectGet(final HttpServerRequest request) {
        log.info("ssoredirect GET called");
        ssoRedirect(request.params().get("SAMLRequest"), request);
    }

    @Post("/saml/post/sso/")
    public void ssoredirectPost(final HttpServerRequest request) {
        log.info("ssoredirect POST called");
        request.setExpectMultipart(true);
        request.pause();
        request.endHandler(new Handler<Void>() {
            @Override
            public void handle(Void v) {
                ssoRedirect(request.formAttributes().get("SAMLRequest"), request);
            }
        });
        request.resume();

    }

    @Get("/saml/metadata/:idp")
    public void idpGar(HttpServerRequest request) {
        JsonObject idpConfig = config.getJsonObject("idp-metadata-mapping", new JsonObject());
        String idpParam = request.getParam("idp");
        if (!idpConfig.isEmpty() && idpConfig.containsKey(idpParam)) {
            request.response().sendFile(idpConfig.getString(idpParam));
        } else {
            request.response().setStatusCode(404).setStatusMessage("idp not found").end();
        }
    }

    /**
     * Generate SAML response from saml Issuer
     * @param xmlStrBase64 (issuer name inside)
     * @param request
     */
    private void ssoRedirect(String xmlStrBase64, HttpServerRequest request) {
        String xmlStr = "";
        try {
            xmlStr = new String(Base64.getDecoder().decode(xmlStrBase64));
        } catch (Exception e) {
            log.error("ssoRedirect decode base64 xml FAILED ", e);
            redirect(request, "");
        }
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder;
        try {
            builder = factory.newDocumentBuilder();
            Document doc = builder.parse(new InputSource(new StringReader(xmlStr)));
            XPathFactory xpf = XPathFactory.newInstance();
            XPath path = xpf.newXPath();
            String expression = "/AuthnRequest/Issuer";
            final String serviceProviderId = (String) path.evaluate(expression, doc.getDocumentElement());
            UserUtils.getUserInfos(eb, request, new Handler<UserInfos>() {
                @Override
                public void handle(final UserInfos user) {
                    ssoGenerateSAML(user, serviceProviderId, request);
                }
            });
        } catch (Exception e) {

            log.info("Provider not available");
            log.info("XML Provider ------------------------------");
            log.info(xmlStr);
            log.info("END XML Provider ------------------------------");
            redirect(request, "");
            log.error("Can't generate SAML response from provider ", e);
        }
    }

    /**
     * Generate HTML auto-submit FORM with samlResponse and render the page
     * @param samlResponse64 base64 SAMLResponse
     * @param destination the recipient (SP acs)
     */
    private void renderSamlResponse(UserInfos user, String samlResponse64, String providerId, String destination,
            HttpServerRequest request) {
        JsonObject paramsFED = new JsonObject();
        paramsFED.put("SAMLResponse", samlResponse64);
        JsonObject relayStateMap = config.getJsonObject("relay-state");
        if (relayStateMap != null) {
            String relayState = relayStateMap.getString(providerId);
            if (relayState != null) {
                paramsFED.put("RelayState", relayState);
            } else {
                log.error("Error loading relay-state for providerId : " + providerId);
            }
        } else {
            log.error("Error loading relay-state properties.");
        }
        paramsFED.put("Destination", destination);
        renderView(request, paramsFED, "fed.html", null);
    }

    private void loginResult(final HttpServerRequest request, String error) {
        if (federatedAuthenticateError) {
            final JsonObject context = new JsonObject();
            if (error != null && !error.trim().isEmpty()) {
                context.put("error", new JsonObject().put("message",
                        I18n.getInstance().translate(error, getHost(request), I18n.acceptLanguage(request))));
            }
            context.put("notLoggedIn", true);
            renderView(request, context, "login.html", null);
        } else {
            redirect(request, LOGIN_PAGE);
        }
    }

    @Post("/saml/acs")
    public void acs(final HttpServerRequest request) {
        validateResponseAndGetAssertion(request, new Handler<Assertion>() {
            @Override
            public void handle(final Assertion assertion) {
                SamlServiceProvider sp = spFactory.serviceProvider(assertion);
                sp.execute(assertion, new Handler<Either<String, Object>>() {
                    @Override
                    public void handle(final Either<String, Object> event) {
                        if (event.isLeft()) {
                            loginResult(request, "fed.auth.error.user.not.found");
                        } else {
                            final String nameIdFromAssertion = getNameId(assertion);
                            final String sessionIndex = getSessionId(assertion);
                            if (log.isDebugEnabled()) {
                                log.debug("NameID : " + nameIdFromAssertion);
                                log.debug("SessionIndex : " + sessionIndex);
                            }
                            if (nameIdFromAssertion == null || sessionIndex == null
                                    || nameIdFromAssertion.trim().isEmpty() || sessionIndex.trim().isEmpty()) {
                                redirect(request, LOGIN_PAGE);
                                return;
                            }

                            // if user is already authenticated in the ENT through the ENT login page, we do not authenticate him again
                            // because this will store the "nameid"

                            // ALGORITHM RULE :
                            // if user has "nameId" : it means user connected first with a federated idp
                            // else he connected to the ENT through the ENT login page
                            // this way we know if we need to disonnect/redirect the user to the federated login/home page OR
                            // if we only disconnect him to the ENT (no nameid)
                            final String sessionId = CookieHelper.getInstance().getSigned("oneSessionId", request);

                            //                     final JsonObject query = new JsonObject().put("_id", sessionId);
                            //                     mongo.findOne(SESSIONS_COLLECTION, query, new io.vertx.core.Handler<Message<JsonObject>>() {
                            federationService.getMongoDbSession(sessionId,
                                    new io.vertx.core.Handler<Message<JsonObject>>() {
                                        @Override
                                        public void handle(Message<JsonObject> eventMongo) {
                                            JsonObject res = eventMongo.body().getJsonObject("result");
                                            String userId;
                                            if ("ok".equals(eventMongo.body().getString("status")) && res != null
                                                    && (userId = res.getString("userId")) != null
                                                    && !userId.trim().isEmpty()) {

                                                String nameID = res.getString("NameID");

                                                String userIdAssertion = null;
                                                if (event.right().getValue() != null
                                                        && event.right().getValue() instanceof JsonObject) {
                                                    userIdAssertion = ((JsonObject) event.right().getValue())
                                                            .getString("id");
                                                }

                                                // no NameID and same userId : user already connected through IDP ENT
                                                if ((nameID == null || nameID.trim().isEmpty())
                                                        && userIdAssertion != null
                                                        && userIdAssertion.equals(userId)) {
                                                    redirect(request, "/");
                                                } else {
                                                    endAcs(request, event, sessionIndex, nameIdFromAssertion);
                                                }
                                            } else {
                                                endAcs(request, event, sessionIndex, nameIdFromAssertion);
                                            }
                                        }
                                    });
                        }
                    }
                });
            }
        });
    }

    /**
     * End of acs method : authenticate user if not already connectd through IDP ENT
     *
     * @param request request from "/acs" method
     * @param event event from SamlServiceProvider.execute(...)
     * @param sessionIndex sessionIndex get from acs assertion
     * @param nameId nameID get from acs assertion
     */
    private void endAcs(final HttpServerRequest request, Either<String, Object> event, final String sessionIndex,
            final String nameId) {
        if (event.right().getValue() != null && event.right().getValue() instanceof JsonObject) {
            final JsonObject res = (JsonObject) event.right().getValue();
            if (res.size() == 0) {
                loginResult(request, "fed.auth.error.user.not.found");
            } else {
                authenticate(res, sessionIndex, nameId, request);
            }
        } else if (event.right().getValue() != null && event.right().getValue() instanceof JsonArray
                && isNotEmpty(signKey)) {
            try {
                JsonObject params = getUsersWithSignatures((JsonArray) event.right().getValue(), sessionIndex,
                        nameId);
                renderView(request, params, "selectFederatedUser.html", null);
            } catch (NoSuchAlgorithmException | InvalidKeyException | UnsupportedEncodingException e) {
                log.error("Error signing federated users.", e);
                redirect(request, LOGIN_PAGE);
            }
        } else {
            redirect(request, LOGIN_PAGE);
        }
    }

    protected String getNameId(Assertion assertion) {
        try {
            return (assertion != null && assertion.getSubject() != null
                    && assertion.getSubject().getNameID() != null)
                            ? SamlUtils.marshallNameId(assertion.getSubject().getNameID())
                            : null;
        } catch (MarshallingException e) {
            log.error("Error marshalling NameId.", e);
            return null;
        }
    }

    @Get("/saml/slo")
    public void slo(final HttpServerRequest request) {
        sloUser(request);
    }

    @Override
    protected void afterDropSession(JsonObject event, final HttpServerRequest request, UserInfos user,
            final String c) {
        request.headers().remove("Cookie");
        event.put("action", "generate-slo-request");
        event.put("IDP", (String) user.getOtherProperties().get("federatedIDP"));
        if (log.isDebugEnabled()) {
            log.debug("Session metadata : " + event.encodePrettily());
        }

        String nameID = event.getString("NameID");
        if (nameID != null && !nameID.isEmpty()) {
            if (softSlo) {
                Matcher academyMatcher = NAME_QUALIFIER_PATTERN.matcher(nameID);
                if (academyMatcher.find()) {
                    String nameQualifier = academyMatcher.group(1);
                    JsonObject confSoftSlo = config.getJsonObject("soft-slo-redirect");
                    if (confSoftSlo != null) {
                        String redirectIDP = confSoftSlo.getString(nameQualifier);
                        if (redirectIDP != null) {
                            redirect(request, redirectIDP, "");
                        } else {
                            log.error("Error loading soft-slo-redirect for IDP : " + nameQualifier);
                            redirect(request, LOGIN_PAGE);
                        }
                    } else {
                        log.error("Error loading soft-slo-redirect properties.");
                        redirect(request, LOGIN_PAGE);
                    }
                }
            } else {
                // normal slo
                vertx.eventBus().send("saml", event, handlerToAsyncHandler(new Handler<Message<JsonObject>>() {
                    @Override
                    public void handle(Message<JsonObject> event) {
                        if (log.isDebugEnabled()) {
                            log.debug("slo request : " + event.body().encodePrettily());
                        }
                        String slo = event.body().getString("slo");
                        try {
                            if (c != null && !c.isEmpty()) {
                                slo = c + URLEncoder.encode(slo, "UTF-8");
                            } else {
                                slo = URLEncoder.encode(slo, "UTF-8");
                            }
                        } catch (UnsupportedEncodingException e) {
                            log.error(e.getMessage(), e);
                        }
                        AuthController.logoutCallback(request, slo, config, eb);
                    }
                }));
            }
        } else {
            AuthController.logoutCallback(request, null, config, eb);
        }
    }

    @Post("/saml/selectUser")
    public void selectUser(final HttpServerRequest request) {
        request.setExpectMultipart(true);
        request.endHandler(new Handler<Void>() {
            @Override
            public void handle(Void v) {
                final JsonObject j = new JsonObject();
                for (String attr : request.formAttributes().names()) {
                    if (isNotEmpty(request.formAttributes().get(attr))) {
                        j.put(attr, request.formAttributes().get(attr));
                    }
                }
                final String nameId = j.getString("nameId", "").replaceAll("\\r", "");
                final String sessionIndex = j.getString("sessionIndex");
                try {
                    if (j.getString("key", "").equals(HmacSha1
                            .sign(sessionIndex + nameId + j.getString("login") + j.getString("id"), signKey))) {
                        authenticate(j, sessionIndex, nameId, request);
                    } else {
                        log.error("Invalid signature for federated user.");
                        redirect(request, LOGIN_PAGE);
                    }
                } catch (NoSuchAlgorithmException | InvalidKeyException | UnsupportedEncodingException e) {
                    log.error("Error validating signature of federated user.", e);
                    redirect(request, LOGIN_PAGE);
                }
            }
        });
    }

    private JsonObject getUsersWithSignatures(JsonArray array, String sessionIndex, String nameId)
            throws NoSuchAlgorithmException, InvalidKeyException, UnsupportedEncodingException {
        for (Object o : array) {
            if (!(o instanceof JsonObject))
                continue;
            JsonObject j = (JsonObject) o;
            j.put("key", HmacSha1.sign(sessionIndex + nameId + j.getString("login") + j.getString("id"), signKey));
            j.put("nameId", nameId);
            j.put("sessionIndex", sessionIndex);
        }
        return new JsonObject().put("users", array);
    }

    private String getSessionId(Assertion assertion) {
        if (assertion != null && assertion.getAuthnStatements() != null) {
            for (AuthnStatement ans : assertion.getAuthnStatements()) {
                if (ans.getSessionIndex() != null && !ans.getSessionIndex().trim().isEmpty()) {
                    return ans.getSessionIndex();
                }
            }
        }
        return null;
    }

    private void validateResponseAndGetAssertion(final HttpServerRequest request,
            final Handler<Assertion> handler) {
        getSamlResponse(request, new Handler<String>() {
            @Override
            public void handle(final String samlResponse) {
                if (samlResponse != null && samlResponse.contains("EncryptedAssertion")) {
                    JsonObject j = new JsonObject().put("action", "validate-signature-decrypt").put("response",
                            samlResponse);
                    vertx.eventBus().send("saml", j, handlerToAsyncHandler(new Handler<Message<JsonObject>>() {
                        @Override
                        public void handle(Message<JsonObject> event) {
                            String assertion = event.body().getString("assertion");
                            if ("ok".equals(event.body().getString("status"))
                                    && event.body().getBoolean("valid", false) && assertion != null) {
                                try {
                                    handler.handle(SamlUtils.unmarshallAssertion(assertion));
                                } catch (Exception e) {
                                    log.error(e.getMessage(), e);
                                    redirect(request, LOGIN_PAGE);
                                }
                            } else {
                                redirect(request, LOGIN_PAGE);
                            }
                        }
                    }));
                } else if (samlResponse != null) {
                    JsonObject j = new JsonObject().put("action", "validate-signature").put("response",
                            samlResponse);
                    vertx.eventBus().send("saml", j, handlerToAsyncHandler(new Handler<Message<JsonObject>>() {
                        @Override
                        public void handle(Message<JsonObject> event) {
                            if ("ok".equals(event.body().getString("status"))
                                    && event.body().getBoolean("valid", false)) {
                                try {
                                    Response response = SamlUtils.unmarshallResponse(samlResponse);
                                    if (response.getAssertions() == null || response.getAssertions().size() != 1) {
                                        redirect(request, LOGIN_PAGE);
                                        return;
                                    }
                                    handler.handle(response.getAssertions().get(0));
                                } catch (Exception e) {
                                    log.error(e.getMessage(), e);
                                    redirect(request, LOGIN_PAGE);
                                }
                            } else {
                                redirect(request, LOGIN_PAGE);
                            }
                        }
                    }));
                } else {
                    redirect(request, LOGIN_PAGE);
                }
            }
        });
    }

    private void getSamlResponse(final HttpServerRequest request, final Handler<String> handler) {
        if (log.isDebugEnabled()) {
            log.debug("getSamlResponse");
        }
        request.setExpectMultipart(true);
        request.endHandler(new Handler<Void>() {
            @Override
            public void handle(Void v) {
                if (samlWayfParams != null) {
                    final String state = CookieHelper.getInstance().getSigned("relaystate", request);
                    if (isEmpty(state) || (!state.equals(request.formAttributes().get("RelayState"))
                            && !state.equals(SamlUtils.SIMPLE_RS))) {
                        forbidden(request, "invalid_state");
                        return;
                    }
                }
                String samlResponse = request.formAttributes().get("SAMLResponse");
                log.debug("samlResponse=" + samlResponse);
                if (samlResponse != null && !samlResponse.trim().isEmpty()) {
                    handler.handle(new String(Base64.getDecoder().decode(samlResponse)));
                } else {
                    badRequest(request); // TODO replace by error page
                }
            }
        });
    }

    public void setServiceProviderFactory(SamlServiceProviderFactory spFactory) {
        this.spFactory = spFactory;
    }

    public void setSamlWayfParams(JsonObject samlWayfParams) {
        this.samlWayfParams = samlWayfParams;
    }

    public void setIgnoreCallBackPattern(String ignoreCallBackPattern) {
        this.ignoreCallBackPattern = ignoreCallBackPattern;
    }

    public void setFederationService(FederationService federationService) {
        this.federationService = federationService;
    }
}