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.saml2;
    19
    20  import java.io.OutputStreamWriter;
    21  import java.io.Writer;
    22  import java.util.ArrayList;
    23
    24  import javax.servlet.http.HttpServletRequest;
    25
    26  import org.joda.time.DateTime;
    27  import org.opensaml.Configuration;
    28  import org.opensaml.common.SAMLObjectBuilder;
    29  import org.opensaml.common.binding.decoding.SAMLMessageDecoder;
    30  import org.opensaml.common.xml.SAMLConstants;
    31  import org.opensaml.saml2.binding.decoding.HandlerChainAwareHTTPSOAP11Decoder;
    32  import org.opensaml.saml2.binding.encoding.HandlerChainAwareHTTPSOAP11Encoder;
    33  import org.opensaml.saml2.core.AttributeStatement;
    34  import org.opensaml.saml2.core.AuthnContext;
    35  import org.opensaml.saml2.core.AuthnContextClassRef;
    36  import org.opensaml.saml2.core.AuthnRequest;
    37  import org.opensaml.saml2.core.AuthnStatement;
    38  import org.opensaml.saml2.core.Response;
    39  import org.opensaml.saml2.core.Statement;
    40  import org.opensaml.saml2.core.StatusCode;
    41  import org.opensaml.saml2.core.Subject;
    42  import org.opensaml.saml2.core.SubjectConfirmation;
    43  import org.opensaml.saml2.metadata.SPSSODescriptor;
    44  import org.opensaml.ws.message.decoder.MessageDecodingException;
    45
    46  import org.opensaml.ws.transport.http.HTTPInTransport;
    47  import org.opensaml.ws.transport.http.HTTPOutTransport;
    48  import org.opensaml.ws.transport.http.HttpServletRequestAdapter;
    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.profile.ProfileException;
    55  import edu.internet2.middleware.shibboleth.common.profile.provider.BaseSAMLProfileRequestContext;
    56  import edu.internet2.middleware.shibboleth.common.relyingparty.ProfileConfiguration;
    57  import edu.internet2.middleware.shibboleth.common.relyingparty.RelyingPartyConfiguration;
    58  import edu.internet2.middleware.shibboleth.common.relyingparty.provider.saml2.ECPConfiguration;
    59
    60  import org.opensaml.ws.message.handler.BasicHandlerChain;
    61  import org.opensaml.ws.message.handler.Handler;
    62  import org.opensaml.ws.message.handler.HandlerChain;
    63  import org.opensaml.ws.message.handler.HandlerChainResolver;
    64  import org.opensaml.ws.message.handler.HandlerException;
    65  import org.opensaml.ws.message.handler.StaticHandlerChainResolver;
    66  import org.opensaml.ws.message.MessageContext;
    67  import org.opensaml.ws.soap.soap11.ActorBearing;
    68  import org.opensaml.ws.soap.util.SOAPHelper;
    69
    70  import org.opensaml.common.binding.SAMLMessageContext;
    71  import org.opensaml.common.binding.encoding.SAMLMessageEncoder;
    72
    73  /** SAML 2.0 ECP request profile handler. */
    74  public class SAML2ECPProfileHandler extends SSOProfileHandler {
    75
    76      /** Class logger. */
    77      private final Logger log = LoggerFactory.getLogger(SAML2ECPProfileHandler.class);
    78
    79      /** A context class reference to insert into the assertion. */
    80      private String authnContextClassRef = AuthnContext.PPT_AUTHN_CTX;
    81
    82      /** Builder of ECP Response object. */
    83      private SAMLObjectBuilder<org.opensaml.saml2.ecp.Response> ecpResponseBuilder;
    84
    85      /** Builder of AuthnContext objects. */
    86      private SAMLObjectBuilder<AuthnContext> authnContextBuilder;
    87
    88      /** Builder of AuthnContextClassRef objects. */
    89      private SAMLObjectBuilder<AuthnContextClassRef> authnContextClassRefBuilder;
    90
    91      /** Static pre-security inbound handler chain resolver. */
    92      private StaticHandlerChainResolver inboundPreSecurityHandlerChainResolver;
    93
    94      /** Static post-security inbound handler chain resolver. */
    95      private StaticHandlerChainResolver inboundPostSecurityHandlerChainResolver;
    96
    97      /** Static outbound handler chain resolver. */
    98      private StaticHandlerChainResolver outboundHandlerChainResolver;
    99
    100     /** SOAP message encoder to use. */
    101     private SAMLMessageEncoder messageEncoder;
    102
    103     /** SOAP message decoder to use. */
    104     private SAMLMessageDecoder messageDecoder;
    105
    106     // canned soap fauilt
    107     private static String soapFaultResponseMessage =
    108 "<env:Envelope xmlns:env=\"http://schemas.xmlsoap.org/soap/envelope/\">" +
    109 " <env:Body>" +
    110 " <env:Fault>" +
    111 " <faultcode>env:Client</faultcode>" +
    112 " <faultstring>An error occurred processing the request.</faultstring>" +
    113 " <detail/>" +
    114 " </env:Fault>" +
    115 " </env:Body>" +
    116 "</env:Envelope>";
    117
    118
    119     /**
    120      * Constructor.
    121      *
    122      */
    123     @SuppressWarnings("unchecked")
    124     public SAML2ECPProfileHandler() {
    125         super("/Save/My/Walrus");   // need a dummy value to build base class
    126
    127         ecpResponseBuilder = (SAMLObjectBuilder<org.opensaml.saml2.ecp.Response>) Configuration.getBuilderFactory().
    128                 getBuilder(org.opensaml.saml2.ecp.Response.DEFAULT_ELEMENT_NAME);
    129
    130         authnContextBuilder = (SAMLObjectBuilder<AuthnContext>) getBuilderFactory().getBuilder(
    131                 AuthnContext.DEFAULT_ELEMENT_NAME);
    132         authnContextClassRefBuilder = (SAMLObjectBuilder<AuthnContextClassRef>) getBuilderFactory().getBuilder(
    133                 AuthnContextClassRef.DEFAULT_ELEMENT_NAME);
    134     }
    135
    136     /** Initialize the profile handler. */
    137     public void initialize() {
    138         messageDecoder = new HandlerChainAwareHTTPSOAP11Decoder();
    139         messageEncoder = new HandlerChainAwareHTTPSOAP11Encoder();
    140         ((HandlerChainAwareHTTPSOAP11Encoder) messageEncoder).setNotConfidential(true);
    141
    142         inboundPreSecurityHandlerChainResolver = new StaticHandlerChainResolver(buildPreSecurityInboundHandlerChain());
    143         inboundPostSecurityHandlerChainResolver = new StaticHandlerChainResolver(buildPostSecurityInboundHandlerChain());
    144         outboundHandlerChainResolver = new StaticHandlerChainResolver(buildOutboundHandlerChain());
    145
    146         // This is needed by the endpoint selector. We're obviously not actually responding over this binding.
    147         // In this profile handler, outbound binding selection is not determined by the product of the
    148         // EndpointSelector.
    149         ArrayList<String> ecpOutboundBindings = new ArrayList<String>();
    150         ecpOutboundBindings.add(SAMLConstants.SAML2_PAOS_BINDING_URI);
    151         setSupportedOutboundBindings(ecpOutboundBindings);
    152     }
    153
    154
    155     /** {@inheritDoc} */
    156     public String getProfileId() {
    157         return ECPConfiguration.PROFILE_ID;
    158     }
    159
    160     /**
    161      * Sets the AuthnContext class reference.
    162      * @param ref AuthnContext class reference to set
    163      */
    164     public void setAuthnContextClassRef(String ref) {
    165         authnContextClassRef = ref;
    166     }
    167
    168     /**
    169      * Gets the AuthnContext class reference.
    170      * @return AuthnContext class reference
    171      */
    172     public String getAuthnContextClassRef() {
    173         return authnContextClassRef;
    174     }
    175
    176     /** {@inheritDoc} */
    177     public void processRequest(HTTPInTransport inTransport, HTTPOutTransport outTransport) throws ProfileException {
    178         ECPRequestContext requestContext = buildRequestContext(inTransport, outTransport);
    179
    180         Response samlResponse;
    181
    182         try {
    183             decodeRequest(requestContext, inTransport, outTransport);
    184             checkSamlVersion(requestContext);
    185             checkNameIDPolicy(requestContext);
    186
    187             if (requestContext.getPrincipalName() == null) {
    188                 requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, StatusCode.AUTHN_FAILED_URI,
    189                         null));
    190                 throw new ProfileException("Authentication not performed");
    191             }
    192
    193             if (requestContext.getSubjectNameIdentifier() != null) {
    194                 log.debug("Authentication request contained a subject with a name identifier, resolving principal from NameID");
    195                 String authenticatedName = requestContext.getPrincipalName();
    196                 resolvePrincipal(requestContext);
    197                 String requestedPrincipalName = requestContext.getPrincipalName();
    198                 if (!DatatypeHelper.safeEquals(authenticatedName, requestedPrincipalName)) {
    199                     log.warn(
    200                             "Authentication request identified principal {} but authentication mechanism identified principal {}",
    201                             requestedPrincipalName, authenticatedName);
    202                     requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, StatusCode.AUTHN_FAILED_URI,
    203                             null));
    204                     throw new ProfileException("User failed authentication");
    205                 }
    206             }
    207
    208             String relyingPartyId = requestContext.getInboundMessageIssuer();
    209             RelyingPartyConfiguration rpConfig = getRelyingPartyConfiguration(relyingPartyId);
    210             ProfileConfiguration ecpConfig = rpConfig.getProfileConfiguration(getProfileId());
    211             if (ecpConfig == null) {
    212                 log.warn("SAML2ECP profile is not configured for relying party '{}'", requestContext.getInboundMessageIssuer());
    213                 requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, StatusCode.REQUEST_UNSUPPORTED_URI,
    214                         null));
    215                 throw new ProfileException("SAML2ECP profile is not configured for relying party");
    216             }
    217
    218             resolveAttributes(requestContext);
    219
    220             ArrayList<Statement> statements = new ArrayList<Statement>();
    221             statements.add(buildAuthnStatement(requestContext));
    222             if (requestContext.getProfileConfiguration().includeAttributeStatement()) {
    223                 AttributeStatement attributeStatement = buildAttributeStatement(requestContext);
    224                 if (attributeStatement != null) {
    225                     requestContext.setReleasedAttributes(requestContext.getAttributes().keySet());
    226                     statements.add(attributeStatement);
    227                 }
    228             }
    229
    230             samlResponse = buildResponse(requestContext, SubjectConfirmation.METHOD_BEARER, statements);
    231             samlResponse.setDestination(requestContext.getPeerEntityEndpoint().getLocation());
    232
    233         } catch (ProfileException e) {
    234             if (requestContext.getPeerEntityEndpoint() != null) {
    235                 samlResponse = buildErrorResponse(requestContext);
    236             } else {
    237                 log.debug("Returning SOAP fault", e);
    238                 try {
    239                    outTransport.setCharacterEncoding("UTF-8");
    240                    outTransport.setHeader("Content-Type", "application/soap+xml");
    241                    outTransport.setStatusCode(500);  // seem to lose the message when we report an error.
    242                    Writer out = new OutputStreamWriter(outTransport.getOutgoingStream(), "UTF-8");
    243                    out.write(soapFaultResponseMessage);
    244                    out.flush();
    245                 } catch (Exception we) {
    246                    log.error("Error returning SOAP fault", we);
    247                 }
    248                 return;
    249             }
    250         }
    251
    252         requestContext.setOutboundSAMLMessage(samlResponse);
    253         requestContext.setOutboundSAMLMessageId(samlResponse.getID());
    254         requestContext.setOutboundSAMLMessageIssueInstant(samlResponse.getIssueInstant());
    255         encodeResponse(requestContext);
    256         writeAuditLogEntry(requestContext);
    257     }
    258
    259     /**
    260      * Decodes an incoming request and stores the information in a created request context.
    261      *
    262      * @param inTransport inbound transport
    263      * @param outTransport outbound transport
    264      * @param requestContext request context to which decoded information should be added
    265      *
    266      * @throws ProfileException thrown if the incoming message failed decoding
    267      */
    268     protected void decodeRequest(ECPRequestContext requestContext, HTTPInTransport inTransport,
    269             HTTPOutTransport outTransport) throws ProfileException {
    270         if (log.isDebugEnabled()) {
    271             log.debug("Decoding message with decoder binding '{}'", getInboundMessageDecoder(requestContext)
    272                     .getBindingURI());
    273         }
    274
    275         try {
    276             SAMLMessageDecoder decoder = getInboundMessageDecoder(requestContext);
    277             requestContext.setMessageDecoder(decoder);
    278             decoder.decode(requestContext);
    279             log.debug("Decoded request from relying party '{}'", requestContext.getInboundMessageIssuer());
    280
    281             if (!(requestContext.getInboundSAMLMessage() instanceof AuthnRequest)) {
    282                 log.warn("Incomming message was not a AuthnRequest, it was a '{}'", requestContext
    283                         .getInboundSAMLMessage().getClass().getName());
    284                 requestContext.setFailureStatus(buildStatus(StatusCode.REQUESTER_URI, StatusCode.REQUEST_UNSUPPORTED_URI,
    285                         "Invalid SAML AuthnRequest message."));
    286                 throw new ProfileException("Invalid SAML AuthnRequest message.");
    287             }
    288
    289             AuthnRequest authnRequest = requestContext.getInboundSAMLMessage();
    290             Subject authnSubject = authnRequest.getSubject();
    291             if (authnSubject != null) {
    292                 requestContext.setSubjectNameIdentifier(authnSubject.getNameID());
    293             }
    294         } catch (MessageDecodingException e) {
    295             String msg = "Error decoding authentication request message";
    296             requestContext.setFailureStatus(buildStatus(StatusCode.REQUESTER_URI, StatusCode.REQUEST_UNSUPPORTED_URI, msg));
    297             log.warn(msg, e);
    298             throw new ProfileException(msg, e);
    299         } catch (SecurityException e) {
    300             String msg = "Message did not meet security requirements";
    301             requestContext.setFailureStatus(buildStatus(StatusCode.REQUESTER_URI, StatusCode.REQUEST_DENIED_URI, msg));
    302             log.warn(msg, e);
    303             throw new ProfileException(msg, e);
    304         }
    305         populateRequestContext(requestContext);
    306     }
    307
    308     /**
    309      * Creates an authentication request context from the current environmental information.
    310      *
    311      * @param in inbound transport
    312      * @param out outbount transport
    313      *
    314      * @return created authentication request context
    315      *
    316      * @throws ProfileException thrown if there is a problem creating the context
    317      */
    318     protected ECPRequestContext buildRequestContext(HTTPInTransport in, HTTPOutTransport out)
    319             throws ProfileException {
    320         ECPRequestContext requestContext = new ECPRequestContext();
    321
    322         requestContext.setCommunicationProfileId(getProfileId());
    323         requestContext.setMessageDecoder(getInboundMessageDecoder(requestContext));
    324         requestContext.setInboundMessageTransport(in);
    325         requestContext.setInboundSAMLProtocol(SAMLConstants.SAML20P_NS);
    326         requestContext.setOutboundMessageTransport(out);
    327         requestContext.setOutboundSAMLProtocol(SAMLConstants.SAML20P_NS);
    328         requestContext.setPeerEntityRole(SPSSODescriptor.DEFAULT_ELEMENT_NAME);
    329         requestContext.setMetadataProvider(getMetadataProvider());
    330         requestContext.setSecurityPolicyResolver(getSecurityPolicyResolver());
    331
    332         // Does this do anything?
    333         String relyingPartyId = requestContext.getInboundMessageIssuer();
    334         requestContext.setPeerEntityId(relyingPartyId);
    335         requestContext.setInboundMessageIssuer(relyingPartyId);
    336
    337         requestContext.setPreSecurityInboundHandlerChainResolver(getPreSecurityInboundHandlerChainResolver());
    338         requestContext.setPostSecurityInboundHandlerChainResolver(getPostSecurityInboundHandlerChainResolver());
    339         requestContext.setOutboundHandlerChainResolver(getOutboundHandlerChainResolver());
    340
    341         return requestContext;
    342     }
    343
    344     /** {@inheritDoc} */
    345     protected void populateSAMLMessageInformation(BaseSAMLProfileRequestContext requestContext) throws ProfileException {
    346         AuthnRequest authnRequest = (AuthnRequest) requestContext.getInboundSAMLMessage();
    347         Subject authnSubject = authnRequest.getSubject();
    348         if (authnSubject != null) {
    349             requestContext.setSubjectNameIdentifier(authnSubject.getNameID());
    350         }
    351     }
    352
    353     /**
    354      * Creates an authentication statement for the current request.
    355      *
    356      * @param requestContext current request context
    357      *
    358      * @return constructed authentication statement
    359      */
    360     protected AuthnStatement buildAuthnStatement(SSORequestContext requestContext) {
    361         AuthnStatement statement = super.buildAuthnStatement(requestContext);
    362         statement.setAuthnInstant(new DateTime());
    363         return statement;
    364     }
    365
    366     /**
    367      * Creates an {@link AuthnContext} for a successful authentication request.
    368      *
    369      * @param requestContext current request
    370      *
    371      * @return the built authn context
    372      */
    373     protected AuthnContext buildAuthnContext(SSORequestContext requestContext) {
    374         if (getAuthnContextClassRef() != null) {
    375             AuthnContext authnContext = authnContextBuilder.buildObject();
    376             AuthnContextClassRef ref = authnContextClassRefBuilder.buildObject();
    377             ref.setAuthnContextClassRef(getAuthnContextClassRef());
    378             authnContext.setAuthnContextClassRef(ref);
    379             return authnContext;
    380         }
    381         return null;
    382     }
    383
    384     /** Stub in case we define additional context data. */
    385     /** In case we ever add something to the base context **/
    386     protected class ECPRequestContext extends SSORequestContext {
    387     }
    388
    389
    390     /**
    391      * Build the pre-security inbound handler chain.
    392      *
    393      * @return the handler chain
    394      */
    395     protected HandlerChain buildPreSecurityInboundHandlerChain() {
    396         BasicHandlerChain handlerChain = new BasicHandlerChain();
    397
    398         handlerChain.getHandlers().add( new Handler() {
    399             public void invoke(MessageContext msgContext) throws HandlerException {
    400                 ECPRequestContext ctx = (ECPRequestContext) msgContext;
    401                 HttpServletRequest httpRequest =
    402                     ((HttpServletRequestAdapter) msgContext.getInboundMessageTransport()).getWrappedRequest();
    403                 String user = httpRequest.getRemoteUser();
    404                 if (user != null) {
    405                     log.debug("Setting principal name: {}", user);
    406                     ctx.setPrincipalName(user);
    407                 } else {
    408                     log.warn("REMOTE_USER not set, unable to set principal name");
    409                 }
    410             }
    411         });
    412
    413        return handlerChain;
    414     }
    415
    416     /**
    417      * Build the post-security inbound handler chain.
    418      *
    419      * @return the handler chain
    420      */
    421     protected HandlerChain buildPostSecurityInboundHandlerChain() {
    422        return null;
    423     }
    424
    425     /**
    426      * Get the resolver used to resolve the pre-security inbound handler chain.
    427      *
    428      * @return the handler chain resolver
    429      */
    430     protected HandlerChainResolver getPreSecurityInboundHandlerChainResolver() {
    431         return inboundPreSecurityHandlerChainResolver;
    432     }
    433
    434     /**
    435      * Get the resolver used to resolve the post-security inbound handler chain.
    436      *
    437      * @return the handler chain resolver
    438      */
    439     protected HandlerChainResolver getPostSecurityInboundHandlerChainResolver() {
    440         return inboundPostSecurityHandlerChainResolver;
    441     }
    442
    443     /**
    444      * Build the outbound handler chain.
    445      *
    446      * @return the handler chain
    447      */
    448     protected HandlerChain buildOutboundHandlerChain() {
    449         BasicHandlerChain handlerChain = new BasicHandlerChain();
    450
    451         handlerChain.getHandlers().add( new Handler() {
    452             public void invoke(MessageContext msgContext) throws HandlerException {
    453                 SAMLMessageContext samlMsgCtx = (SAMLMessageContext) msgContext;
    454                 org.opensaml.saml2.ecp.Response response = ecpResponseBuilder.buildObject();
    455                 if (samlMsgCtx.getPeerEntityEndpoint() == null || samlMsgCtx.getPeerEntityEndpoint().getLocation() == null) {
    456                     throw new HandlerException("Unable to determine ACS URL for response.");
    457                 }
    458                 response.setAssertionConsumerServiceURL(samlMsgCtx.getPeerEntityEndpoint().getLocation());
    459                 SOAPHelper.addSOAP11MustUnderstandAttribute(response, true);
    460                 SOAPHelper.addSOAP11ActorAttribute(response, ActorBearing.SOAP11_ACTOR_NEXT);
    461                 SOAPHelper.addHeaderBlock(msgContext, response);
    462             }
    463         });
    464
    465         return handlerChain;
    466     }
    467
    468     /**
    469      * Get the resolver used to resolve the outbound handler chain.
    470      *
    471      * @return the handler chain resolver
    472      */
    473     protected HandlerChainResolver getOutboundHandlerChainResolver() {
    474         return outboundHandlerChainResolver;
    475     }
    476
    477     /** {@inheritDoc} */
    478     protected SAMLMessageEncoder getOutboundMessageEncoder(BaseSAMLProfileRequestContext requestContext)
    479             throws ProfileException {
    480         return messageEncoder;
    481     }
    482
    483     /** {@inheritDoc} */
    484     protected SAMLMessageDecoder getInboundMessageDecoder(BaseSAMLProfileRequestContext requestContext)
    485             throws ProfileException {
    486         return messageDecoder;
    487     }
    488 }