View Javadoc

    1   /*
    2    * Copyright 2007 University Corporation for Advanced Internet Development, Inc.
    3    *
    4    * Licensed under the Apache License, Version 2.0 (the "License");
    5    * you may not use this file except in compliance with the License.
    6    * You may obtain a copy of the License at
    7    *
    8    * http://www.apache.org/licenses/LICENSE-2.0
    9    *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16
    17  package edu.internet2.middleware.shibboleth.idp.profile.saml2;
    18
    19  import java.io.IOException;
    20  import java.io.StringReader;
    21  import java.util.ArrayList;
    22
    23  import javax.servlet.ServletContext;
    24  import javax.servlet.http.HttpServletRequest;
    25  import javax.servlet.http.HttpServletResponse;
    26
    27  import org.joda.time.DateTime;
    28  import org.joda.time.DateTimeZone;
    29  import org.opensaml.Configuration;
    30  import org.opensaml.common.SAMLObjectBuilder;
    31  import org.opensaml.common.binding.decoding.SAMLMessageDecoder;
    32  import org.opensaml.common.xml.SAMLConstants;
    33  import org.opensaml.saml2.binding.AuthnResponseEndpointSelector;
    34  import org.opensaml.saml2.core.Assertion;
    35  import org.opensaml.saml2.core.AttributeStatement;
    36  import org.opensaml.saml2.core.AuthnContext;
    37  import org.opensaml.saml2.core.AuthnContextClassRef;
    38  import org.opensaml.saml2.core.AuthnContextDeclRef;
    39  import org.opensaml.saml2.core.AuthnRequest;
    40  import org.opensaml.saml2.core.AuthnStatement;
    41  import org.opensaml.saml2.core.NameID;
    42  import org.opensaml.saml2.core.NameIDPolicy;
    43  import org.opensaml.saml2.core.RequestedAuthnContext;
    44  import org.opensaml.saml2.core.Response;
    45  import org.opensaml.saml2.core.Statement;
    46  import org.opensaml.saml2.core.StatusCode;
    47  import org.opensaml.saml2.core.Subject;
    48  import org.opensaml.saml2.core.SubjectConfirmation;
    49  import org.opensaml.saml2.core.SubjectConfirmationData;
    50  import org.opensaml.saml2.core.SubjectLocality;
    51  import org.opensaml.saml2.metadata.AffiliateMember;
    52  import org.opensaml.saml2.metadata.AffiliationDescriptor;
    53  import org.opensaml.saml2.metadata.AssertionConsumerService;
    54  import org.opensaml.saml2.metadata.Endpoint;
    55  import org.opensaml.saml2.metadata.EntityDescriptor;
    56  import org.opensaml.saml2.metadata.IDPSSODescriptor;
    57  import org.opensaml.saml2.metadata.SPSSODescriptor;
    58  import org.opensaml.saml2.metadata.provider.MetadataProviderException;
    59  import org.opensaml.ws.message.decoder.MessageDecodingException;
    60  import org.opensaml.ws.transport.http.HTTPInTransport;
    61  import org.opensaml.ws.transport.http.HTTPOutTransport;
    62  import org.opensaml.ws.transport.http.HttpServletRequestAdapter;
    63  import org.opensaml.ws.transport.http.HttpServletResponseAdapter;
    64  import org.opensaml.xml.io.MarshallingException;
    65  import org.opensaml.xml.io.Unmarshaller;
    66  import org.opensaml.xml.io.UnmarshallingException;
    67  import org.opensaml.xml.security.SecurityException;
    68  import org.opensaml.xml.util.DatatypeHelper;
    69  import org.slf4j.Logger;
    70  import org.slf4j.LoggerFactory;
    71  import org.w3c.dom.Element;
    72
    73  import edu.internet2.middleware.shibboleth.common.profile.ProfileException;
    74  import edu.internet2.middleware.shibboleth.common.profile.provider.BaseSAMLProfileRequestContext;
    75  import edu.internet2.middleware.shibboleth.common.relyingparty.ProfileConfiguration;
    76  import edu.internet2.middleware.shibboleth.common.relyingparty.RelyingPartyConfiguration;
    77  import edu.internet2.middleware.shibboleth.common.relyingparty.provider.SAMLMDRelyingPartyConfigurationManager;
    78  import edu.internet2.middleware.shibboleth.common.relyingparty.provider.saml2.SSOConfiguration;
    79  import edu.internet2.middleware.shibboleth.common.util.HttpHelper;
    80  import edu.internet2.middleware.shibboleth.idp.authn.PassiveAuthenticationException;
    81  import edu.internet2.middleware.shibboleth.idp.authn.Saml2LoginContext;
    82  import edu.internet2.middleware.shibboleth.idp.authn.LoginContext;
    83  import edu.internet2.middleware.shibboleth.idp.session.Session;
    84  import edu.internet2.middleware.shibboleth.idp.util.HttpServletHelper;
    85
    86  /** SAML 2.0 SSO request profile handler. */
    87  public class SSOProfileHandler extends AbstractSAML2ProfileHandler {
    88
    89      /** Class logger. */
    90      private final Logger log = LoggerFactory.getLogger(SSOProfileHandler.class);
    91
    92      /** Builder of AuthnStatement objects. */
    93      private SAMLObjectBuilder<AuthnStatement> authnStatementBuilder;
    94
    95      /** Builder of AuthnContext objects. */
    96      private SAMLObjectBuilder<AuthnContext> authnContextBuilder;
    97
    98      /** Builder of AuthnContextClassRef objects. */
    99      private SAMLObjectBuilder<AuthnContextClassRef> authnContextClassRefBuilder;
    100
    101     /** Builder of AuthnContextDeclRef objects. */
    102     private SAMLObjectBuilder<AuthnContextDeclRef> authnContextDeclRefBuilder;
    103
    104     /** Builder of SubjectLocality objects. */
    105     private SAMLObjectBuilder<SubjectLocality> subjectLocalityBuilder;
    106
    107     /** Builder of Endpoint objects. */
    108     private SAMLObjectBuilder<Endpoint> endpointBuilder;
    109
    110     /** URL of the authentication manager Servlet. */
    111     private String authenticationManagerPath;
    112
    113     /**
    114      * Constructor.
    115      *
    116      * @param authnManagerPath path to the authentication manager Servlet
    117      */
    118     @SuppressWarnings("unchecked")
    119     public SSOProfileHandler(String authnManagerPath) {
    120         super();
    121
    122         if (DatatypeHelper.isEmpty(authnManagerPath)) {
    123             throw new IllegalArgumentException("Authentication manager path may not be null");
    124         }
    125         if (authnManagerPath.startsWith("/")) {
    126             authenticationManagerPath = authnManagerPath;
    127         } else {
    128             authenticationManagerPath = "/" + authnManagerPath;
    129         }
    130
    131         authnStatementBuilder = (SAMLObjectBuilder<AuthnStatement>) getBuilderFactory().getBuilder(
    132                 AuthnStatement.DEFAULT_ELEMENT_NAME);
    133         authnContextBuilder = (SAMLObjectBuilder<AuthnContext>) getBuilderFactory().getBuilder(
    134                 AuthnContext.DEFAULT_ELEMENT_NAME);
    135         authnContextClassRefBuilder = (SAMLObjectBuilder<AuthnContextClassRef>) getBuilderFactory().getBuilder(
    136                 AuthnContextClassRef.DEFAULT_ELEMENT_NAME);
    137         authnContextDeclRefBuilder = (SAMLObjectBuilder<AuthnContextDeclRef>) getBuilderFactory().getBuilder(
    138                 AuthnContextDeclRef.DEFAULT_ELEMENT_NAME);
    139         subjectLocalityBuilder = (SAMLObjectBuilder<SubjectLocality>) getBuilderFactory().getBuilder(
    140                 SubjectLocality.DEFAULT_ELEMENT_NAME);
    141         endpointBuilder = (SAMLObjectBuilder<Endpoint>) getBuilderFactory().getBuilder(
    142                 AssertionConsumerService.DEFAULT_ELEMENT_NAME);
    143     }
    144
    145     /** {@inheritDoc} */
    146     public String getProfileId() {
    147         return SSOConfiguration.PROFILE_ID;
    148     }
    149
    150     /** {@inheritDoc} */
    151     public void processRequest(HTTPInTransport inTransport, HTTPOutTransport outTransport) throws ProfileException {
    152         HttpServletRequest httpRequest = ((HttpServletRequestAdapter) inTransport).getWrappedRequest();
    153         HttpServletResponse httpResponse = ((HttpServletResponseAdapter) outTransport).getWrappedResponse();
    154         ServletContext servletContext = httpRequest.getSession().getServletContext();
    155
    156         LoginContext loginContext = HttpServletHelper.getLoginContext(getStorageService(),
    157                 servletContext, httpRequest);
    158         if (loginContext == null || !(loginContext instanceof Saml2LoginContext)) {
    159             log.debug("Incoming request does not contain a login context, processing as first leg of request");
    160             performAuthentication(inTransport, outTransport);
    161         } else if (loginContext.isPrincipalAuthenticated() || loginContext.getAuthenticationFailure() != null) {
    162             log.debug("Incoming request contains a login context, processing as second leg of request");
    163             HttpServletHelper.unbindLoginContext(getStorageService(), servletContext, httpRequest, httpResponse);
    164             completeAuthenticationRequest((Saml2LoginContext)loginContext, inTransport, outTransport);
    165         } else {
    166             log.debug("Incoming request contained a login context but principal was not authenticated, processing as first leg of request");
    167             performAuthentication(inTransport, outTransport);
    168         }
    169     }
    170
    171     /**
    172      * Creates a {@link Saml2LoginContext} an sends the request off to the AuthenticationManager to begin the process of
    173      * authenticating the user.
    174      *
    175      * @param inTransport inbound request transport
    176      * @param outTransport outbound response transport
    177      *
    178      * @throws ProfileException thrown if there is a problem creating the login context and transferring control to the
    179      *             authentication manager
    180      */
    181     protected void performAuthentication(HTTPInTransport inTransport, HTTPOutTransport outTransport)
    182             throws ProfileException {
    183         HttpServletRequest httpRequest = ((HttpServletRequestAdapter) inTransport).getWrappedRequest();
    184         HttpServletResponse httpResponse = ((HttpServletResponseAdapter) outTransport).getWrappedResponse();
    185
    186         SSORequestContext requestContext = new SSORequestContext();
    187
    188         try {
    189             decodeRequest(requestContext, inTransport, outTransport);
    190
    191             String relyingPartyId = requestContext.getInboundMessageIssuer();
    192             requestContext.setPeerEntityId(relyingPartyId);
    193             RelyingPartyConfiguration rpConfig = getRelyingPartyConfiguration(relyingPartyId);
    194             ProfileConfiguration ssoConfig = rpConfig.getProfileConfiguration(getProfileId());
    195             if (ssoConfig == null) {
    196                 String msg = "SAML 2 SSO profile is not configured for relying party "
    197                         + requestContext.getInboundMessageIssuer();
    198                 log.warn(msg);
    199                 throw new ProfileException(msg);
    200             }
    201
    202             log.debug("Creating login context and transferring control to authentication engine");
    203             Saml2LoginContext loginContext = new Saml2LoginContext(relyingPartyId, requestContext.getRelayState(),
    204                     requestContext.getInboundSAMLMessage());
    205             loginContext.setUnsolicited(requestContext.isUnsolicited());
    206             loginContext.setAuthenticationEngineURL(authenticationManagerPath);
    207             loginContext.setProfileHandlerURL(HttpHelper.getRequestUriWithoutContext(httpRequest));
    208             loginContext.setDefaultAuthenticationMethod(rpConfig.getDefaultAuthenticationMethod());
    209
    210             HttpServletHelper.bindLoginContext(loginContext, getStorageService(), httpRequest.getSession()
    211                     .getServletContext(), httpRequest, httpResponse);
    212
    213             String authnEngineUrl = HttpServletHelper.getContextRelativeUrl(httpRequest, authenticationManagerPath)
    214                     .buildURL();
    215             log.debug("Redirecting user to authentication engine at {}", authnEngineUrl);
    216             httpResponse.sendRedirect(authnEngineUrl);
    217         } catch (MarshallingException e) {
    218             log.error("Unable to marshall authentication request context");
    219             throw new ProfileException("Unable to marshall authentication request context", e);
    220         } catch (IOException ex) {
    221             log.error("Error forwarding SAML 2 AuthnRequest to AuthenticationManager", ex);
    222             throw new ProfileException("Error forwarding SAML 2 AuthnRequest to AuthenticationManager", ex);
    223         }
    224     }
    225
    226     /**
    227      * Creates a response to the {@link AuthnRequest} and sends the user, with response in tow, back to the relying
    228      * party after they've been authenticated.
    229      *
    230      * @param loginContext login context for this request
    231      * @param inTransport inbound message transport
    232      * @param outTransport outbound message transport
    233      *
    234      * @throws ProfileException thrown if the response can not be created and sent back to the relying party
    235      */
    236     protected void completeAuthenticationRequest(Saml2LoginContext loginContext, HTTPInTransport inTransport,
    237             HTTPOutTransport outTransport) throws ProfileException {
    238         SSORequestContext requestContext = buildRequestContext(loginContext, inTransport, outTransport);
    239
    240         Response samlResponse;
    241         try {
    242             checkSamlVersion(requestContext);
    243             checkNameIDPolicy(requestContext);
    244
    245             if (loginContext.getAuthenticationFailure() != null) {
    246                 if (loginContext.getAuthenticationFailure() instanceof PassiveAuthenticationException) {
    247                     requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, StatusCode.NO_PASSIVE_URI,
    248                             null));
    249                 } else {
    250                     requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, StatusCode.AUTHN_FAILED_URI,
    251                             null));
    252                 }
    253                 throw new ProfileException("Authentication failure", loginContext.getAuthenticationFailure());
    254             }
    255
    256             if (requestContext.getSubjectNameIdentifier() != null) {
    257                 log.debug("Authentication request contained a subject with a name identifier, resolving principal from NameID");
    258                 resolvePrincipal(requestContext);
    259                 String requestedPrincipalName = requestContext.getPrincipalName();
    260                 if (!DatatypeHelper.safeEquals(loginContext.getPrincipalName(), requestedPrincipalName)) {
    261                     log.warn(
    262                             "Authentication request identified principal {} but authentication mechanism identified principal {}",
    263                             requestedPrincipalName, loginContext.getPrincipalName());
    264                     requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, StatusCode.AUTHN_FAILED_URI,
    265                             null));
    266                     throw new ProfileException("User failed authentication");
    267                 }
    268             }
    269
    270             resolveAttributes(requestContext);
    271
    272             ArrayList<Statement> statements = new ArrayList<Statement>();
    273             statements.add(buildAuthnStatement(requestContext));
    274             if (requestContext.getProfileConfiguration().includeAttributeStatement()) {
    275                 AttributeStatement attributeStatement = buildAttributeStatement(requestContext);
    276                 if (attributeStatement != null) {
    277                     requestContext.setReleasedAttributes(requestContext.getAttributes().keySet());
    278                     statements.add(attributeStatement);
    279                 }
    280             }
    281
    282             samlResponse = buildResponse(requestContext, "urn:oasis:names:tc:SAML:2.0:cm:bearer", statements);
    283         } catch (ProfileException e) {
    284             if (requestContext.isUnsolicited()) {
    285                 // Just delegate to the IdP's global error handler
    286                 log.warn("Unsolicited response generation failed: {}", e.getMessage());
    287                 throw e;
    288             }
    289             samlResponse = buildErrorResponse(requestContext);
    290         }
    291
    292         requestContext.setOutboundSAMLMessage(samlResponse);
    293         requestContext.setOutboundSAMLMessageId(samlResponse.getID());
    294         requestContext.setOutboundSAMLMessageIssueInstant(samlResponse.getIssueInstant());
    295         encodeResponse(requestContext);
    296         writeAuditLogEntry(requestContext);
    297     }
    298
    299     /**
    300      * Decodes an incoming request and stores the information in a created request context.
    301      *
    302      * @param inTransport inbound transport
    303      * @param outTransport outbound transport
    304      * @param requestContext request context to which decoded information should be added
    305      *
    306      * @throws ProfileException thrown if the incoming message failed decoding
    307      */
    308     protected void decodeRequest(SSORequestContext requestContext, HTTPInTransport inTransport,
    309             HTTPOutTransport outTransport) throws ProfileException {
    310         if (log.isDebugEnabled()) {
    311             log.debug("Decoding message with decoder binding '{}'", getInboundMessageDecoder(requestContext)
    312                     .getBindingURI());
    313         }
    314
    315         requestContext.setCommunicationProfileId(getProfileId());
    316
    317         requestContext.setMetadataProvider(getMetadataProvider());
    318         requestContext.setSecurityPolicyResolver(getSecurityPolicyResolver());
    319
    320         requestContext.setCommunicationProfileId(getProfileId());
    321         requestContext.setInboundMessageTransport(inTransport);
    322         requestContext.setInboundSAMLProtocol(SAMLConstants.SAML20P_NS);
    323         requestContext.setPeerEntityRole(SPSSODescriptor.DEFAULT_ELEMENT_NAME);
    324
    325         requestContext.setOutboundMessageTransport(outTransport);
    326         requestContext.setOutboundSAMLProtocol(SAMLConstants.SAML20P_NS);
    327
    328         try {
    329             SAMLMessageDecoder decoder = getInboundMessageDecoder(requestContext);
    330             requestContext.setMessageDecoder(decoder);
    331             decoder.decode(requestContext);
    332             log.debug("Decoded request from relying party '{}'", requestContext.getInboundMessageIssuer());
    333
    334             if (!(requestContext.getInboundSAMLMessage() instanceof AuthnRequest)) {
    335                 log.warn("Incomming message was not a AuthnRequest, it was a '{}'", requestContext
    336                         .getInboundSAMLMessage().getClass().getName());
    337                 requestContext.setFailureStatus(buildStatus(StatusCode.REQUESTER_URI, null,
    338                         "Invalid SAML AuthnRequest message."));
    339                 throw new ProfileException("Invalid SAML AuthnRequest message.");
    340             }
    341         } catch (MessageDecodingException e) {
    342             String msg = "Error decoding authentication request message";
    343             log.warn(msg, e);
    344             throw new ProfileException(msg, e);
    345         } catch (SecurityException e) {
    346             String msg = "Message did not meet security requirements";
    347             log.warn(msg, e);
    348             throw new ProfileException(msg, e);
    349         }
    350     }
    351
    352     /**
    353      * Checks to see, if present, if the affiliation associated with the SPNameQualifier given in the AuthnRequest
    354      * NameIDPolicy lists the inbound message issuer as a member.
    355      *
    356      * @param requestContext current request context
    357      *
    358      * @throws ProfileException thrown if there the request is not a member of the affiliation or if there was a problem
    359      *             determining membership
    360      */
    361     protected void checkNameIDPolicy(SSORequestContext requestContext) throws ProfileException {
    362         AuthnRequest request = requestContext.getInboundSAMLMessage();
    363
    364         NameIDPolicy nameIdPolcy = request.getNameIDPolicy();
    365         if (nameIdPolcy == null) {
    366             return;
    367         }
    368
    369         String spNameQualifier = DatatypeHelper.safeTrimOrNullString(nameIdPolcy.getSPNameQualifier());
    370         if (spNameQualifier == null) {
    371             return;
    372         }
    373
    374         if (DatatypeHelper.safeEquals(spNameQualifier, requestContext.getInboundMessageIssuer())) {
    375             log.debug("SPNameQualifier '{}' matches message issuer.", spNameQualifier);
    376             return;
    377         }
    378
    379         log.debug("Checking if message issuer is a member of affiliation '{}'", spNameQualifier);
    380         try {
    381             EntityDescriptor affiliation = getMetadataProvider().getEntityDescriptor(spNameQualifier);
    382             if (affiliation != null) {
    383                 AffiliationDescriptor affiliationDescriptor = affiliation.getAffiliationDescriptor();
    384                 if (affiliationDescriptor != null && affiliationDescriptor.getMembers() != null) {
    385                     for (AffiliateMember member : affiliationDescriptor.getMembers()) {
    386                         if (DatatypeHelper.safeEquals(member.getID(), requestContext.getInboundMessageIssuer())) {
    387                             return;
    388                         }
    389                     }
    390                 }
    391             }
    392
    393             requestContext.setFailureStatus(buildStatus(StatusCode.REQUESTER_URI, StatusCode.INVALID_NAMEID_POLICY_URI,
    394                     "Invalid SPNameQualifier for this request"));
    395             throw new ProfileException("Relying party '" + requestContext.getInboundMessageIssuer()
    396                     + "' is not a member of the affiliation " + spNameQualifier);
    397         } catch (MetadataProviderException e) {
    398             requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, null, "Internal service error"));
    399             log.error("Error looking up metadata for affiliation", e);
    400             throw new ProfileException("Relying party '" + requestContext.getInboundMessageIssuer()
    401                     + "' is not a member of the affiliation " + spNameQualifier);
    402         }
    403     }
    404
    405     /**
    406      * Creates an authentication request context from the current environmental information.
    407      *
    408      * @param loginContext current login context
    409      * @param in inbound transport
    410      * @param out outbount transport
    411      *
    412      * @return created authentication request context
    413      *
    414      * @throws ProfileException thrown if there is a problem creating the context
    415      */
    416     protected SSORequestContext buildRequestContext(Saml2LoginContext loginContext, HTTPInTransport in,
    417             HTTPOutTransport out) throws ProfileException {
    418         SSORequestContext requestContext = new SSORequestContext();
    419         requestContext.setCommunicationProfileId(getProfileId());
    420
    421         requestContext.setMessageDecoder(getInboundMessageDecoder(requestContext));
    422
    423         requestContext.setLoginContext(loginContext);
    424         requestContext.setUnsolicited(loginContext.isUnsolicited());
    425
    426         requestContext.setInboundMessageTransport(in);
    427         requestContext.setInboundSAMLProtocol(SAMLConstants.SAML20P_NS);
    428
    429         requestContext.setOutboundMessageTransport(out);
    430         requestContext.setOutboundSAMLProtocol(SAMLConstants.SAML20P_NS);
    431
    432         requestContext.setMetadataProvider(getMetadataProvider());
    433
    434         String relyingPartyId = loginContext.getRelyingPartyId();
    435         requestContext.setPeerEntityId(relyingPartyId);
    436         requestContext.setInboundMessageIssuer(relyingPartyId);
    437
    438         populateRequestContext(requestContext);
    439
    440         return requestContext;
    441     }
    442
    443     /** {@inheritDoc} */
    444     protected void populateRelyingPartyInformation(BaseSAMLProfileRequestContext requestContext)
    445             throws ProfileException {
    446         super.populateRelyingPartyInformation(requestContext);
    447
    448         EntityDescriptor relyingPartyMetadata = requestContext.getPeerEntityMetadata();
    449         if (relyingPartyMetadata != null) {
    450             requestContext.setPeerEntityRole(SPSSODescriptor.DEFAULT_ELEMENT_NAME);
    451             requestContext.setPeerEntityRoleMetadata(relyingPartyMetadata.getSPSSODescriptor(SAMLConstants.SAML20P_NS));
    452         }
    453     }
    454
    455     /** {@inheritDoc} */
    456     protected void populateAssertingPartyInformation(BaseSAMLProfileRequestContext requestContext)
    457             throws ProfileException {
    458         super.populateAssertingPartyInformation(requestContext);
    459
    460         EntityDescriptor localEntityDescriptor = requestContext.getLocalEntityMetadata();
    461         if (localEntityDescriptor != null) {
    462             requestContext.setLocalEntityRole(IDPSSODescriptor.DEFAULT_ELEMENT_NAME);
    463             requestContext.setLocalEntityRoleMetadata(localEntityDescriptor
    464                     .getIDPSSODescriptor(SAMLConstants.SAML20P_NS));
    465         }
    466     }
    467
    468     /**
    469      * Populates the request context with information from the inbound SAML message.
    470      *
    471      * This method requires the the following request context properties to be populated: login context
    472      *
    473      * This methods populates the following request context properties: inbound saml message, relay state, inbound saml
    474      * message ID, subject name identifier
    475      *
    476      * @param requestContext current request context
    477      *
    478      * @throws ProfileException thrown if the inbound SAML message or subject identifier is null
    479      */
    480     protected void populateSAMLMessageInformation(BaseSAMLProfileRequestContext requestContext) throws ProfileException {
    481         SSORequestContext ssoRequestContext = (SSORequestContext) requestContext;
    482         try {
    483             Saml2LoginContext loginContext = ssoRequestContext.getLoginContext();
    484             requestContext.setRelayState(loginContext.getRelayState());
    485
    486             AuthnRequest authnRequest = deserializeRequest(loginContext.getAuthenticationRequest());
    487             requestContext.setInboundMessage(authnRequest);
    488             requestContext.setInboundSAMLMessage(authnRequest);
    489             requestContext.setInboundSAMLMessageId(authnRequest.getID());
    490
    491             Subject authnSubject = authnRequest.getSubject();
    492             if (authnSubject != null) {
    493                 requestContext.setSubjectNameIdentifier(authnSubject.getNameID());
    494             }
    495         } catch (UnmarshallingException e) {
    496             log.error("Unable to unmarshall authentication request context");
    497             ssoRequestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, null,
    498                     "Error recovering request state"));
    499             throw new ProfileException("Error recovering request state", e);
    500         }
    501     }
    502
    503     /**
    504      * Creates an authentication statement for the current request.
    505      *
    506      * @param requestContext current request context
    507      *
    508      * @return constructed authentication statement
    509      */
    510     protected AuthnStatement buildAuthnStatement(SSORequestContext requestContext) {
    511         Saml2LoginContext loginContext = requestContext.getLoginContext();
    512
    513         AuthnContext authnContext = buildAuthnContext(requestContext);
    514
    515         AuthnStatement statement = authnStatementBuilder.buildObject();
    516         statement.setAuthnContext(authnContext);
    517         statement.setAuthnInstant(loginContext != null ? loginContext.getAuthenticationInstant() : null);
    518
    519         Session session = getUserSession(requestContext.getInboundMessageTransport());
    520         if (session != null) {
    521             statement.setSessionIndex(session.getSessionID());
    522         }
    523
    524         long maxSPSessionLifetime = requestContext.getProfileConfiguration().getMaximumSPSessionLifetime();
    525         if (maxSPSessionLifetime > 0) {
    526             DateTime lifetime = new DateTime(DateTimeZone.UTC).plus(maxSPSessionLifetime);
    527             log.debug("Explicitly setting SP session expiration time to '{}'", lifetime.toString());
    528             statement.setSessionNotOnOrAfter(lifetime);
    529         }
    530
    531         statement.setSubjectLocality(buildSubjectLocality(requestContext));
    532
    533         return statement;
    534     }
    535
    536     /**
    537      * Creates an {@link AuthnContext} for a successful authentication request.
    538      *
    539      * @param requestContext current request
    540      *
    541      * @return the built authn context
    542      */
    543     protected AuthnContext buildAuthnContext(SSORequestContext requestContext) {
    544         AuthnContext authnContext = authnContextBuilder.buildObject();
    545
    546         Saml2LoginContext loginContext = requestContext.getLoginContext();
    547         AuthnRequest authnRequest = requestContext.getInboundSAMLMessage();
    548         RequestedAuthnContext requestedAuthnContext = authnRequest.getRequestedAuthnContext();
    549         if (requestedAuthnContext != null) {
    550             if (requestedAuthnContext.getAuthnContextClassRefs() != null) {
    551                 for (AuthnContextClassRef classRef : requestedAuthnContext.getAuthnContextClassRefs()) {
    552                     if (DatatypeHelper.safeEquals(classRef.getAuthnContextClassRef(),
    553                             loginContext.getAuthenticationMethod())) {
    554                         AuthnContextClassRef ref = authnContextClassRefBuilder.buildObject();
    555                         ref.setAuthnContextClassRef(loginContext.getAuthenticationMethod());
    556                         authnContext.setAuthnContextClassRef(ref);
    557                     }
    558                 }
    559             } else if (requestedAuthnContext.getAuthnContextDeclRefs() != null) {
    560                 for (AuthnContextDeclRef declRef : requestedAuthnContext.getAuthnContextDeclRefs()) {
    561                     if (DatatypeHelper.safeEquals(declRef.getAuthnContextDeclRef(),
    562                             loginContext.getAuthenticationMethod())) {
    563                         AuthnContextDeclRef ref = authnContextDeclRefBuilder.buildObject();
    564                         ref.setAuthnContextDeclRef(loginContext.getAuthenticationMethod());
    565                         authnContext.setAuthnContextDeclRef(ref);
    566                     }
    567                 }
    568             }
    569         }
    570
    571         if (authnContext.getAuthnContextClassRef() == null || authnContext.getAuthnContextDeclRef() == null) {
    572             AuthnContextClassRef ref = authnContextClassRefBuilder.buildObject();
    573             ref.setAuthnContextClassRef(loginContext.getAuthenticationMethod());
    574             authnContext.setAuthnContextClassRef(ref);
    575         }
    576
    577         return authnContext;
    578     }
    579
    580     /**
    581      * Constructs the subject locality for the authentication statement.
    582      *
    583      * @param requestContext curent request context
    584      *
    585      * @return subject locality for the authentication statement
    586      */
    587     protected SubjectLocality buildSubjectLocality(SSORequestContext requestContext) {
    588         HTTPInTransport transport = (HTTPInTransport) requestContext.getInboundMessageTransport();
    589         SubjectLocality subjectLocality = subjectLocalityBuilder.buildObject();
    590         subjectLocality.setAddress(transport.getPeerAddress());
    591
    592         return subjectLocality;
    593     }
    594
    595     /** {@inheritDoc} */
    596     protected String getRequiredNameIDFormat(BaseSAMLProfileRequestContext requestContext) {
    597         String requiredNameFormat = null;
    598         AuthnRequest authnRequest = (AuthnRequest) requestContext.getInboundSAMLMessage();
    599         NameIDPolicy nameIdPolicy = authnRequest.getNameIDPolicy();
    600         if (nameIdPolicy != null) {
    601             requiredNameFormat = DatatypeHelper.safeTrimOrNullString(nameIdPolicy.getFormat());
    602             // Check for unspec'd or encryption formats, which aren't relevant for this section of code.
    603             if (requiredNameFormat != null
    604                     && (NameID.ENCRYPTED.equals(requiredNameFormat) || NameID.UNSPECIFIED.equals(requiredNameFormat))) {
    605                 requiredNameFormat = null;
    606             }
    607         }
    608
    609         return requiredNameFormat;
    610     }
    611
    612     /** {@inheritDoc} */
    613     protected NameID buildNameId(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext) throws ProfileException {
    614         NameID nameId = super.buildNameId(requestContext);
    615         if (nameId != null) {
    616             AuthnRequest authnRequest = (AuthnRequest) requestContext.getInboundSAMLMessage();
    617             NameIDPolicy nameIdPolicy = authnRequest.getNameIDPolicy();
    618             if (nameIdPolicy != null) {
    619                 String spNameQualifier = DatatypeHelper.safeTrimOrNullString(nameIdPolicy.getSPNameQualifier());
    620                 if (spNameQualifier != null) {
    621                     // Right now the resolver/encoder layer doesn't support forcing the SPNameQualifier
    622                     // to be set, but if it ever does, this should detect a mismatch with NameIDPolicy.
    623                     if (nameId.getSPNameQualifier() != null) {
    624                         if (!nameId.getSPNameQualifier().equals(spNameQualifier)) {
    625                             // Requester specified a different qualifier than we produced.
    626                             requestContext.setFailureStatus(buildStatus(StatusCode.REQUESTER_URI,
    627                                     StatusCode.INVALID_NAMEID_POLICY_URI,
    628                                     "Invalid SPNameQualifier for this request"));
    629                             throw new ProfileException("Requested SPNameQualifier '{" + spNameQualifier
    630                                     + "}' conflicts with generated value '{" + nameId.getSPNameQualifier() + "}'");
    631                         }
    632                     } else {
    633                         // Set to the requester's preference.
    634                         nameId.setSPNameQualifier(spNameQualifier);
    635                     }
    636                 } else {
    637                     nameId.setSPNameQualifier(requestContext.getInboundMessageIssuer());
    638                 }
    639             }
    640         }
    641
    642         return nameId;
    643     }
    644
    645     /**
    646      * Selects the appropriate endpoint for the relying party and stores it in the request context.
    647      *
    648      * @param requestContext current request context
    649      *
    650      * @return Endpoint selected from the information provided in the request context
    651      */
    652     protected Endpoint selectEndpoint(BaseSAMLProfileRequestContext requestContext) {
    653         AuthnRequest authnRequest = ((SSORequestContext) requestContext).getInboundSAMLMessage();
    654
    655         Endpoint endpoint = null;
    656         if (requestContext.getRelyingPartyConfiguration().getRelyingPartyId() == SAMLMDRelyingPartyConfigurationManager.ANONYMOUS_RP_NAME) {
    657             if (authnRequest.getAssertionConsumerServiceURL() != null) {
    658                 endpoint = endpointBuilder.buildObject();
    659                 endpoint.setLocation(authnRequest.getAssertionConsumerServiceURL());
    660                 if (authnRequest.getProtocolBinding() != null) {
    661                     endpoint.setBinding(authnRequest.getProtocolBinding());
    662                 } else {
    663                     endpoint.setBinding(getSupportedOutboundBindings().get(0));
    664                 }
    665                 log.warn(
    666                         "Generating endpoint for anonymous relying party self-identified as '{}', ACS url '{}' and binding '{}'",
    667                         new Object[] { requestContext.getInboundMessageIssuer(), endpoint.getLocation(),
    668                                 endpoint.getBinding(), });
    669             } else {
    670                 log.warn("Unable to generate endpoint for anonymous party.  No ACS URL provided.");
    671             }
    672         } else {
    673             AuthnResponseEndpointSelector endpointSelector = new AuthnResponseEndpointSelector();
    674             endpointSelector.setEndpointType(AssertionConsumerService.DEFAULT_ELEMENT_NAME);
    675             endpointSelector.setMetadataProvider(getMetadataProvider());
    676             endpointSelector.setEntityMetadata(requestContext.getPeerEntityMetadata());
    677             endpointSelector.setEntityRoleMetadata(requestContext.getPeerEntityRoleMetadata());
    678             endpointSelector.setSamlRequest(requestContext.getInboundSAMLMessage());
    679             endpointSelector.getSupportedIssuerBindings().addAll(getSupportedOutboundBindings());
    680             endpoint = endpointSelector.selectEndpoint();
    681         }
    682
    683         return endpoint;
    684     }
    685
    686     /**
    687      * Deserializes an authentication request from a string.
    688      *
    689      * @param request request to deserialize
    690      *
    691      * @return the request XMLObject
    692      *
    693      * @throws UnmarshallingException thrown if the request can no be deserialized and unmarshalled
    694      */
    695     protected AuthnRequest deserializeRequest(String request) throws UnmarshallingException {
    696         try {
    697             Element requestElem = getParserPool().parse(new StringReader(request)).getDocumentElement();
    698             Unmarshaller unmarshaller = Configuration.getUnmarshallerFactory().getUnmarshaller(requestElem);
    699             return (AuthnRequest) unmarshaller.unmarshall(requestElem);
    700         } catch (Exception e) {
    701             throw new UnmarshallingException("Unable to read serialized authentication request");
    702         }
    703     }
    704
    705     /** {@inheritDoc} */
    706     protected void postProcessAssertion(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext, Assertion assertion)
    707             throws ProfileException {
    708         SSORequestContext ctx = (SSORequestContext) requestContext;
    709         if (ctx.isUnsolicited()) {
    710             Subject subject = assertion.getSubject();
    711             if (subject != null) {
    712                 for (SubjectConfirmation sc : subject.getSubjectConfirmations()) {
    713                     if (sc != null) {
    714                         SubjectConfirmationData scd = sc.getSubjectConfirmationData();
    715                         if (scd != null) {
    716                             scd.setInResponseTo(null);
    717                         }
    718                     }
    719                 }
    720             }
    721         }
    722     }
    723
    724     /** {@inheritDoc} */
    725     protected void postProcessResponse(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext, Response samlResponse)
    726             throws ProfileException {
    727         SSORequestContext ctx = (SSORequestContext) requestContext;
    728         if (ctx.isUnsolicited()) {
    729             samlResponse.setInResponseTo(null);
    730         }
    731     }
    732
    733     /** Represents the internal state of a SAML 2.0 SSO Request while it's being processed by the IdP. */
    734     protected class SSORequestContext extends BaseSAML2ProfileRequestContext<AuthnRequest, Response, SSOConfiguration> {
    735
    736         /** Unsolicited SSO indicator. */
    737         private boolean unsolicited;
    738
    739         /** Current login context. */
    740         private Saml2LoginContext loginContext;
    741
    742         /**
    743          * Returns the unsolicited SSO indicator.
    744          *
    745          * @return the unsolicited SSO indicator
    746          */
    747         public boolean isUnsolicited() {
    748             return unsolicited;
    749         }
    750
    751         /**
    752          * Gets the current login context.
    753          *
    754          * @return current login context
    755          */
    756         public Saml2LoginContext getLoginContext() {
    757             return loginContext;
    758         }
    759
    760         /**
    761          * Sets the unsolicited SSO indicator.
    762          *
    763          * @param unsolicited unsolicited SSO indicator to set
    764          */
    765         public void setUnsolicited(boolean unsolicited) {
    766             this.unsolicited = unsolicited;
    767         }
    768
    769         /**
    770          * Sets the current login context.
    771          *
    772          * @param context current login context
    773          */
    774         public void setLoginContext(Saml2LoginContext context) {
    775             loginContext = context;
    776         }
    777
    778     }
    779 }