2 * Copyright [2005] [University Corporation for Advanced Internet Development, Inc.]
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package edu.internet2.middleware.shibboleth.idp.provider;
19 import java.io.IOException;
21 import java.net.URISyntaxException;
22 import java.security.Principal;
23 import java.security.cert.X509Certificate;
24 import java.util.ArrayList;
25 import java.util.Arrays;
26 import java.util.Collections;
27 import java.util.Date;
28 import java.util.Iterator;
30 import javax.security.auth.x500.X500Principal;
31 import javax.servlet.ServletException;
32 import javax.servlet.http.HttpServletRequest;
33 import javax.servlet.http.HttpServletResponse;
35 import org.apache.log4j.Logger;
36 import org.opensaml.SAMLAssertion;
37 import org.opensaml.SAMLAttribute;
38 import org.opensaml.SAMLAttributeDesignator;
39 import org.opensaml.SAMLAttributeQuery;
40 import org.opensaml.SAMLAttributeStatement;
41 import org.opensaml.SAMLAudienceRestrictionCondition;
42 import org.opensaml.SAMLCondition;
43 import org.opensaml.SAMLException;
44 import org.opensaml.SAMLNameIdentifier;
45 import org.opensaml.SAMLRequest;
46 import org.opensaml.SAMLResponse;
47 import org.opensaml.SAMLStatement;
48 import org.opensaml.SAMLSubject;
49 import org.opensaml.XML;
50 import org.w3c.dom.Element;
52 import edu.internet2.middleware.shibboleth.aa.AAException;
53 import edu.internet2.middleware.shibboleth.common.InvalidNameIdentifierException;
54 import edu.internet2.middleware.shibboleth.common.NameIdentifierMapping;
55 import edu.internet2.middleware.shibboleth.common.NameIdentifierMappingException;
56 import edu.internet2.middleware.shibboleth.common.RelyingParty;
57 import edu.internet2.middleware.shibboleth.common.ShibbolethConfigurationException;
58 import edu.internet2.middleware.shibboleth.idp.IdPProtocolHandler;
59 import edu.internet2.middleware.shibboleth.idp.IdPProtocolSupport;
60 import edu.internet2.middleware.shibboleth.metadata.AttributeRequesterDescriptor;
61 import edu.internet2.middleware.shibboleth.metadata.EntityDescriptor;
62 import edu.internet2.middleware.shibboleth.metadata.RoleDescriptor;
63 import edu.internet2.middleware.shibboleth.metadata.SPSSODescriptor;
66 * @author Walter Hoehn
68 public class SAMLv1_AttributeQueryHandler extends BaseServiceHandler implements IdPProtocolHandler {
70 private static Logger log = Logger.getLogger(SAMLv1_AttributeQueryHandler.class.getName());
73 * Required DOM-based constructor.
75 public SAMLv1_AttributeQueryHandler(Element config) throws ShibbolethConfigurationException {
81 * @see edu.internet2.middleware.shibboleth.idp.ProtocolHandler#getHandlerName()
83 public String getHandlerName() {
85 return "SAML v1.1 Attribute Query";
88 private String authenticateAs(String assertedId, X509Certificate[] chain, IdPProtocolSupport support)
89 throws InvalidProviderCredentialException {
90 // See if we have metadata for this provider
91 EntityDescriptor provider = support.lookup(assertedId);
92 if (provider == null) {
93 log.info("No metadata found for providerId: (" + assertedId + ").");
97 log.info("Metadata found for providerId: (" + assertedId + ").");
99 RoleDescriptor ar_role = provider.getAttributeRequesterDescriptor(XML.SAML11_PROTOCOL_ENUM);
100 RoleDescriptor sp_role = provider.getSPSSODescriptor(XML.SAML11_PROTOCOL_ENUM);
101 if (ar_role == null && sp_role == null) {
102 log.info("SPSSO and Stand-Alone Requester roles not found in metadata for provider: ("
103 + assertedId + ").");
107 // Make sure that the supplied credential is valid for the selected provider role.
108 if ((ar_role != null && support.getTrust().validate(chain[0], chain, ar_role)) ||
109 (sp_role != null && support.getTrust().validate(chain[0], chain, sp_role))) {
110 log.info("Supplied credentials validated for this provider.");
113 log.error("Supplied credentials ("
114 + chain[0].getSubjectX500Principal().getName(X500Principal.RFC2253)
115 + ") are NOT valid for provider (" + assertedId + ").");
116 throw new InvalidProviderCredentialException("Invalid credentials.");
121 * @see edu.internet2.middleware.shibboleth.idp.ProtocolHandler#processRequest(javax.servlet.http.HttpServletRequest,
122 * javax.servlet.http.HttpServletResponse, org.opensaml.SAMLRequest,
123 * edu.internet2.middleware.shibboleth.idp.ProtocolSupport)
125 public SAMLResponse processRequest(HttpServletRequest request, HttpServletResponse response,
126 SAMLRequest samlRequest, IdPProtocolSupport support) throws SAMLException, IOException, ServletException {
128 if (samlRequest == null || samlRequest.getQuery() == null || !(samlRequest.getQuery() instanceof SAMLAttributeQuery)) {
129 log.error("Protocol Handler can only respond to SAML Attribute Queries.");
130 throw new SAMLException("General error processing request.");
133 RelyingParty relyingParty = null;
134 SAMLAttributeQuery attributeQuery = (SAMLAttributeQuery) samlRequest.getQuery();
136 // This is the requester name that will be passed to subsystems
137 String effectiveName = null;
139 // Log the physical credential supplied, if any.
140 X509Certificate[] credentials = (X509Certificate[]) request.getAttribute("javax.servlet.request.X509Certificate");
141 if (credentials == null || credentials.length == 0 ||
142 credentials[0].getSubjectX500Principal().getName(X500Principal.RFC2253).equals("")) {
143 log.info("Request contained no credentials, treating as an unauthenticated service provider.");
146 log.info("Request contains credentials: ("
147 + credentials[0].getSubjectX500Principal().getName(X500Principal.RFC2253) + ").");
149 // Try and authenticate the requester as any of the potentially relevant identifiers we know.
151 if (attributeQuery.getResource() != null) {
152 log.info("Remote provider has identified itself as: (" + attributeQuery.getResource() + ").");
153 effectiveName = authenticateAs(attributeQuery.getResource(), credentials, support);
156 if (effectiveName == null) {
157 log.info("Remote provider not yet identified, attempting to derive requesting provider from credentials.");
159 // Try the additional candidates.
160 String[] candidateNames = getCredentialNames(credentials[0]);
161 for (int c = 0; effectiveName == null && c < candidateNames.length; c++) {
162 effectiveName = authenticateAs(candidateNames[c], credentials, support);
165 } catch (InvalidProviderCredentialException ipc) {
166 throw new SAMLException(SAMLException.REQUESTER, "Invalid credentials for request.");
170 if (effectiveName == null) {
171 log.info("Unable to locate metadata about provider, treating as an unauthenticated service provider.");
172 relyingParty = support.getServiceProviderMapper().getRelyingParty(null);
173 if(log.isDebugEnabled()) {
174 log.debug("Using default Relying Party, " + relyingParty.getName() + " for unauthenticated provider.");
178 // Identify a Relying Party
179 log.debug("Mapping authenticated provider (" + effectiveName + ") to Relying Party.");
180 relyingParty = support.getServiceProviderMapper().getRelyingParty(effectiveName);
183 // Fail if we can't honor SAML Subject Confirmation unless the only one supplied is
184 // bearer, in which case this is probably a Shib 1.1 query, and we'll let it slide for now.
185 // TODO: remove the compatibility with 1.1 and be strict about this?
186 boolean hasConfirmationMethod = false;
187 boolean hasOnlyBearer = true;
188 Iterator iterator = attributeQuery.getSubject().getConfirmationMethods();
189 while (iterator.hasNext()) {
190 String method = (String) iterator.next();
191 log.info("Request contains SAML Subject Confirmation method: (" + method + ").");
192 hasConfirmationMethod = true;
193 if (!method.equals(SAMLSubject.CONF_BEARER))
194 hasOnlyBearer = false;
196 if (hasConfirmationMethod && !hasOnlyBearer) {
197 throw new SAMLException(SAMLException.REQUESTER,
198 "This SAML authority cannot honor requests containing the supplied SAML Subject Confirmation Method(s).");
201 // Map Subject to local principal
202 Principal principal = null;
204 SAMLNameIdentifier nameId = attributeQuery.getSubject().getNameIdentifier();
205 log.debug("Name Identifier format: (" + nameId.getFormat() + ").");
206 NameIdentifierMapping mapping = null;
208 mapping = support.getNameMapper().getNameIdentifierMapping(new URI(nameId.getFormat()));
209 } catch (URISyntaxException e) {
210 log.error("Invalid Name Identifier format.");
212 if (mapping == null) { throw new NameIdentifierMappingException("Name Identifier format not registered."); }
214 // Don't honor the request if the active relying party configuration does not contain a mapping with the
215 // name identifier format from the request
216 if (!Arrays.asList(relyingParty.getNameMapperIds()).contains(mapping.getId())) { throw new NameIdentifierMappingException(
217 "Name Identifier format not valid for this relying party."); }
219 principal = mapping.getPrincipal(nameId, relyingParty, relyingParty.getIdentityProvider());
220 log.info("Request is for principal (" + principal.getName() + ").");
222 // Get attributes from resolver
223 SAMLAttribute[] attrs;
224 Iterator requestedAttrsIterator = attributeQuery.getDesignators();
225 if (requestedAttrsIterator.hasNext()) {
226 log.info("Request designates specific attributes, resolving this set.");
227 ArrayList requestedAttrs = new ArrayList();
228 while (requestedAttrsIterator.hasNext()) {
229 SAMLAttributeDesignator attribute = (SAMLAttributeDesignator) requestedAttrsIterator.next();
231 log.debug("Designated attribute: (" + attribute.getName() + ")");
232 requestedAttrs.add(new URI(attribute.getName()));
233 } catch (URISyntaxException use) {
234 log.error("Request designated an attribute name that does not conform "
235 + "to the required URI syntax (" + attribute.getName() + "). Ignoring this attribute");
239 attrs = support.getReleaseAttributes(principal, relyingParty, effectiveName, null,
240 (URI[]) requestedAttrs.toArray(new URI[0]));
242 log.info("Request does not designate specific attributes, resolving all available.");
243 attrs = support.getReleaseAttributes(principal, relyingParty, effectiveName, null);
246 log.info("Found " + attrs.length + " attribute(s) for " + principal.getName());
248 // Put attributes names in the transaction log when it is set to DEBUG
249 if (support.getTransactionLog().isDebugEnabled() && attrs.length > 0) {
250 StringBuffer attrNameBuffer = new StringBuffer();
251 for (int i = 0; i < attrs.length; i++) {
252 attrNameBuffer.append("(" + attrs[i].getName() + ")");
254 support.getTransactionLog()
256 "Attribute assertion generated for provider (" + effectiveName
257 + ") on behalf of principal (" + principal.getName()
258 + ") with the following attributes: " + attrNameBuffer.toString());
261 SAMLResponse samlResponse = null;
263 if (attrs == null || attrs.length == 0) {
264 // No attribute found
265 samlResponse = new SAMLResponse(samlRequest.getId(), null, null, null);
268 // Reference requested subject
269 SAMLSubject rSubject = (SAMLSubject) attributeQuery.getSubject().clone();
271 ArrayList audiences = new ArrayList();
272 if (relyingParty.getProviderId() != null) {
273 audiences.add(relyingParty.getProviderId());
275 if (relyingParty.getName() != null && !relyingParty.getName().equals(relyingParty.getProviderId())) {
276 audiences.add(relyingParty.getName());
279 SAMLCondition condition = new SAMLAudienceRestrictionCondition(audiences);
281 // Put all attributes into an assertion
282 SAMLStatement statement = new SAMLAttributeStatement(rSubject, Arrays.asList(attrs));
284 // Set assertion expiration to longest attribute expiration
286 for (int i = 0; i < attrs.length; i++) {
287 if (max < attrs[i].getLifetime()) {
288 max = attrs[i].getLifetime();
291 Date now = new Date();
292 Date then = new Date(now.getTime() + (max * 1000)); // max is in
295 SAMLAssertion sAssertion = new SAMLAssertion(relyingParty.getIdentityProvider().getProviderId(), now,
296 then, Collections.singleton(condition), null, Collections.singleton(statement));
298 // Sign the assertions, if necessary
299 boolean metaDataIndicatesSignAssertions = false;
300 EntityDescriptor descriptor = support.lookup(relyingParty.getProviderId());
301 if (descriptor != null) {
302 AttributeRequesterDescriptor ar = descriptor
303 .getAttributeRequesterDescriptor(org.opensaml.XML.SAML11_PROTOCOL_ENUM);
305 if (ar.getWantAssertionsSigned()) {
306 metaDataIndicatesSignAssertions = true;
309 if (!metaDataIndicatesSignAssertions) {
310 SPSSODescriptor sp = descriptor.getSPSSODescriptor(org.opensaml.XML.SAML11_PROTOCOL_ENUM);
312 if (sp.getWantAssertionsSigned()) {
313 metaDataIndicatesSignAssertions = true;
318 if (relyingParty.wantsAssertionsSigned() || metaDataIndicatesSignAssertions) {
319 support.signAssertions(new SAMLAssertion[]{sAssertion}, relyingParty);
322 samlResponse = new SAMLResponse(samlRequest.getId(), null, Collections.singleton(sAssertion), null);
325 if (log.isDebugEnabled()) { // This takes some processing, so only do it if we need to
326 log.debug("Dumping generated SAML Response:" + System.getProperty("line.separator")
327 + samlResponse.toString());
330 log.info("Successfully created response for principal (" + principal.getName() + ").");
332 if (effectiveName == null) {
333 support.getTransactionLog().info(
334 "Attribute assertion issued to anonymous provider at (" + request.getRemoteAddr()
335 + ") on behalf of principal (" + principal.getName() + ").");
337 support.getTransactionLog().info(
338 "Attribute assertion issued to provider (" + effectiveName + ") on behalf of principal ("
339 + principal.getName() + ").");
344 } catch (SAMLException e) {
345 if (relyingParty.passThruErrors()) {
346 throw new SAMLException("General error processing request.", e);
348 throw new SAMLException("General error processing request.");
351 } catch (InvalidNameIdentifierException e) {
352 log.error("Could not associate the request's subject with a principal: " + e);
353 if (relyingParty.passThruErrors()) {
354 throw new SAMLException(Arrays.asList(e.getSAMLErrorCodes()), "The supplied Subject was unrecognized.",
357 throw new SAMLException(Arrays.asList(e.getSAMLErrorCodes()), "The supplied Subject was unrecognized.");
360 } catch (NameIdentifierMappingException e) {
361 log.error("Encountered an error while mapping the name identifier from the request: " + e);
362 if (relyingParty.passThruErrors()) {
363 throw new SAMLException("General error processing request.", e);
365 throw new SAMLException("General error processing request.");
368 } catch (AAException e) {
369 log.error("Encountered an error while resolving resolving attributes: " + e);
370 if (relyingParty.passThruErrors()) {
371 throw new SAMLException("General error processing request.", e);
373 throw new SAMLException("General error processing request.");
376 } catch (CloneNotSupportedException e) {
377 log.error("Encountered an error while cloning request subject for use in response: " + e);
378 if (relyingParty.passThruErrors()) {
379 throw new SAMLException("General error processing request.", e);
381 throw new SAMLException("General error processing request.");