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}