001/* 002 * (C) Copyright 2014 Nuxeo SA (http://nuxeo.com/) and contributors. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the GNU Lesser General Public License 006 * (LGPL) version 2.1 which accompanies this distribution, and is available at 007 * http://www.gnu.org/licenses/lgpl-2.1.html 008 * 009 * This library is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 012 * Lesser General Public License for more details. 013 * 014 * Contributors: 015 * Nelson Silva <nelson.silva@inevo.pt> 016 */ 017package org.nuxeo.ecm.platform.auth.saml; 018 019import org.apache.commons.logging.Log; 020import org.apache.commons.logging.LogFactory; 021import org.joda.time.DateTime; 022import org.nuxeo.ecm.platform.ui.web.auth.NuxeoAuthenticationFilter; 023import org.nuxeo.ecm.platform.web.common.vh.VirtualHostHelper; 024import org.opensaml.Configuration; 025import org.opensaml.common.SAMLException; 026import org.opensaml.common.SAMLObject; 027import org.opensaml.common.binding.SAMLMessageContext; 028import org.opensaml.common.xml.SAMLConstants; 029 030import org.opensaml.saml2.core.Assertion; 031import org.opensaml.saml2.core.AuthnRequest; 032import org.opensaml.saml2.core.Conditions; 033import org.opensaml.saml2.core.Issuer; 034import org.opensaml.saml2.core.NameIDType; 035import org.opensaml.saml2.core.Response; 036import org.opensaml.saml2.encryption.Decrypter; 037import org.opensaml.saml2.metadata.AssertionConsumerService; 038import org.opensaml.saml2.metadata.Endpoint; 039import org.opensaml.saml2.metadata.IDPSSODescriptor; 040import org.opensaml.security.MetadataCriteria; 041import org.opensaml.security.SAMLSignatureProfileValidator; 042import org.opensaml.xml.XMLObjectBuilderFactory; 043import org.opensaml.xml.encryption.DecryptionException; 044import org.opensaml.xml.security.CriteriaSet; 045import org.opensaml.xml.security.SecurityException; 046import org.opensaml.xml.security.credential.UsageType; 047import org.opensaml.xml.security.criteria.EntityIDCriteria; 048import org.opensaml.xml.security.criteria.UsageCriteria; 049import org.opensaml.xml.signature.Signature; 050import org.opensaml.xml.signature.SignatureTrustEngine; 051import org.opensaml.xml.validation.ValidationException; 052 053import javax.servlet.ServletRequest; 054import javax.xml.namespace.QName; 055import java.util.Date; 056import java.util.UUID; 057 058/** 059 * Base abstract class for SAML profile processors. 060 * 061 * @since 6.0 062 */ 063public abstract class AbstractSAMLProfile { 064 protected final static Log log = LogFactory.getLog(AbstractSAMLProfile.class); 065 066 protected final XMLObjectBuilderFactory builderFactory; 067 068 private final Endpoint endpoint; 069 070 private SignatureTrustEngine trustEngine; 071 072 private Decrypter decrypter; 073 074 public AbstractSAMLProfile(Endpoint endpoint) { 075 this.endpoint = endpoint; 076 this.builderFactory = Configuration.getBuilderFactory(); 077 } 078 079 /** 080 * @return the profile identifier (Uri). 081 */ 082 abstract public String getProfileIdentifier(); 083 084 protected <T extends SAMLObject> T build(QName qName) { 085 return (T) builderFactory.getBuilder(qName).buildObject(qName); 086 } 087 088 // VALIDATION 089 090 protected void validateSignature(Signature signature, String IDPEntityID) throws ValidationException, 091 org.opensaml.xml.security.SecurityException { 092 093 if (trustEngine == null) { 094 throw new SecurityException("Trust engine is not set, signature can't be verified"); 095 } 096 097 SAMLSignatureProfileValidator validator = new SAMLSignatureProfileValidator(); 098 validator.validate(signature); 099 CriteriaSet criteriaSet = new CriteriaSet(); 100 criteriaSet.add(new EntityIDCriteria(IDPEntityID)); 101 criteriaSet.add(new MetadataCriteria(IDPSSODescriptor.DEFAULT_ELEMENT_NAME, SAMLConstants.SAML20P_NS)); 102 criteriaSet.add(new UsageCriteria(UsageType.SIGNING)); 103 log.debug("Verifying signature: " + signature); 104 105 if (!getTrustEngine().validate(signature, criteriaSet)) { 106 throw new ValidationException("Signature is not trusted or invalid"); 107 } 108 } 109 110 protected void validateIssuer(Issuer issuer, SAMLMessageContext context) throws SAMLException { 111 // Validate format of issuer 112 if (issuer.getFormat() != null && !issuer.getFormat().equals(NameIDType.ENTITY)) { 113 throw new SAMLException("Assertion invalidated by issuer type"); 114 } 115 // Validate that issuer is expected peer entity 116 if (!context.getPeerEntityMetadata().getEntityID().equals(issuer.getValue())) { 117 throw new SAMLException("Assertion invalidated by unexpected issuer value"); 118 } 119 } 120 121 protected void validateEndpoint(Response response, Endpoint endpoint) throws SAMLException { 122 // Verify that destination in the response matches one of the available endpoints 123 String destination = response.getDestination(); 124 125 if (destination != null) { 126 if (destination.equals(endpoint.getLocation())) { 127 } else if (destination.equals(endpoint.getResponseLocation())) { 128 } else { 129 log.debug("Intended destination " + destination + " doesn't match any of the endpoint URLs"); 130 throw new SAMLException("Intended destination " + destination 131 + " doesn't match any of the endpoint URLs"); 132 } 133 } 134 135 // Verify response to field if present, set request if correct 136 AuthnRequest request = retrieveRequest(response); 137 138 // Verify endpoint requested in the original request 139 if (request != null) { 140 AssertionConsumerService assertionConsumerService = (AssertionConsumerService) endpoint; 141 if (request.getAssertionConsumerServiceIndex() != null) { 142 if (!request.getAssertionConsumerServiceIndex().equals(assertionConsumerService.getIndex())) { 143 log.info("SAML response was received at a different endpoint " + "index than was requested"); 144 } 145 } else { 146 String requestedResponseURL = request.getAssertionConsumerServiceURL(); 147 String requestedBinding = request.getProtocolBinding(); 148 if (requestedResponseURL != null) { 149 String responseLocation; 150 if (assertionConsumerService.getResponseLocation() != null) { 151 responseLocation = assertionConsumerService.getResponseLocation(); 152 } else { 153 responseLocation = assertionConsumerService.getLocation(); 154 } 155 if (!requestedResponseURL.equals(responseLocation)) { 156 log.info("SAML response was received at a different endpoint URL " + responseLocation 157 + " than was requested " + requestedResponseURL); 158 } 159 } 160 /* 161 * if (requestedBinding != null) { if (!requestedBinding.equals(context.getInboundSAMLBinding())) { 162 * log.info("SAML response was received using a different binding {} than was requested {}", 163 * context.getInboundSAMLBinding(), requestedBinding); } } 164 */ 165 } 166 } 167 } 168 169 protected void validateAssertion(Assertion assertion, SAMLMessageContext context) throws SAMLException, 170 org.opensaml.xml.security.SecurityException, ValidationException, DecryptionException { 171 172 validateIssuer(assertion.getIssuer(), context); 173 174 Conditions conditions = assertion.getConditions(); 175 176 // validate conditions timestamps: notBefore, notOnOrAfter 177 Date now = new DateTime().toDate(); 178 Date condition_notBefore = null; 179 Date condition_NotOnOrAfter = null; 180 if (conditions.getNotBefore() != null) { 181 condition_notBefore = conditions.getNotBefore().toDate(); 182 } 183 if (conditions.getNotOnOrAfter() != null) { 184 condition_NotOnOrAfter = conditions.getNotOnOrAfter().toDate(); 185 } 186 if (condition_notBefore != null && now.before(condition_notBefore)) { 187 log.debug("Current time: [" + now + "] NotBefore: [" + condition_notBefore + "]"); 188 throw new SAMLException("Conditions are not yet active"); 189 } else if (condition_NotOnOrAfter != null 190 && (now.after(condition_NotOnOrAfter) || now.equals(condition_NotOnOrAfter))) { 191 log.debug("Current time: [" + now + "] NotOnOrAfter: [" + condition_NotOnOrAfter + "]"); 192 throw new SAMLException("Conditions have expired"); 193 } 194 195 Signature signature = assertion.getSignature(); 196 197 if (signature != null) { 198 validateSignature(signature, context.getPeerEntityMetadata().getEntityID()); 199 } 200 201 // TODO(nfgs) : Check subject 202 } 203 204 protected AuthnRequest retrieveRequest(Response response) throws SAMLException { 205 // TODO(nfgs) - Store SAML messages to validate response.getInResponseTo() 206 return null; 207 } 208 209 public Endpoint getEndpoint() { 210 return endpoint; 211 } 212 213 public SignatureTrustEngine getTrustEngine() { 214 return trustEngine; 215 } 216 217 public void setTrustEngine(SignatureTrustEngine trustEngine) { 218 this.trustEngine = trustEngine; 219 } 220 221 public Decrypter getDecrypter() { 222 return decrypter; 223 } 224 225 public void setDecrypter(Decrypter decrypter) { 226 this.decrypter = decrypter; 227 } 228 229 protected String newUUID() { 230 return UUID.randomUUID().toString(); 231 } 232 233 protected String getBaseURL(ServletRequest request) { 234 return VirtualHostHelper.getBaseURL(request); 235 } 236 237 protected String getStartPageURL(ServletRequest request) { 238 return getBaseURL(request) + NuxeoAuthenticationFilter.DEFAULT_START_PAGE; 239 } 240}