001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2014-2015 ForgeRock AS.
015 */
016package org.forgerock.openig.handler.saml;
017
018import static java.lang.String.format;
019import static org.forgerock.openig.heap.Keys.ENVIRONMENT_HEAP_KEY;
020
021import java.io.File;
022import java.io.IOException;
023import java.util.ArrayList;
024import java.util.Collections;
025import java.util.HashMap;
026import java.util.HashSet;
027import java.util.List;
028import java.util.Map;
029import java.util.Properties;
030
031import javax.servlet.ServletException;
032import javax.servlet.http.HttpServletRequest;
033import javax.servlet.http.HttpServletResponse;
034
035import org.forgerock.http.Handler;
036import org.forgerock.http.header.LocationHeader;
037import org.forgerock.http.protocol.Form;
038import org.forgerock.http.protocol.Request;
039import org.forgerock.http.protocol.Response;
040import org.forgerock.http.protocol.Status;
041import org.forgerock.http.session.Session;
042import org.forgerock.http.session.SessionContext;
043import org.forgerock.json.JsonValue;
044import org.forgerock.openig.config.Environment;
045import org.forgerock.openig.heap.GenericHeapObject;
046import org.forgerock.openig.heap.GenericHeaplet;
047import org.forgerock.openig.heap.HeapException;
048import org.forgerock.services.context.AttributesContext;
049import org.forgerock.services.context.Context;
050import org.forgerock.util.promise.NeverThrowsException;
051import org.forgerock.util.promise.Promise;
052import org.forgerock.util.promise.Promises;
053
054import com.sun.identity.common.ShutdownManager;
055import com.sun.identity.plugin.session.SessionException;
056import com.sun.identity.saml2.assertion.Assertion;
057import com.sun.identity.saml2.assertion.AuthnStatement;
058import com.sun.identity.saml2.assertion.Subject;
059import com.sun.identity.saml2.common.SAML2Constants;
060import com.sun.identity.saml2.common.SAML2Exception;
061import com.sun.identity.saml2.common.SAML2Utils;
062import com.sun.identity.saml2.jaxb.entityconfig.SPSSOConfigElement;
063import com.sun.identity.saml2.meta.SAML2MetaManager;
064import com.sun.identity.saml2.profile.CacheObject;
065import com.sun.identity.saml2.profile.LogoutUtil;
066import com.sun.identity.saml2.profile.SPACSUtils;
067import com.sun.identity.saml2.profile.SPCache;
068import com.sun.identity.saml2.profile.SPSSOFederate;
069import com.sun.identity.saml2.profile.SPSingleLogout;
070import com.sun.identity.saml2.servlet.SPSingleLogoutServiceSOAP;
071
072/**
073 * The SAML federation handler.
074 */
075public class SamlFederationHandler extends GenericHeapObject implements Handler {
076
077    /** Default Realm is always / in this case. */
078    private static final String DEFAULT_REALM = "/";
079
080    /**
081     * Marker for already-completed CHF Response (response filled through Servlet API).
082     */
083    private static final Response RESPONSE_ALREADY_COMPLETED = null;
084
085    /** The attribute mapping. */
086    private final Map<String, String> attributeMapping;
087
088    /** The value contained in the assertion subject is set as the value of the attribute subjectName in the session. */
089    private final String subjectMapping;
090
091    /** The delimiter to use when there are multiple contexts in the assertion. */
092    private final String authnContextDelimiter;
093
094    /** The name to use when placing context values into the session. */
095    private final String authnContext;
096
097    private final String sessionIndexMapping;
098
099    private final String redirectURI;
100
101    private final String logoutURI;
102
103    private final String assertionConsumerEndpoint;
104
105    private final String sPinitiatedSSOEndpoint;
106
107    private final String singleLogoutEndpoint;
108
109    /** IDP Single Logout SOAP Endpoint. */
110    private final String singleLogoutEndpointSoap;
111
112    /** SP Single Logout Endpoint. */
113    private final String sPinitiatedSLOEndpoint;
114
115    /**
116     * Constructs a federation handler according to the specified parameters.
117     *
118     * @param attributeMapping
119     *            The attribute mapping.
120     * @param subjectMapping
121     *            The value contained in the assertion subject is set as the value of the attribute subjectName in the
122     *            session.
123     * @param authnContextDelimiter
124     *            The delimiter to use when there are multiple contexts in the assertion.
125     * @param authnContext
126     *            The name to use when placing context values into the session.
127     * @param sessionIndexMapping
128     *            The IDP's sessionIndex for the user is sent in the assertion.
129     * @param redirectURI
130     *            The redirectURI should be set to the page the Form-Filter recognizes as the login page for the target
131     *            application.
132     * @param logoutURI
133     *            The logoutURI should be set to the URI which logs the user out of the target application.
134     * @param assertionConsumerEndpoint
135     *            The assertionMapping defines how to transform the attributes from the incoming assertion to attribute
136     *            value pairs in the session.
137     * @param sPinitiatedSSOEndpoint
138     *            The default value is SPInitiatedSSO.
139     * @param singleLogoutEndpoint
140     *            The default value of fedletSLO is the same as the Fedlet.
141     * @param singleLogoutEndpointSoap
142     *            IDP Single Logout SOAP Endpoint.
143     * @param sPinitiatedSLOEndpoint
144     *            SP Single Logout Endpoint.
145     */
146    SamlFederationHandler(Map<String, String> attributeMapping,
147                          String subjectMapping,
148                          String authnContextDelimiter,
149                          String authnContext,
150                          String sessionIndexMapping,
151                          String redirectURI,
152                          String logoutURI,
153                          String assertionConsumerEndpoint,
154                          String sPinitiatedSSOEndpoint,
155                          String singleLogoutEndpoint,
156                          String singleLogoutEndpointSoap,
157                          String sPinitiatedSLOEndpoint) {
158        this.attributeMapping = Collections.unmodifiableMap(attributeMapping);
159        this.subjectMapping = subjectMapping;
160        this.authnContextDelimiter = authnContextDelimiter;
161        this.authnContext = authnContext;
162        this.sessionIndexMapping = sessionIndexMapping;
163        this.redirectURI = redirectURI;
164        this.logoutURI = logoutURI;
165        this.assertionConsumerEndpoint = assertionConsumerEndpoint;
166        this.sPinitiatedSSOEndpoint = sPinitiatedSSOEndpoint;
167        this.singleLogoutEndpoint = singleLogoutEndpoint;
168        this.singleLogoutEndpointSoap = singleLogoutEndpointSoap;
169        this.sPinitiatedSLOEndpoint = sPinitiatedSLOEndpoint;
170    }
171
172    @Override
173    public Promise<Response, NeverThrowsException> handle(final Context context, final Request request) {
174        HttpServletRequest servletRequest = adaptRequest(context, request);
175        HttpServletResponse servletResponse = adaptResponse(context);
176
177        Session session = context.asContext(SessionContext.class).getSession();
178        try {
179            String path = request.getUri().getPath();
180            if (path.indexOf(assertionConsumerEndpoint) > 0) {
181                return complete(serviceAssertionConsumer(session, servletRequest, servletResponse));
182            } else if (path.indexOf(sPinitiatedSSOEndpoint) > 0) {
183                return complete(serviceSPInitiatedSSO(request, servletRequest, servletResponse));
184            } else if (path.indexOf(sPinitiatedSLOEndpoint) > 0) {
185                return complete(serviceSPInitiatedSLO(request, session, servletRequest, servletResponse));
186            } else if (path.indexOf(singleLogoutEndpointSoap) > 0) {
187                return complete(serviceIDPInitiatedSLOSOAP(servletRequest, servletResponse));
188            } else if (path.indexOf(singleLogoutEndpoint) > 0) {
189                return complete(serviceIDPInitiatedSLO(request, session, servletRequest, servletResponse));
190            } else {
191                logger.warning(format("FederationServlet warning: URI not in service %s", request.getUri()));
192                return complete(RESPONSE_ALREADY_COMPLETED);
193            }
194        } catch (IOException ioe) {
195            return complete(withError(servletResponse, ioe.getMessage()));
196        } catch (ServletException se) {
197            return complete(withError(servletResponse, se.getMessage()));
198        } catch (SAML2Exception sme) {
199            return complete(withError(servletResponse, sme.getMessage()));
200        } catch (SessionException se) {
201            return complete(withError(servletResponse, se.getMessage()));
202        }
203    }
204
205    private static Promise<Response, NeverThrowsException> complete(Response response) {
206        return Promises.newResultPromise(response);
207    }
208
209    /**
210     * Whether IDP or SP initiated, the final request ends up here.
211     * <p>
212     * The assertion is validated, attributes are retrieved from and set in the HttpSession where downstream filters
213     * can access them and pass them on to the target application.
214     */
215    private Response serviceAssertionConsumer(Session session,
216                                              HttpServletRequest request,
217                                              HttpServletResponse response) throws IOException,
218                                                                                   ServletException,
219                                                                                   SAML2Exception,
220                                                                                   SessionException {
221        Map<?, ?> map = SPACSUtils.processResponseForFedlet(request, response);
222        addAttributesToSession(session, map);
223        /*
224         * Redirect back to the original target application's login page and let the filters take over. If the relayURI
225         * is set in the assertion we must use that, otherwise we will use the configured value, which should be the
226         * login page for the target application.
227         */
228        String relayURI = (String) map.get(SAML2Constants.RELAY_STATE);
229        String uri = isRelayURIProvided(relayURI) ? relayURI : redirectURI;
230        return sendRedirect(uri);
231    }
232
233    private boolean isRelayURIProvided(String relayURI) {
234        return relayURI != null && !relayURI.isEmpty();
235    }
236
237    /**
238     * Store attribute value pairs in the session based on the assertionMapping found in config file.
239     * <p>
240     * The intent is to have a filter use one of these attributes as the subject and possibly the password. The
241     * presence of these attributes in the Session implies the assertion has been processed and validated.
242     *
243     * @param session
244     *            Request's {@link Session}
245     * @param assertion
246     *            SAML assertion content
247     */
248    private void addAttributesToSession(final Session session, Map<?, ?> assertion) {
249        Map<?, ?> attributeStatement = (Map<?, ?>) assertion.get(SAML2Constants.ATTRIBUTE_MAP);
250        if (attributeStatement != null) {
251            for (String key : attributeMapping.keySet()) {
252                HashSet<?> t = (HashSet<?>) attributeStatement.get(attributeMapping.get(key));
253                if (t != null) {
254                    String sessionValue = (String) t.iterator().next();
255                    session.put(key, sessionValue);
256                } else {
257                    logger.warning(format("FederationServlet: Warning no assertion attribute found for : %s",
258                                          attributeMapping.get(key)));
259                }
260            }
261        } else {
262            logger.warning("FederationServlet attribute statement was not present in assertion");
263        }
264        if (subjectMapping != null) {
265            String subjectValue = ((Subject) assertion.get(SAML2Constants.SUBJECT)).getNameID().getValue();
266            session.put(subjectMapping, subjectValue);
267            logger.debug(format("FederationServlet adding subject to session: %s = %s", subjectMapping,
268                                subjectValue));
269        }
270
271        if (sessionIndexMapping != null) {
272            String sessionIndexValue = (String) assertion.get(SAML2Constants.SESSION_INDEX);
273            session.put(sessionIndexMapping, sessionIndexValue);
274            logger.debug(format("FederationServlet adding session index: %s = %s", sessionIndexMapping,
275                                sessionIndexValue));
276        }
277
278        if (authnContext != null) {
279            @SuppressWarnings("unchecked")
280            List<AuthnStatement> authnStatements = ((Assertion) assertion.get(SAML2Constants.ASSERTION))
281                    .getAuthnStatements();
282            StringBuilder authnContextValues = new StringBuilder();
283            for (AuthnStatement authnStatement : authnStatements) {
284                String authnContextValue = authnStatement.getAuthnContext().getAuthnContextClassRef();
285                if (authnContextValue != null && !authnContextValue.isEmpty()) {
286                    authnContextValues.append(authnContextValue);
287                    authnContextValues.append(authnContextDelimiter);
288                }
289            }
290            if (authnContextValues.length() > 0) {
291                // remove the last delimiter as it is redundant
292                authnContextValues.deleteCharAt(authnContextValues.length() - 1);
293                session.put(authnContext, authnContextValues.toString());
294                logger.debug(format("FederationServlet adding authentication contexts to session: %s = %s",
295                                    authnContext, authnContextValues));
296            }
297        }
298    }
299
300    @SuppressWarnings("unchecked")
301    private static Response serviceSPInitiatedSSO(Request request,
302                                              HttpServletRequest servletRequest,
303                                              HttpServletResponse servletResponse) throws SAML2Exception {
304        Form form = request.getForm();
305        String metaAlias = form.getFirst(SAML2Constants.METAALIAS);
306        if (metaAlias == null || metaAlias.length() == 0) {
307            SAML2MetaManager manager = new SAML2MetaManager();
308            List<String> spMetaAliases =
309                    manager.getAllHostedServiceProviderMetaAliases(DEFAULT_REALM);
310            if (spMetaAliases != null && !spMetaAliases.isEmpty()) {
311                metaAlias = spMetaAliases.get(0);
312            }
313        }
314        String idpEntityID = form.getFirst(SAML2Constants.IDPENTITYID);
315        Map<String, List<?>> paramsMap = SAML2Utils.getParamsMap(servletRequest);
316        List<String> list = new ArrayList<>();
317        list.add(SAML2Constants.NAMEID_TRANSIENT_FORMAT);
318
319        // next line testing to see if we can change the name format
320        paramsMap.put(SAML2Constants.NAMEID_POLICY_FORMAT, list);
321
322        // TODO: add option to specify artifact
323        if (paramsMap.get(SAML2Constants.BINDING) == null) {
324            // use POST binding
325            list = new ArrayList<>();
326            list.add(SAML2Constants.HTTP_POST);
327            paramsMap.put(SAML2Constants.BINDING, list);
328        }
329        if (idpEntityID == null || idpEntityID.length() == 0) {
330            SAML2MetaManager manager = new SAML2MetaManager();
331            List<String> idpEntities = manager.getAllRemoteIdentityProviderEntities(DEFAULT_REALM);
332            if (idpEntities != null && !idpEntities.isEmpty()) {
333                idpEntityID = idpEntities.get(0);
334            }
335        }
336        if (metaAlias == null || idpEntityID == null) {
337            throw new SAML2Exception("No metadata for SP or IDP");
338        }
339        SPSSOFederate.initiateAuthnRequest(servletRequest, servletResponse, metaAlias, idpEntityID, paramsMap);
340        return RESPONSE_ALREADY_COMPLETED;
341    }
342
343    /**
344     * This implementation is based on the {@literal spSingleLogoutInit.jsp} from OpenAM.
345     * Expects to find the {@literal NameID} and {@literal SessionIndex} in the session, these are stored during the
346     * IDP login process.
347     * <p>
348     * Optional request parameters are:
349     * <ul>
350     *     <li>{@literal RelayState} - the target URL on successful Single Logout.</li>
351     *     <li>{@literal spEntityID} - SP entity ID. When it is missing, first SP from metadata is used.</li>
352     *     <li>{@literal idpEntityID} - IDP entity ID. When it is missing, first IDP from metadata is used.</li>
353     *     <li>{@literal binding} - binding used for this request and when not set it will use the default binding of
354     *     the IDP.</li>
355     * </ul>
356     */
357    @SuppressWarnings("unchecked")
358    private Response serviceSPInitiatedSLO(Request request, Session session,
359                                           HttpServletRequest servletRequest,
360                                           HttpServletResponse servletResponse)
361            throws IOException, SAML2Exception {
362        logger.debug("FederationServlet.serviceSPInitiatedSLO entering");
363
364        // Retrieve these values from the session, if they do not exist then the session has expired
365        // or this is being called before we have authenticated to the IDP
366        String nameID = (String) session.get(subjectMapping);
367        String sessionIndex = (String) session.get(sessionIndexMapping);
368        if (nameID == null || nameID.isEmpty() || sessionIndex == null || sessionIndex.isEmpty()) {
369            throw new SAML2Exception(SAML2Utils.bundle.getString("nullNameID"));
370        }
371
372        Form form = request.getForm();
373        SAML2MetaManager manager = new SAML2MetaManager();
374        String relayState = form.getFirst(SAML2Constants.RELAY_STATE);
375        String spEntityID = form.getFirst(SAML2Constants.SPENTITYID);
376        String binding = form.getFirst(SAML2Constants.BINDING);
377        String idpEntityID = form.getFirst(SAML2Constants.IDPENTITYID);
378
379        // If the idpEntityID has not been specified then read it from the IDP metadata.
380        if (idpEntityID == null || idpEntityID.isEmpty()) {
381            List<String> idpEntities = manager.getAllRemoteIdentityProviderEntities(DEFAULT_REALM);
382            if (idpEntities != null && !idpEntities.isEmpty()) {
383                // Just take the first one since only one is supported
384                idpEntityID = idpEntities.get(0);
385            }
386        }
387        logger.debug(format("FederationServlet.serviceSPInitiatedSLO idpEntityID: %s", idpEntityID));
388
389
390        String metaAlias = null;
391        // If the spEntityID has not been specified then read it from the SP metadata.
392        if (spEntityID == null || spEntityID.isEmpty()) {
393            List<String> spMetaAliases = manager.getAllHostedServiceProviderMetaAliases(DEFAULT_REALM);
394            if (spMetaAliases != null && !spMetaAliases.isEmpty()) {
395                // Just take the first one since only one is supported
396                metaAlias = spMetaAliases.get(0);
397                spEntityID = manager.getEntityByMetaAlias(metaAlias);
398            }
399        } else {
400            SPSSOConfigElement spConfig = manager.getSPSSOConfig(DEFAULT_REALM, spEntityID);
401            if (spConfig != null) {
402                metaAlias = spConfig.getMetaAlias();
403            }
404        }
405        logger.debug(format("FederationServlet.serviceSPInitiatedSLO metaAlias: %s", metaAlias));
406        logger.debug(format("FederationServlet.serviceSPInitiatedSLO spEntityID: %s", spEntityID));
407
408
409        if (metaAlias == null || idpEntityID == null) {
410            throw new SAML2Exception("No metadata for SP or IDP");
411        }
412
413        // If the binding has not been specified then look up the IDP's default binding.
414        if (binding == null) {
415            binding = LogoutUtil.getSLOBindingInfo(servletRequest, metaAlias, SAML2Constants.SP_ROLE, idpEntityID);
416        }
417
418        if (!SAML2Utils.isSPProfileBindingSupported(DEFAULT_REALM, spEntityID, SAML2Constants.SLO_SERVICE, binding)) {
419            logger.error(format("FederationServlet.serviceSPInitiatedSLO unsupported binding: %s", binding));
420            throw new SAML2Exception(SAML2Utils.bundle.getString("unsupportedBinding"));
421        }
422
423        logger.debug(format("FederationServlet.serviceSPInitiatedSLO binding: %s", binding));
424
425        HashMap<String, String> paramsMap = new HashMap<>(7);
426        paramsMap.put(SAML2Constants.INFO_KEY, spEntityID + "|" + idpEntityID + "|" + nameID);
427        paramsMap.put(SAML2Constants.SESSION_INDEX, sessionIndex);
428        paramsMap.put(SAML2Constants.METAALIAS, metaAlias);
429        paramsMap.put(SAML2Constants.IDPENTITYID, idpEntityID);
430        paramsMap.put(SAML2Constants.ROLE, SAML2Constants.SP_ROLE);
431        paramsMap.put(SAML2Constants.BINDING, binding);
432
433        // If the relayState has not been specified try to use the SP default otherwise set it to the logoutURI if set.
434        if (relayState == null || relayState.isEmpty()) {
435            relayState = SAML2Utils.getAttributeValueFromSSOConfig(DEFAULT_REALM, spEntityID, SAML2Constants.SP_ROLE,
436                                                                   SAML2Constants.DEFAULT_RELAY_STATE);
437            if (relayState == null || (relayState.isEmpty() && logoutURI != null && !logoutURI.isEmpty())) {
438                relayState = logoutURI;
439            }
440        }
441        if (relayState != null && !relayState.isEmpty()) {
442            paramsMap.put(SAML2Constants.RELAY_STATE, relayState);
443        }
444
445        logger.debug(format("FederationServlet.serviceSPInitiatedSLO relayState: %s", relayState));
446
447        SPSingleLogout.initiateLogoutRequest(servletRequest, servletResponse, binding, paramsMap);
448
449        if (SAML2Constants.SOAP.equalsIgnoreCase(binding)) {
450            return sendRedirect(relayState);
451        }
452
453        return RESPONSE_ALREADY_COMPLETED;
454    }
455
456    private Response serviceIDPInitiatedSLO(final Request request,
457                                            final Session session,
458                                            HttpServletRequest servletRequest,
459                                            HttpServletResponse servletResponse)
460            throws SAML2Exception, SessionException, IOException {
461        logger.debug("FederationServlet.serviceIDPInitiatedSLO entering");
462
463        String relayState = getLogoutRelayState(request);
464        logger.debug(format("FederationServlet.serviceIDPInitiatedSLO relayState : %s", relayState));
465
466        Form form = request.getForm();
467        // Check if this is a request as part of an IDP initiated SLO
468        String samlRequest = form.getFirst(SAML2Constants.SAML_REQUEST);
469        Response response = RESPONSE_ALREADY_COMPLETED;
470        if (samlRequest != null) {
471            logger.debug("FederationServlet.serviceIDPInitiatedSLO processing IDP request");
472            SPSingleLogout.processLogoutRequest(servletRequest, servletResponse, samlRequest, relayState);
473        } else {
474            // Otherwise it might be a response from the IDP as part of a SP initiated SLO
475            String samlResponse = form.getFirst(SAML2Constants.SAML_RESPONSE);
476            if (samlResponse != null) {
477                logger.debug("FederationServlet.serviceIDPInitiatedSLO processing IDP response");
478                SPSingleLogout.processLogoutResponse(servletRequest, servletResponse, samlResponse, relayState);
479                if (relayState != null) {
480                    response = sendRedirect(relayState);
481                }
482            }
483        }
484
485        cleanSession(session);
486        return response;
487    }
488
489    private Response serviceIDPInitiatedSLOSOAP(HttpServletRequest request, HttpServletResponse response)
490            throws IOException, ServletException {
491
492        logger.debug("FederationServlet.serviceIDPInitiatedSLOSOAP entering");
493
494        SPSingleLogoutServiceSOAP spSingleLogoutServiceSOAP = new SPSingleLogoutServiceSOAP();
495        spSingleLogoutServiceSOAP.doPost(request, response);
496
497        return RESPONSE_ALREADY_COMPLETED;
498    }
499
500    private String getLogoutRelayState(Request request) {
501
502        Form form = request.getForm();
503        String relayState = form.getFirst(SAML2Constants.RELAY_STATE);
504        if (relayState != null) {
505            // Check the SP cache for the actual relayState value as the relayState is
506            // often passed as an ID to the IDP as part of the SP initiated SLO. Based on
507            // code from spSingleLogoutPOST.jsp in OpenAM
508            CacheObject tmpRs = (CacheObject) SPCache.relayStateHash.remove(relayState);
509            if (tmpRs != null) {
510                relayState = (String) tmpRs.getObject();
511            }
512        }
513
514        if (relayState == null || (relayState.isEmpty() && logoutURI != null && !logoutURI.isEmpty())) {
515            relayState = logoutURI;
516        }
517
518        return relayState;
519    }
520
521    /** Clean the session at the end of the SLO process. */
522    private void cleanSession(final Session session) {
523        logger.debug("End of SLO - Processing to session cleanup");
524        session.remove(subjectMapping);
525        session.remove(sessionIndexMapping);
526        session.remove(authnContext);
527
528        if (attributeMapping != null) {
529            for (final String key : attributeMapping.keySet()) {
530                session.remove(key);
531            }
532        }
533    }
534
535    private Response withError(HttpServletResponse response, String message) {
536        final String msg = format("SSO Failed: %s", message);
537        logger.error(msg);
538        if (!response.isCommitted()) {
539            return sendError(Status.INTERNAL_SERVER_ERROR, msg);
540        }
541        return null;
542    }
543
544    private static Response sendError(Status status, String message) {
545        Response response = new Response();
546        response.setStatus(status);
547        response.getEntity().setString(message);
548        return response;
549    }
550
551    private static Response sendRedirect(String redirectUri) {
552        Response response = new Response();
553        // Redirect with a 302 (Found) status code
554        response.setStatus(Status.FOUND);
555        // Web container was rebasing location header against server URL
556        // Not useful if relayState is already (and always) an absolute URL
557        response.getHeaders().put(LocationHeader.NAME, redirectUri);
558        return response;
559    }
560
561    private static HttpServletResponse adaptResponse(Context context) {
562        AttributesContext attributesContext = context.asContext(AttributesContext.class);
563        return (HttpServletResponse) attributesContext.getAttributes().get(HttpServletResponse.class.getName());
564    }
565
566    private static HttpServletRequest adaptRequest(Context context, Request req) {
567        AttributesContext attributesContext = context.asContext(AttributesContext.class);
568        HttpServletRequest request = (HttpServletRequest) attributesContext.getAttributes()
569                                                                  .get(HttpServletRequest.class.getName());
570        return new RequestAdapter(request, req);
571    }
572
573    /**
574     * Reads the actual federation servlet from the JSON configuration file.
575     */
576    public static class Heaplet extends GenericHeaplet {
577        @Override
578        public Object create() throws HeapException {
579            final Map<String, String> attributeMapping = new HashMap<>();
580            JsonValue mappings = config.get("assertionMapping").expect(Map.class);
581            if (mappings != null) {
582                for (String key : mappings.keys()) {
583                    attributeMapping.put(key, mappings.get(key).asString());
584                }
585            }
586            final String authnContextDelimiter = config.get("authnContextDelimiter").defaultTo("|").asString();
587            final String authnContext = config.get("authnContext").asString();
588            final String redirectURI = config.get("redirectURI").asString();
589            final String logoutURI = config.get("logoutURI").asString();
590            // Give subjectMapping and sessionIndexMapping a default value as they are needed when doing SP initiated
591            // SLO
592            final String subjectMapping = config.get("subjectMapping").defaultTo("subjectMapping").asString();
593            final String sessionIndexMapping = config.get("sessionIndexMapping").defaultTo("sessionIndexMapping")
594                    .asString();
595            final String assertionConsumerEndpoint = config.get("assertionConsumerEndpoint")
596                    .defaultTo("fedletapplication").asString();
597            final String sPinitiatedSSOEndpoint = config.get("SPinitiatedSSOEndpoint").defaultTo("SPInitiatedSSO")
598                    .asString();
599            final String singleLogoutEndpoint = config.get("singleLogoutEndpoint").defaultTo("fedletSloRedirect")
600                    .asString();
601            final String singleLogoutEndpointSoap = config.get("singleLogoutEndpointSoap").defaultTo("fedletSloSoap")
602                    .asString();
603            final String sPinitiatedSLOEndpoint = config.get("SPinitiatedSLOEndpoint").defaultTo("SPInitiatedSLO")
604                    .asString();
605            /*
606             * Get the gateway configuration directory and set it as a system property to override the default openFed
607             * location. Federation config files will reside in the SAML directory.
608             */
609            Environment environment = heap.get(ENVIRONMENT_HEAP_KEY, Environment.class);
610            String samlDirectory = new File(environment.getBaseDirectory(), "SAML").getPath();
611            logger.info(format("FederationServlet init directory: %s", samlDirectory));
612            Properties p = System.getProperties();
613            p.setProperty("com.sun.identity.fedlet.home", samlDirectory);
614            System.setProperties(p);
615
616            return new SamlFederationHandler(attributeMapping,
617                                             subjectMapping,
618                                             authnContextDelimiter,
619                                             authnContext,
620                                             sessionIndexMapping,
621                                             redirectURI,
622                                             logoutURI,
623                                             assertionConsumerEndpoint,
624                                             sPinitiatedSSOEndpoint,
625                                             singleLogoutEndpoint,
626                                             singleLogoutEndpointSoap,
627                                             sPinitiatedSLOEndpoint);
628        }
629
630        @Override
631        public void destroy() {
632            // Automatically shutdown the fedlet
633            ShutdownManager manager = ShutdownManager.getInstance();
634            if (manager.acquireValidLock()) {
635                try {
636                    manager.shutdown();
637                } finally {
638                    manager.releaseLockAndNotify();
639                }
640            }
641            super.destroy();
642        }
643    }
644}