View Javadoc

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