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 <[email protected]> 018 */ 019package org.nuxeo.ecm.platform.auth.saml; 020 021import java.util.UUID; 022 023import javax.servlet.ServletRequest; 024import javax.xml.namespace.QName; 025 026import org.apache.commons.logging.Log; 027import org.apache.commons.logging.LogFactory; 028import org.joda.time.DateTime; 029import org.nuxeo.ecm.platform.ui.web.auth.LoginScreenHelper; 030import org.nuxeo.ecm.platform.web.common.vh.VirtualHostHelper; 031import org.opensaml.Configuration; 032import org.opensaml.common.SAMLException; 033import org.opensaml.common.SAMLObject; 034import org.opensaml.common.binding.SAMLMessageContext; 035import org.opensaml.common.xml.SAMLConstants; 036import org.opensaml.saml2.core.Assertion; 037import org.opensaml.saml2.core.AuthnRequest; 038import org.opensaml.saml2.core.Conditions; 039import org.opensaml.saml2.core.Issuer; 040import org.opensaml.saml2.core.NameIDType; 041import org.opensaml.saml2.core.Response; 042import org.opensaml.saml2.encryption.Decrypter; 043import org.opensaml.saml2.metadata.AssertionConsumerService; 044import org.opensaml.saml2.metadata.Endpoint; 045import org.opensaml.saml2.metadata.IDPSSODescriptor; 046import org.opensaml.security.MetadataCriteria; 047import org.opensaml.security.SAMLSignatureProfileValidator; 048import org.opensaml.xml.XMLObjectBuilderFactory; 049import org.opensaml.xml.security.CriteriaSet; 050import org.opensaml.xml.security.SecurityException; 051import org.opensaml.xml.security.credential.UsageType; 052import org.opensaml.xml.security.criteria.EntityIDCriteria; 053import org.opensaml.xml.security.criteria.UsageCriteria; 054import org.opensaml.xml.signature.Signature; 055import org.opensaml.xml.signature.SignatureTrustEngine; 056import org.opensaml.xml.validation.ValidationException; 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 private int skewTimeMillis; 075 076 public AbstractSAMLProfile(Endpoint endpoint) { 077 this.endpoint = endpoint; 078 this.builderFactory = Configuration.getBuilderFactory(); 079 this.skewTimeMillis = SAMLConfiguration.getSkewTimeMillis(); 080 } 081 082 /** 083 * @return the profile identifier (Uri). 084 */ 085 abstract public String getProfileIdentifier(); 086 087 protected <T extends SAMLObject> T build(QName qName) { 088 return (T) builderFactory.getBuilder(qName).buildObject(qName); 089 } 090 091 // VALIDATION 092 093 protected void validateSignature(Signature signature, String IDPEntityID) throws SAMLException { 094 095 if (trustEngine == null) { 096 throw new SAMLException("Trust engine is not set, signature can't be verified"); 097 } 098 099 try { 100 SAMLSignatureProfileValidator validator = new SAMLSignatureProfileValidator(); 101 validator.validate(signature); 102 CriteriaSet criteriaSet = new CriteriaSet(); 103 criteriaSet.add(new EntityIDCriteria(IDPEntityID)); 104 criteriaSet.add(new MetadataCriteria(IDPSSODescriptor.DEFAULT_ELEMENT_NAME, SAMLConstants.SAML20P_NS)); 105 criteriaSet.add(new UsageCriteria(UsageType.SIGNING)); 106 log.debug("Verifying signature: " + signature); 107 108 if (!getTrustEngine().validate(signature, criteriaSet)) { 109 throw new SAMLException("Signature is not trusted or invalid"); 110 } 111 } catch (ValidationException | SecurityException e) { 112 throw new SAMLException("Error validating signature", e); 113 } 114 115 } 116 117 protected void validateIssuer(Issuer issuer, SAMLMessageContext context) throws SAMLException { 118 // Validate format of issuer 119 if (issuer.getFormat() != null && !issuer.getFormat().equals(NameIDType.ENTITY)) { 120 throw new SAMLException("Assertion invalidated by issuer type"); 121 } 122 // Validate that issuer is expected peer entity 123 if (context.getPeerEntityMetadata() != null 124 && !context.getPeerEntityMetadata().getEntityID().equals(issuer.getValue())) { 125 throw new SAMLException("Assertion invalidated by unexpected issuer value"); 126 } 127 } 128 129 protected void validateEndpoint(Response response, Endpoint endpoint) throws SAMLException { 130 // Verify that destination in the response matches one of the available endpoints 131 String destination = response.getDestination(); 132 133 if (destination != null) { 134 if (destination.equals(endpoint.getLocation())) { 135 } else if (destination.equals(endpoint.getResponseLocation())) { 136 } else { 137 log.debug("Intended destination " + destination + " doesn't match any of the endpoint URLs"); 138 throw new SAMLException( 139 "Intended destination " + destination + " doesn't match any of the endpoint URLs"); 140 } 141 } 142 143 // Verify response to field if present, set request if correct 144 AuthnRequest request = retrieveRequest(response); 145 146 // Verify endpoint requested in the original request 147 if (request != null) { 148 AssertionConsumerService assertionConsumerService = (AssertionConsumerService) endpoint; 149 if (request.getAssertionConsumerServiceIndex() != null) { 150 if (!request.getAssertionConsumerServiceIndex().equals(assertionConsumerService.getIndex())) { 151 log.info("SAML response was received at a different endpoint " + "index than was requested"); 152 } 153 } else { 154 String requestedResponseURL = request.getAssertionConsumerServiceURL(); 155 String requestedBinding = request.getProtocolBinding(); 156 if (requestedResponseURL != null) { 157 String responseLocation; 158 if (assertionConsumerService.getResponseLocation() != null) { 159 responseLocation = assertionConsumerService.getResponseLocation(); 160 } else { 161 responseLocation = assertionConsumerService.getLocation(); 162 } 163 if (!requestedResponseURL.equals(responseLocation)) { 164 log.info("SAML response was received at a different endpoint URL " + responseLocation 165 + " than was requested " + requestedResponseURL); 166 } 167 } 168 /* 169 * if (requestedBinding != null) { if (!requestedBinding.equals(context.getInboundSAMLBinding())) { 170 * log.info("SAML response was received using a different binding {} than was requested {}", 171 * context.getInboundSAMLBinding(), requestedBinding); } } 172 */ 173 } 174 } 175 } 176 177 protected void validateAssertion(Assertion assertion, SAMLMessageContext context) throws SAMLException { 178 179 validateIssuer(assertion.getIssuer(), context); 180 181 Conditions conditions = assertion.getConditions(); 182 183 // validate conditions timestamps: notBefore, notOnOrAfter 184 DateTime now = new DateTime(); 185 DateTime notBefore = conditions.getNotBefore(); 186 DateTime notOnOrAfter = conditions.getNotOnOrAfter(); 187 188 if (notBefore != null && notBefore.minusMillis(getSkewTimeMillis()).isAfterNow()) { 189 log.debug("Current time: [" + now + "] NotBefore: [" + notBefore + "]"); 190 throw new SAMLException("Conditions are not yet active"); 191 } else if (notOnOrAfter != null && notOnOrAfter.plusMillis(getSkewTimeMillis()).isBeforeNow()) { 192 log.debug("Current time: [" + now + "] NotOnOrAfter: [" + notOnOrAfter + "]"); 193 throw new SAMLException("Conditions have expired"); 194 } 195 196 Signature signature = assertion.getSignature(); 197 198 if (signature != null) { 199 validateSignature(signature, context.getPeerEntityMetadata().getEntityID()); 200 } 201 202 // TODO(nfgs) : Check subject 203 } 204 205 protected AuthnRequest retrieveRequest(Response response) throws SAMLException { 206 // TODO(nfgs) - Store SAML messages to validate response.getInResponseTo() 207 return null; 208 } 209 210 public Endpoint getEndpoint() { 211 return endpoint; 212 } 213 214 public SignatureTrustEngine getTrustEngine() { 215 return trustEngine; 216 } 217 218 public void setTrustEngine(SignatureTrustEngine trustEngine) { 219 this.trustEngine = trustEngine; 220 } 221 222 public Decrypter getDecrypter() { 223 return decrypter; 224 } 225 226 public void setDecrypter(Decrypter decrypter) { 227 this.decrypter = decrypter; 228 } 229 230 public int getSkewTimeMillis() { 231 return skewTimeMillis; 232 } 233 234 public void setSkewTimeMillis(int skewTimeMillis) { 235 this.skewTimeMillis = skewTimeMillis; 236 } 237 238 protected String newUUID() { 239 return "_" + UUID.randomUUID().toString(); 240 } 241 242 protected String getBaseURL(ServletRequest request) { 243 return VirtualHostHelper.getBaseURL(request); 244 } 245 246 protected String getStartPageURL(ServletRequest request) { 247 return getBaseURL(request) + LoginScreenHelper.getStartupPagePath(); 248 } 249}