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}