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