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.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}