View Javadoc

    1   /*
    2    * Licensed to the University Corporation for Advanced Internet Development,
    3    * Inc. (UCAID) under one or more contributor license agreements.  See the
    4    * NOTICE file distributed with this work for additional information regarding
    5    * copyright ownership. The UCAID licenses this file to You under the Apache
    6    * License, Version 2.0 (the "License"); you may not use this file except in
    7    * compliance with the License.  You may obtain a copy of the License at
    8    *
    9    *    http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   */
    17
    18  package edu.internet2.middleware.shibboleth.idp.profile.saml1;
    19
    20  import java.io.IOException;
    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.opensaml.common.SAMLObjectBuilder;
    28  import org.opensaml.common.binding.decoding.SAMLMessageDecoder;
    29  import org.opensaml.common.xml.SAMLConstants;
    30  import org.opensaml.saml1.core.AttributeStatement;
    31  import org.opensaml.saml1.core.AuthenticationStatement;
    32  import org.opensaml.saml1.core.Request;
    33  import org.opensaml.saml1.core.Response;
    34  import org.opensaml.saml1.core.Statement;
    35  import org.opensaml.saml1.core.StatusCode;
    36  import org.opensaml.saml1.core.Subject;
    37  import org.opensaml.saml1.core.SubjectLocality;
    38  import org.opensaml.saml2.metadata.AssertionConsumerService;
    39  import org.opensaml.saml2.metadata.Endpoint;
    40  import org.opensaml.saml2.metadata.EntityDescriptor;
    41  import org.opensaml.saml2.metadata.IDPSSODescriptor;
    42  import org.opensaml.saml2.metadata.SPSSODescriptor;
    43  import org.opensaml.util.URLBuilder;
    44  import org.opensaml.ws.message.decoder.MessageDecodingException;
    45  import org.opensaml.ws.transport.http.HTTPInTransport;
    46  import org.opensaml.ws.transport.http.HTTPOutTransport;
    47  import org.opensaml.ws.transport.http.HttpServletRequestAdapter;
    48  import org.opensaml.ws.transport.http.HttpServletResponseAdapter;
    49  import org.opensaml.xml.security.SecurityException;
    50  import org.opensaml.xml.util.DatatypeHelper;
    51  import org.slf4j.Logger;
    52  import org.slf4j.LoggerFactory;
    53
    54  import edu.internet2.middleware.shibboleth.common.ShibbolethConstants;
    55  import edu.internet2.middleware.shibboleth.common.profile.ProfileException;
    56  import edu.internet2.middleware.shibboleth.common.profile.provider.BaseSAMLProfileRequestContext;
    57  import edu.internet2.middleware.shibboleth.common.relyingparty.ProfileConfiguration;
    58  import edu.internet2.middleware.shibboleth.common.relyingparty.RelyingPartyConfiguration;
    59  import edu.internet2.middleware.shibboleth.common.relyingparty.provider.SAMLMDRelyingPartyConfigurationManager;
    60  import edu.internet2.middleware.shibboleth.common.relyingparty.provider.saml1.ShibbolethSSOConfiguration;
    61  import edu.internet2.middleware.shibboleth.common.util.HttpHelper;
    62  import edu.internet2.middleware.shibboleth.idp.authn.ShibbolethSSOLoginContext;
    63  import edu.internet2.middleware.shibboleth.idp.authn.LoginContext;
    64  import edu.internet2.middleware.shibboleth.idp.util.HttpServletHelper;
    65
    66  /** Shibboleth SSO request profile handler. */
    67  public class ShibbolethSSOProfileHandler extends AbstractSAML1ProfileHandler {
    68
    69      /** Class logger. */
    70      private final Logger log = LoggerFactory.getLogger(ShibbolethSSOProfileHandler.class);
    71
    72      /** Builder of AuthenticationStatement objects. */
    73      private SAMLObjectBuilder<AuthenticationStatement> authnStatementBuilder;
    74
    75      /** Builder of SubjectLocality objects. */
    76      private SAMLObjectBuilder<SubjectLocality> subjectLocalityBuilder;
    77
    78      /** Builder of Endpoint objects. */
    79      private SAMLObjectBuilder<Endpoint> endpointBuilder;
    80
    81      /** URL of the authentication manager servlet. */
    82      private String authenticationManagerPath;
    83
    84      /**
    85       * Constructor.
    86       *
    87       * @param authnManagerPath path to the authentication manager servlet
    88       */
    89      public ShibbolethSSOProfileHandler(String authnManagerPath) {
    90          if (DatatypeHelper.isEmpty(authnManagerPath)) {
    91              throw new IllegalArgumentException("Authentication manager path may not be null");
    92          }
    93          if (authnManagerPath.startsWith("/")) {
    94              authenticationManagerPath = authnManagerPath;
    95          } else {
    96              authenticationManagerPath = "/" + authnManagerPath;
    97          }
    98
    99          authnStatementBuilder = (SAMLObjectBuilder<AuthenticationStatement>) getBuilderFactory().getBuilder(
    100                 AuthenticationStatement.DEFAULT_ELEMENT_NAME);
    101
    102         subjectLocalityBuilder = (SAMLObjectBuilder<SubjectLocality>) getBuilderFactory().getBuilder(
    103                 SubjectLocality.DEFAULT_ELEMENT_NAME);
    104
    105         endpointBuilder = (SAMLObjectBuilder<Endpoint>) getBuilderFactory().getBuilder(
    106                 AssertionConsumerService.DEFAULT_ELEMENT_NAME);
    107     }
    108
    109     /** {@inheritDoc} */
    110     public String getProfileId() {
    111         return ShibbolethSSOConfiguration.PROFILE_ID;
    112     }
    113
    114     /** {@inheritDoc} */
    115     public void processRequest(HTTPInTransport inTransport, HTTPOutTransport outTransport) throws ProfileException {
    116         log.debug("Processing incoming request");
    117
    118         HttpServletRequest httpRequest = ((HttpServletRequestAdapter) inTransport).getWrappedRequest();
    119         HttpServletResponse httpResponse = ((HttpServletResponseAdapter) outTransport).getWrappedResponse();
    120         ServletContext servletContext = httpRequest.getSession().getServletContext();
    121
    122 	LoginContext loginContext = HttpServletHelper.getLoginContext(
    123                 getStorageService(), servletContext, httpRequest);
    124
    125         if (loginContext == null || !(loginContext instanceof ShibbolethSSOLoginContext)) {
    126             log.debug("Incoming request does not contain a login context, processing as first leg of request");
    127             performAuthentication(inTransport, outTransport);
    128         } else if (loginContext.isPrincipalAuthenticated() || loginContext.getAuthenticationFailure() != null) {
    129             log.debug("Incoming request contains a login context, processing as second leg of request");
    130             HttpServletHelper.unbindLoginContext(getStorageService(), servletContext, httpRequest, httpResponse);
    131             completeAuthenticationRequest((ShibbolethSSOLoginContext)loginContext, inTransport, outTransport);
    132         } else {
    133             log.debug("Incoming request contained a login context but principal was not authenticated, processing as first leg of request");
    134             performAuthentication(inTransport, outTransport);
    135         }
    136     }
    137
    138     /**
    139      * Creates a {@link ShibbolethSSOLoginContext} an sends the request off to the AuthenticationManager to begin the
    140      * process of authenticating the user.
    141      *
    142      * @param inTransport inbound message transport
    143      * @param outTransport outbound message transport
    144      *
    145      * @throws ProfileException thrown if there is a problem creating the login context and transferring control to the
    146      *             authentication manager
    147      */
    148     protected void performAuthentication(HTTPInTransport inTransport, HTTPOutTransport outTransport)
    149             throws ProfileException {
    150
    151         HttpServletRequest httpRequest = ((HttpServletRequestAdapter) inTransport).getWrappedRequest();
    152         HttpServletResponse httpResponse = ((HttpServletResponseAdapter) outTransport).getWrappedResponse();
    153         ShibbolethSSORequestContext requestContext = new ShibbolethSSORequestContext();
    154
    155         decodeRequest(requestContext, inTransport, outTransport);
    156         ShibbolethSSOLoginContext loginContext = requestContext.getLoginContext();
    157
    158         RelyingPartyConfiguration rpConfig = getRelyingPartyConfiguration(loginContext.getRelyingPartyId());
    159         loginContext.setDefaultAuthenticationMethod(rpConfig.getDefaultAuthenticationMethod());
    160         ProfileConfiguration ssoConfig = rpConfig.getProfileConfiguration(ShibbolethSSOConfiguration.PROFILE_ID);
    161         if (ssoConfig == null) {
    162             String msg = "Shibboleth SSO profile is not configured for relying party "
    163                     + loginContext.getRelyingPartyId();
    164             log.warn(msg);
    165             throw new ProfileException(msg);
    166         }
    167
    168         HttpServletHelper.bindLoginContext(loginContext, getStorageService(), httpRequest.getSession()
    169                 .getServletContext(), httpRequest, httpResponse);
    170
    171         try {
    172             String authnEngineUrl = HttpServletHelper.getContextRelativeUrl(httpRequest, authenticationManagerPath)
    173                     .buildURL();
    174             log.debug("Redirecting user to authentication engine at {}", authnEngineUrl);
    175             httpResponse.sendRedirect(authnEngineUrl);
    176         } catch (IOException e) {
    177             String msg = "Error forwarding Shibboleth SSO request to AuthenticationManager";
    178             log.error(msg, e);
    179             throw new ProfileException(msg, e);
    180         }
    181     }
    182
    183     /**
    184      * Decodes an incoming request and populates a created request context with the resultant information.
    185      *
    186      * @param inTransport inbound message transport
    187      * @param outTransport outbound message transport
    188      * @param requestContext the request context to which decoded information should be added
    189      *
    190      * @throws ProfileException throw if there is a problem decoding the request
    191      */
    192     protected void decodeRequest(ShibbolethSSORequestContext requestContext, HTTPInTransport inTransport,
    193             HTTPOutTransport outTransport) throws ProfileException {
    194         if (log.isDebugEnabled()) {
    195             log.debug("Decoding message with decoder binding {}", getInboundMessageDecoder(requestContext)
    196                     .getBindingURI());
    197         }
    198
    199         HttpServletRequest httpRequest = ((HttpServletRequestAdapter) inTransport).getWrappedRequest();
    200
    201         requestContext.setCommunicationProfileId(getProfileId());
    202
    203         requestContext.setMetadataProvider(getMetadataProvider());
    204         requestContext.setSecurityPolicyResolver(getSecurityPolicyResolver());
    205
    206         requestContext.setCommunicationProfileId(ShibbolethSSOConfiguration.PROFILE_ID);
    207         requestContext.setInboundMessageTransport(inTransport);
    208         requestContext.setInboundSAMLProtocol(ShibbolethConstants.SHIB_SSO_PROFILE_URI);
    209         requestContext.setPeerEntityRole(SPSSODescriptor.DEFAULT_ELEMENT_NAME);
    210
    211         requestContext.setOutboundMessageTransport(outTransport);
    212         requestContext.setOutboundSAMLProtocol(SAMLConstants.SAML11P_NS);
    213
    214         SAMLMessageDecoder decoder = getInboundMessageDecoder(requestContext);
    215         requestContext.setMessageDecoder(decoder);
    216         try {
    217             decoder.decode(requestContext);
    218             log.debug("Decoded Shibboleth SSO request from relying party '{}'",
    219                     requestContext.getInboundMessageIssuer());
    220         } catch (MessageDecodingException e) {
    221             String msg = "Error decoding Shibboleth SSO request";
    222             log.warn(msg, e);
    223             throw new ProfileException(msg, e);
    224         } catch (SecurityException e) {
    225             String msg = "Shibboleth SSO request does not meet security requirements: " + e.getMessage();
    226             log.warn(msg);
    227             throw new ProfileException(msg, e);
    228         }
    229
    230         ShibbolethSSOLoginContext loginContext = new ShibbolethSSOLoginContext();
    231         loginContext.setRelyingParty(requestContext.getInboundMessageIssuer());
    232         loginContext.setSpAssertionConsumerService(requestContext.getSpAssertionConsumerService());
    233         loginContext.setSpTarget(requestContext.getRelayState());
    234         loginContext.setAuthenticationEngineURL(authenticationManagerPath);
    235         loginContext.setProfileHandlerURL(HttpHelper.getRequestUriWithoutContext(httpRequest));
    236         requestContext.setLoginContext(loginContext);
    237     }
    238
    239     /**
    240      * Creates a response to the Shibboleth SSO and sends the user, with response in tow, back to the relying party
    241      * after they've been authenticated.
    242      *
    243      * @param loginContext login context for this request
    244      * @param inTransport inbound message transport
    245      * @param outTransport outbound message transport
    246      *
    247      * @throws ProfileException thrown if the response can not be created and sent back to the relying party
    248      */
    249     protected void completeAuthenticationRequest(ShibbolethSSOLoginContext loginContext, HTTPInTransport inTransport,
    250             HTTPOutTransport outTransport) throws ProfileException {
    251         ShibbolethSSORequestContext requestContext = buildRequestContext(loginContext, inTransport, outTransport);
    252
    253         Response samlResponse;
    254         try {
    255             if (loginContext.getAuthenticationFailure() != null) {
    256                 requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER, null, "User failed authentication"));
    257                 throw new ProfileException("Authentication failure", loginContext.getAuthenticationFailure());
    258             }
    259
    260             resolveAttributes(requestContext);
    261
    262             ArrayList<Statement> statements = new ArrayList<Statement>();
    263             statements.add(buildAuthenticationStatement(requestContext));
    264             if (requestContext.getProfileConfiguration().includeAttributeStatement()) {
    265                 AttributeStatement attributeStatement = buildAttributeStatement(requestContext,
    266                         "urn:oasis:names:tc:SAML:1.0:cm:bearer");
    267                 if (attributeStatement != null) {
    268                     requestContext.setReleasedAttributes(requestContext.getAttributes().keySet());
    269                     statements.add(attributeStatement);
    270                 }
    271             }
    272
    273             samlResponse = buildResponse(requestContext, statements);
    274         } catch (ProfileException e) {
    275             samlResponse = buildErrorResponse(requestContext);
    276         }
    277
    278         requestContext.setOutboundSAMLMessage(samlResponse);
    279         requestContext.setOutboundSAMLMessageId(samlResponse.getID());
    280         requestContext.setOutboundSAMLMessageIssueInstant(samlResponse.getIssueInstant());
    281         encodeResponse(requestContext);
    282         writeAuditLogEntry(requestContext);
    283     }
    284
    285     /**
    286      * Creates an authentication request context from the current environmental information.
    287      *
    288      * @param loginContext current login context
    289      * @param in inbound transport
    290      * @param out outbount transport
    291      *
    292      * @return created authentication request context
    293      *
    294      * @throws ProfileException thrown if there is a problem creating the context
    295      */
    296     protected ShibbolethSSORequestContext buildRequestContext(ShibbolethSSOLoginContext loginContext,
    297             HTTPInTransport in, HTTPOutTransport out) throws ProfileException {
    298         ShibbolethSSORequestContext requestContext = new ShibbolethSSORequestContext();
    299         requestContext.setCommunicationProfileId(getProfileId());
    300
    301         requestContext.setMessageDecoder(getInboundMessageDecoder(requestContext));
    302
    303         requestContext.setLoginContext(loginContext);
    304         requestContext.setRelayState(loginContext.getSpTarget());
    305
    306         requestContext.setInboundMessageTransport(in);
    307         requestContext.setInboundSAMLProtocol(ShibbolethConstants.SHIB_SSO_PROFILE_URI);
    308
    309         requestContext.setOutboundMessageTransport(out);
    310         requestContext.setOutboundSAMLProtocol(SAMLConstants.SAML11P_NS);
    311
    312         requestContext.setMetadataProvider(getMetadataProvider());
    313
    314         String relyingPartyId = loginContext.getRelyingPartyId();
    315         requestContext.setPeerEntityId(relyingPartyId);
    316         requestContext.setInboundMessageIssuer(relyingPartyId);
    317
    318         populateRequestContext(requestContext);
    319
    320         return requestContext;
    321     }
    322
    323     /** {@inheritDoc} */
    324     protected void populateRelyingPartyInformation(BaseSAMLProfileRequestContext requestContext)
    325             throws ProfileException {
    326         super.populateRelyingPartyInformation(requestContext);
    327
    328         EntityDescriptor relyingPartyMetadata = requestContext.getPeerEntityMetadata();
    329         if (relyingPartyMetadata != null) {
    330             requestContext.setPeerEntityRole(SPSSODescriptor.DEFAULT_ELEMENT_NAME);
    331             requestContext.setPeerEntityRoleMetadata(relyingPartyMetadata.getSPSSODescriptor(SAMLConstants.SAML11P_NS));
    332         }
    333     }
    334
    335     /** {@inheritDoc} */
    336     protected void populateAssertingPartyInformation(BaseSAMLProfileRequestContext requestContext)
    337             throws ProfileException {
    338         super.populateAssertingPartyInformation(requestContext);
    339
    340         EntityDescriptor localEntityDescriptor = requestContext.getLocalEntityMetadata();
    341         if (localEntityDescriptor != null) {
    342             requestContext.setLocalEntityRole(IDPSSODescriptor.DEFAULT_ELEMENT_NAME);
    343             requestContext.setLocalEntityRoleMetadata(localEntityDescriptor
    344                     .getIDPSSODescriptor(SAMLConstants.SAML20P_NS));
    345         }
    346     }
    347
    348     /** {@inheritDoc} */
    349     protected void populateSAMLMessageInformation(BaseSAMLProfileRequestContext requestContext) throws ProfileException {
    350         // nothing to do here
    351     }
    352
    353     /**
    354      * Selects the appropriate endpoint for the relying party and stores it in the request context.
    355      *
    356      * @param requestContext current request context
    357      *
    358      * @return Endpoint selected from the information provided in the request context
    359      */
    360     protected Endpoint selectEndpoint(BaseSAMLProfileRequestContext requestContext) {
    361         ShibbolethSSOLoginContext loginContext = ((ShibbolethSSORequestContext) requestContext).getLoginContext();
    362
    363         Endpoint endpoint = null;
    364         if (requestContext.getRelyingPartyConfiguration().getRelyingPartyId() == SAMLMDRelyingPartyConfigurationManager.ANONYMOUS_RP_NAME) {
    365             if (loginContext.getSpAssertionConsumerService() != null) {
    366                 endpoint = endpointBuilder.buildObject();
    367                 endpoint.setLocation(loginContext.getSpAssertionConsumerService());
    368                 endpoint.setBinding(getSupportedOutboundBindings().get(0));
    369                 log.warn("Generating endpoint for anonymous relying party. ACS url {} and binding {}", new Object[] {
    370                         requestContext.getInboundMessageIssuer(), endpoint.getLocation(), endpoint.getBinding(), });
    371             } else {
    372                 log.warn("Unable to generate endpoint for anonymous party.  No ACS url provided.");
    373             }
    374         } else {
    375             ShibbolethSSOEndpointSelector endpointSelector = new ShibbolethSSOEndpointSelector();
    376             endpointSelector.setSpAssertionConsumerService(loginContext.getSpAssertionConsumerService());
    377             endpointSelector.setEndpointType(AssertionConsumerService.DEFAULT_ELEMENT_NAME);
    378             endpointSelector.setMetadataProvider(getMetadataProvider());
    379             endpointSelector.setEntityMetadata(requestContext.getPeerEntityMetadata());
    380             endpointSelector.setEntityRoleMetadata(requestContext.getPeerEntityRoleMetadata());
    381             endpointSelector.setSamlRequest(requestContext.getInboundSAMLMessage());
    382             endpointSelector.getSupportedIssuerBindings().addAll(getSupportedOutboundBindings());
    383             endpoint = endpointSelector.selectEndpoint();
    384         }
    385
    386         return endpoint;
    387     }
    388
    389     /**
    390      * Builds the authentication statement for the authenticated principal.
    391      *
    392      * @param requestContext current request context
    393      *
    394      * @return the created statement
    395      *
    396      * @throws ProfileException thrown if the authentication statement can not be created
    397      */
    398     protected AuthenticationStatement buildAuthenticationStatement(ShibbolethSSORequestContext requestContext)
    399             throws ProfileException {
    400         ShibbolethSSOLoginContext loginContext = requestContext.getLoginContext();
    401
    402         AuthenticationStatement statement = authnStatementBuilder.buildObject();
    403         statement.setAuthenticationInstant(loginContext.getAuthenticationInstant());
    404         statement.setAuthenticationMethod(loginContext.getAuthenticationMethod());
    405
    406         statement.setSubjectLocality(buildSubjectLocality(requestContext));
    407
    408         Subject statementSubject;
    409         Endpoint endpoint = selectEndpoint(requestContext);
    410         if (endpoint.getBinding().equals(SAMLConstants.SAML1_ARTIFACT_BINDING_URI)) {
    411             statementSubject = buildSubject(requestContext, "urn:oasis:names:tc:SAML:1.0:cm:artifact");
    412         } else {
    413             statementSubject = buildSubject(requestContext, "urn:oasis:names:tc:SAML:1.0:cm:bearer");
    414         }
    415         statement.setSubject(statementSubject);
    416
    417         return statement;
    418     }
    419
    420     /**
    421      * Constructs the subject locality for the authentication statement.
    422      *
    423      * @param requestContext current request context
    424      *
    425      * @return subject locality for the authentication statement
    426      */
    427     protected SubjectLocality buildSubjectLocality(ShibbolethSSORequestContext requestContext) {
    428         SubjectLocality subjectLocality = subjectLocalityBuilder.buildObject();
    429
    430         HTTPInTransport inTransport = (HTTPInTransport) requestContext.getInboundMessageTransport();
    431         subjectLocality.setIPAddress(inTransport.getPeerAddress());
    432
    433         return subjectLocality;
    434     }
    435
    436     /** Represents the internal state of a Shibboleth SSO Request while it's being processed by the IdP. */
    437     public class ShibbolethSSORequestContext extends
    438             BaseSAML1ProfileRequestContext<Request, Response, ShibbolethSSOConfiguration> {
    439
    440         /** SP-provide assertion consumer service URL. */
    441         private String spAssertionConsumerService;
    442
    443         /** Current login context. */
    444         private ShibbolethSSOLoginContext loginContext;
    445
    446         /**
    447          * Gets the current login context.
    448          *
    449          * @return current login context
    450          */
    451         public ShibbolethSSOLoginContext getLoginContext() {
    452             return loginContext;
    453         }
    454
    455         /**
    456          * Sets the current login context.
    457          *
    458          * @param context current login context
    459          */
    460         public void setLoginContext(ShibbolethSSOLoginContext context) {
    461             loginContext = context;
    462         }
    463
    464         /**
    465          * Gets the SP-provided assertion consumer service URL.
    466          *
    467          * @return SP-provided assertion consumer service URL
    468          */
    469         public String getSpAssertionConsumerService() {
    470             return spAssertionConsumerService;
    471         }
    472
    473         /**
    474          * Sets the SP-provided assertion consumer service URL.
    475          *
    476          * @param acs SP-provided assertion consumer service URL
    477          */
    478         public void setSpAssertionConsumerService(String acs) {
    479             spAssertionConsumerService = acs;
    480         }
    481     }
    482 }