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}