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) 093 throws ValidationException, org.opensaml.xml.security.SecurityException { 094 095 if (trustEngine == null) { 096 throw new SecurityException("Trust engine is not set, signature can't be verified"); 097 } 098 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 ValidationException("Signature is not trusted or invalid"); 109 } 110 } 111 112 protected void validateIssuer(Issuer issuer, SAMLMessageContext context) throws SAMLException { 113 // Validate format of issuer 114 if (issuer.getFormat() != null && !issuer.getFormat().equals(NameIDType.ENTITY)) { 115 throw new SAMLException("Assertion invalidated by issuer type"); 116 } 117 // Validate that issuer is expected peer entity 118 if (!context.getPeerEntityMetadata().getEntityID().equals(issuer.getValue())) { 119 throw new SAMLException("Assertion invalidated by unexpected issuer value"); 120 } 121 } 122 123 protected void validateEndpoint(Response response, Endpoint endpoint) throws SAMLException { 124 // Verify that destination in the response matches one of the available endpoints 125 String destination = response.getDestination(); 126 127 if (destination != null) { 128 if (destination.equals(endpoint.getLocation())) { 129 } else if (destination.equals(endpoint.getResponseLocation())) { 130 } else { 131 log.debug("Intended destination " + destination + " doesn't match any of the endpoint URLs"); 132 throw new SAMLException( 133 "Intended destination " + destination + " doesn't match any of the endpoint URLs"); 134 } 135 } 136 137 // Verify response to field if present, set request if correct 138 AuthnRequest request = retrieveRequest(response); 139 140 // Verify endpoint requested in the original request 141 if (request != null) { 142 AssertionConsumerService assertionConsumerService = (AssertionConsumerService) endpoint; 143 if (request.getAssertionConsumerServiceIndex() != null) { 144 if (!request.getAssertionConsumerServiceIndex().equals(assertionConsumerService.getIndex())) { 145 log.info("SAML response was received at a different endpoint " + "index than was requested"); 146 } 147 } else { 148 String requestedResponseURL = request.getAssertionConsumerServiceURL(); 149 String requestedBinding = request.getProtocolBinding(); 150 if (requestedResponseURL != null) { 151 String responseLocation; 152 if (assertionConsumerService.getResponseLocation() != null) { 153 responseLocation = assertionConsumerService.getResponseLocation(); 154 } else { 155 responseLocation = assertionConsumerService.getLocation(); 156 } 157 if (!requestedResponseURL.equals(responseLocation)) { 158 log.info("SAML response was received at a different endpoint URL " + responseLocation 159 + " than was requested " + requestedResponseURL); 160 } 161 } 162 /* 163 * if (requestedBinding != null) { if (!requestedBinding.equals(context.getInboundSAMLBinding())) { 164 * log.info("SAML response was received using a different binding {} than was requested {}", 165 * context.getInboundSAMLBinding(), requestedBinding); } } 166 */ 167 } 168 } 169 } 170 171 protected void validateAssertion(Assertion assertion, SAMLMessageContext context) throws SAMLException, 172 org.opensaml.xml.security.SecurityException, ValidationException, DecryptionException { 173 174 validateIssuer(assertion.getIssuer(), context); 175 176 Conditions conditions = assertion.getConditions(); 177 178 // validate conditions timestamps: notBefore, notOnOrAfter 179 Date now = new DateTime().toDate(); 180 Date condition_notBefore = null; 181 Date condition_NotOnOrAfter = null; 182 if (conditions.getNotBefore() != null) { 183 condition_notBefore = conditions.getNotBefore().toDate(); 184 } 185 if (conditions.getNotOnOrAfter() != null) { 186 condition_NotOnOrAfter = conditions.getNotOnOrAfter().toDate(); 187 } 188 if (condition_notBefore != null && now.before(condition_notBefore)) { 189 log.debug("Current time: [" + now + "] NotBefore: [" + condition_notBefore + "]"); 190 throw new SAMLException("Conditions are not yet active"); 191 } else if (condition_NotOnOrAfter != null 192 && (now.after(condition_NotOnOrAfter) || now.equals(condition_NotOnOrAfter))) { 193 log.debug("Current time: [" + now + "] NotOnOrAfter: [" + condition_NotOnOrAfter + "]"); 194 throw new SAMLException("Conditions have expired"); 195 } 196 197 Signature signature = assertion.getSignature(); 198 199 if (signature != null) { 200 validateSignature(signature, context.getPeerEntityMetadata().getEntityID()); 201 } 202 203 // TODO(nfgs) : Check subject 204 } 205 206 protected AuthnRequest retrieveRequest(Response response) throws SAMLException { 207 // TODO(nfgs) - Store SAML messages to validate response.getInResponseTo() 208 return null; 209 } 210 211 public Endpoint getEndpoint() { 212 return endpoint; 213 } 214 215 public SignatureTrustEngine getTrustEngine() { 216 return trustEngine; 217 } 218 219 public void setTrustEngine(SignatureTrustEngine trustEngine) { 220 this.trustEngine = trustEngine; 221 } 222 223 public Decrypter getDecrypter() { 224 return decrypter; 225 } 226 227 public void setDecrypter(Decrypter decrypter) { 228 this.decrypter = decrypter; 229 } 230 231 protected String newUUID() { 232 return "_" + UUID.randomUUID().toString(); 233 } 234 235 protected String getBaseURL(ServletRequest request) { 236 return VirtualHostHelper.getBaseURL(request); 237 } 238 239 protected String getStartPageURL(ServletRequest request) { 240 return getBaseURL(request) + LoginScreenHelper.getStartupPagePath(); 241 } 242}