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.util.ArrayList;
    21
    22  import org.opensaml.common.SAMLObjectBuilder;
    23  import org.opensaml.common.binding.BasicEndpointSelector;
    24  import org.opensaml.common.binding.decoding.SAMLMessageDecoder;
    25  import org.opensaml.common.xml.SAMLConstants;
    26  import org.opensaml.saml2.core.AttributeQuery;
    27  import org.opensaml.saml2.core.AttributeStatement;
    28  import org.opensaml.saml2.core.NameID;
    29  import org.opensaml.saml2.core.Response;
    30  import org.opensaml.saml2.core.Statement;
    31  import org.opensaml.saml2.core.StatusCode;
    32  import org.opensaml.saml2.core.Subject;
    33  import org.opensaml.saml2.metadata.AssertionConsumerService;
    34  import org.opensaml.saml2.metadata.AttributeAuthorityDescriptor;
    35  import org.opensaml.saml2.metadata.Endpoint;
    36  import org.opensaml.saml2.metadata.EntityDescriptor;
    37  import org.opensaml.saml2.metadata.SPSSODescriptor;
    38  import org.opensaml.saml2.metadata.provider.MetadataProvider;
    39  import org.opensaml.ws.message.decoder.MessageDecodingException;
    40  import org.opensaml.ws.transport.http.HTTPInTransport;
    41  import org.opensaml.ws.transport.http.HTTPOutTransport;
    42  import org.opensaml.xml.security.SecurityException;
    43  import org.slf4j.Logger;
    44  import org.slf4j.LoggerFactory;
    45
    46  import edu.internet2.middleware.shibboleth.common.attribute.provider.BasicAttribute;
    47  import edu.internet2.middleware.shibboleth.common.profile.ProfileException;
    48  import edu.internet2.middleware.shibboleth.common.profile.provider.BaseSAMLProfileRequestContext;
    49  import edu.internet2.middleware.shibboleth.common.relyingparty.provider.saml2.AttributeQueryConfiguration;
    50  import edu.internet2.middleware.shibboleth.idp.session.AuthenticationMethodInformation;
    51  import edu.internet2.middleware.shibboleth.idp.session.Session;
    52
    53  /** SAML 2.0 Attribute Query profile handler. */
    54  public class AttributeQueryProfileHandler extends AbstractSAML2ProfileHandler {
    55
    56      /** Class logger. */
    57      private static Logger log = LoggerFactory.getLogger(AttributeQueryProfileHandler.class);
    58
    59      /** Builder of NameID objects. */
    60      private SAMLObjectBuilder<NameID> nameIDBuilder;
    61
    62      /** Builder of assertion consumer service endpoints. */
    63      private SAMLObjectBuilder<AssertionConsumerService> acsEndpointBuilder;
    64
    65      /** Constructor. */
    66      public AttributeQueryProfileHandler() {
    67          super();
    68
    69          nameIDBuilder = (SAMLObjectBuilder<NameID>) getBuilderFactory().getBuilder(
    70                  NameID.DEFAULT_ELEMENT_NAME);
    71          acsEndpointBuilder = (SAMLObjectBuilder<AssertionConsumerService>) getBuilderFactory().getBuilder(
    72                  AssertionConsumerService.DEFAULT_ELEMENT_NAME);
    73      }
    74
    75      /** {@inheritDoc} */
    76      public String getProfileId() {
    77          return AttributeQueryConfiguration.PROFILE_ID;
    78      }
    79
    80      /** {@inheritDoc} */
    81      public void processRequest(HTTPInTransport inTransport, HTTPOutTransport outTransport) throws ProfileException {
    82          Response samlResponse;
    83
    84          AttributeQueryContext requestContext = new AttributeQueryContext();
    85
    86          try {
    87              decodeRequest(requestContext, inTransport, outTransport);
    88
    89              if (requestContext.getProfileConfiguration() == null) {
    90                  String msg = "SAML 2 Attribute Query profile is not configured for relying party "
    91                          + requestContext.getInboundMessage();
    92                  requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, StatusCode.REQUEST_DENIED_URI,
    93                          msg));
    94                  log.warn(msg);
    95                  samlResponse = buildErrorResponse(requestContext);
    96              } else {
    97                  checkSamlVersion(requestContext);
    98
    99                  // TODO add proper requested attribute filtering support
    100                 AttributeQuery query = requestContext.getInboundSAMLMessage();
    101                 if (query.getAttributes() != null && !query.getAttributes().isEmpty()) {
    102                     log.warn("Specific attributes requested in query '{}'. This functionality is not yet supported",
    103                             requestContext.getInboundMessageIssuer());
    104                     requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI,
    105                             StatusCode.REQUEST_UNSUPPORTED_URI,
    106                             "Request of specific attributes during an attribute query is not supported"));
    107                     throw new ProfileException();
    108                 }
    109
    110                 // Resolve attribute query name id to principal name and place in context
    111                 resolvePrincipal(requestContext);
    112
    113                 Session idpSession = getSessionManager().getSession(requestContext.getPrincipalName());
    114                 if (idpSession != null) {
    115                     requestContext.setUserSession(idpSession);
    116                     AuthenticationMethodInformation authnInfo = idpSession.getAuthenticationMethods().get(
    117                             requestContext.getInboundMessageIssuer());
    118                     if (authnInfo != null) {
    119                         requestContext.setPrincipalAuthenticationMethod(authnInfo.getAuthenticationMethod());
    120                     }
    121                 }
    122
    123                 resolveAttributes(requestContext);
    124
    125                 // Lookup principal name and attributes, create attribute statement from information
    126                 ArrayList<Statement> statements = new ArrayList<Statement>();
    127                 AttributeStatement attributeStatement = buildAttributeStatement(requestContext);
    128                 if (attributeStatement != null) {
    129                     requestContext.setReleasedAttributes(requestContext.getAttributes().keySet());
    130                     statements.add(attributeStatement);
    131                 }
    132
    133                 // create the SAML response
    134                 samlResponse = buildResponse(requestContext, "urn:oasis:names:tc:SAML:2.0:cm:sender-vouches",
    135                         statements);
    136             }
    137         } catch (ProfileException e) {
    138             samlResponse = buildErrorResponse(requestContext);
    139         }
    140
    141         requestContext.setOutboundSAMLMessage(samlResponse);
    142         requestContext.setOutboundSAMLMessageId(samlResponse.getID());
    143         requestContext.setOutboundSAMLMessageIssueInstant(samlResponse.getIssueInstant());
    144
    145         encodeResponse(requestContext);
    146         writeAuditLogEntry(requestContext);
    147     }
    148
    149     /**
    150      * Decodes an incoming request and populates a created request context with the resultant information.
    151      *
    152      * @param inTransport inbound message transport
    153      * @param outTransport outbound message transport *
    154      * @param requestContext request context to which decoded information should be added
    155      *
    156      * @throws ProfileException throw if there is a problem decoding the request
    157      */
    158     protected void decodeRequest(AttributeQueryContext requestContext, HTTPInTransport inTransport,
    159             HTTPOutTransport outTransport) throws ProfileException {
    160         if (log.isDebugEnabled()) {
    161             log.debug("Decoding message with decoder binding '{}'", getInboundMessageDecoder(requestContext)
    162                     .getBindingURI());
    163         }
    164
    165         requestContext.setCommunicationProfileId(getProfileId());
    166
    167         MetadataProvider metadataProvider = getMetadataProvider();
    168         requestContext.setMetadataProvider(metadataProvider);
    169
    170         requestContext.setInboundMessageTransport(inTransport);
    171         requestContext.setInboundSAMLProtocol(SAMLConstants.SAML20P_NS);
    172         requestContext.setSecurityPolicyResolver(getSecurityPolicyResolver());
    173         requestContext.setPeerEntityRole(SPSSODescriptor.DEFAULT_ELEMENT_NAME);
    174
    175         requestContext.setOutboundMessageTransport(outTransport);
    176         requestContext.setOutboundSAMLProtocol(SAMLConstants.SAML20P_NS);
    177
    178         try {
    179             SAMLMessageDecoder decoder = getInboundMessageDecoder(requestContext);
    180             requestContext.setMessageDecoder(decoder);
    181             decoder.decode(requestContext);
    182             log.debug("Decoded request from relying party '{}'", requestContext.getInboundMessageIssuer());
    183
    184             if (!(requestContext.getInboundSAMLMessage() instanceof AttributeQuery)) {
    185                 log.warn("Incoming message was not a AttributeQuery, it was a {}", requestContext
    186                         .getInboundSAMLMessage().getClass().getName());
    187                 requestContext.setFailureStatus(buildStatus(StatusCode.REQUESTER_URI, null,
    188                         "Invalid SAML AttributeQuery message."));
    189                 throw new ProfileException("Invalid SAML AttributeQuery message.");
    190             }
    191         } catch (MessageDecodingException e) {
    192             String msg = "Error decoding attribute query message";
    193             log.warn(msg, e);
    194             requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, null, msg));
    195             throw new ProfileException(msg);
    196         } catch (SecurityException e) {
    197             String msg = "Message did not meet security requirements";
    198             log.warn(msg, e);
    199             requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, StatusCode.REQUEST_DENIED_URI, msg));
    200             throw new ProfileException(msg, e);
    201         } finally {
    202             // Set as much information as can be retrieved from the decoded message
    203             populateRequestContext(requestContext);
    204         }
    205     }
    206
    207     /** {@inheritDoc} */
    208     protected void populateRelyingPartyInformation(BaseSAMLProfileRequestContext requestContext)
    209             throws ProfileException {
    210         super.populateRelyingPartyInformation(requestContext);
    211
    212         EntityDescriptor relyingPartyMetadata = requestContext.getPeerEntityMetadata();
    213         if (relyingPartyMetadata != null) {
    214             requestContext.setPeerEntityRole(SPSSODescriptor.DEFAULT_ELEMENT_NAME);
    215             requestContext.setPeerEntityRoleMetadata(relyingPartyMetadata.getSPSSODescriptor(SAMLConstants.SAML20P_NS));
    216         }
    217     }
    218
    219     /** {@inheritDoc} */
    220     protected void populateAssertingPartyInformation(BaseSAMLProfileRequestContext requestContext)
    221             throws ProfileException {
    222         super.populateAssertingPartyInformation(requestContext);
    223
    224         EntityDescriptor localEntityDescriptor = requestContext.getLocalEntityMetadata();
    225         if (localEntityDescriptor != null) {
    226             requestContext.setLocalEntityRole(AttributeAuthorityDescriptor.DEFAULT_ELEMENT_NAME);
    227             requestContext.setLocalEntityRoleMetadata(localEntityDescriptor
    228                     .getAttributeAuthorityDescriptor(SAMLConstants.SAML20P_NS));
    229         }
    230     }
    231
    232     /**
    233      * Populates the request context with information from the inbound SAML message.
    234      *
    235      * This method requires the the following request context properties to be populated: inbound saml message
    236      *
    237      * This methods populates the following request context properties: subject name identifier
    238      *
    239      * @param requestContext current request context
    240      *
    241      * @throws ProfileException thrown if the inbound SAML message or subject identifier is null
    242      */
    243     protected void populateSAMLMessageInformation(BaseSAMLProfileRequestContext requestContext) throws ProfileException {
    244         AttributeQuery query = (AttributeQuery) requestContext.getInboundSAMLMessage();
    245         if (query != null) {
    246             Subject subject = query.getSubject();
    247             if (subject == null) {
    248                 String msg = "Attribute query did not contain a proper subject";
    249                 log.warn(msg);
    250                 ((AttributeQueryContext) requestContext).setFailureStatus(buildStatus(StatusCode.REQUESTER_URI, null,
    251                         msg));
    252                 throw new ProfileException(msg);
    253             }
    254             requestContext.setSubjectNameIdentifier(subject.getNameID());
    255         }
    256     }
    257
    258     /**
    259      * Selects the appropriate endpoint for the relying party and stores it in the request context.
    260      *
    261      * @param requestContext current request context
    262      *
    263      * @return Endpoint selected from the information provided in the request context
    264      */
    265     protected Endpoint selectEndpoint(BaseSAMLProfileRequestContext requestContext) {
    266         Endpoint endpoint;
    267
    268         if (getInboundBinding().equals(SAMLConstants.SAML2_SOAP11_BINDING_URI)) {
    269             endpoint = acsEndpointBuilder.buildObject();
    270             endpoint.setBinding(SAMLConstants.SAML2_SOAP11_BINDING_URI);
    271         } else {
    272             BasicEndpointSelector endpointSelector = new BasicEndpointSelector();
    273             endpointSelector.setEndpointType(AssertionConsumerService.DEFAULT_ELEMENT_NAME);
    274             endpointSelector.setMetadataProvider(getMetadataProvider());
    275             endpointSelector.setEntityMetadata(requestContext.getPeerEntityMetadata());
    276             endpointSelector.setEntityRoleMetadata(requestContext.getPeerEntityRoleMetadata());
    277             endpointSelector.setSamlRequest(requestContext.getInboundSAMLMessage());
    278             endpointSelector.getSupportedIssuerBindings().addAll(getSupportedOutboundBindings());
    279             endpoint = endpointSelector.selectEndpoint();
    280         }
    281
    282         return endpoint;
    283     }
    284
    285     /** {@inheritDoc} */
    286     protected NameID buildNameId(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext)
    287         throws ProfileException {
    288
    289         log.debug("Reusing NameID supplied in query");
    290         NameID src = requestContext.getSubjectNameIdentifier();
    291         if (src != null) {
    292             NameID dest = nameIDBuilder.buildObject();
    293             dest.setValue(src.getValue());
    294             dest.setNameQualifier(src.getNameQualifier());
    295             dest.setSPNameQualifier(src.getSPNameQualifier());
    296             dest.setFormat(src.getFormat());
    297             dest.setSPProvidedID(src.getSPProvidedID());
    298
    299             if (dest.getValue() != null) {
    300                 // TODO: this is a hack to satisfy the audit log, but we should fix the
    301                 // context API to handle the NameID value directly
    302                 BasicAttribute<String> attribute = new BasicAttribute<String>();
    303                 attribute.setId("outboundQueryNameID");
    304                 attribute.getValues().add(dest.getValue());
    305                 requestContext.setNameIdentifierAttribute(attribute);
    306             }
    307
    308             return dest;
    309         }
    310         return null;
    311     }
    312
    313
    314     /** Basic data structure used to accumulate information as a request is being processed. */
    315     protected class AttributeQueryContext extends
    316             BaseSAML2ProfileRequestContext<AttributeQuery, Response, AttributeQueryConfiguration> {
    317
    318     }
    319 }