org.nuxeo.ecm.platform.auth.saml.AbstractSAMLProfile.java Source code

Java tutorial

Introduction

Here is the source code for org.nuxeo.ecm.platform.auth.saml.AbstractSAMLProfile.java

Source

/*
 * (C) Copyright 2014 Nuxeo SA (http://nuxeo.com/) and others.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * Contributors:
 *     Nelson Silva <nelson.silva@inevo.pt>
 */
package org.nuxeo.ecm.platform.auth.saml;

import java.util.Date;
import java.util.UUID;

import javax.servlet.ServletRequest;
import javax.xml.namespace.QName;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.joda.time.DateTime;
import org.nuxeo.ecm.platform.ui.web.auth.LoginScreenHelper;
import org.nuxeo.ecm.platform.web.common.vh.VirtualHostHelper;
import org.opensaml.Configuration;
import org.opensaml.common.SAMLException;
import org.opensaml.common.SAMLObject;
import org.opensaml.common.binding.SAMLMessageContext;
import org.opensaml.common.xml.SAMLConstants;
import org.opensaml.saml2.core.Assertion;
import org.opensaml.saml2.core.AuthnRequest;
import org.opensaml.saml2.core.Conditions;
import org.opensaml.saml2.core.Issuer;
import org.opensaml.saml2.core.NameIDType;
import org.opensaml.saml2.core.Response;
import org.opensaml.saml2.encryption.Decrypter;
import org.opensaml.saml2.metadata.AssertionConsumerService;
import org.opensaml.saml2.metadata.Endpoint;
import org.opensaml.saml2.metadata.IDPSSODescriptor;
import org.opensaml.security.MetadataCriteria;
import org.opensaml.security.SAMLSignatureProfileValidator;
import org.opensaml.xml.XMLObjectBuilderFactory;
import org.opensaml.xml.encryption.DecryptionException;
import org.opensaml.xml.security.CriteriaSet;
import org.opensaml.xml.security.SecurityException;
import org.opensaml.xml.security.credential.UsageType;
import org.opensaml.xml.security.criteria.EntityIDCriteria;
import org.opensaml.xml.security.criteria.UsageCriteria;
import org.opensaml.xml.signature.Signature;
import org.opensaml.xml.signature.SignatureTrustEngine;
import org.opensaml.xml.validation.ValidationException;

/**
 * Base abstract class for SAML profile processors.
 *
 * @since 6.0
 */
public abstract class AbstractSAMLProfile {
    protected final static Log log = LogFactory.getLog(AbstractSAMLProfile.class);

    protected final XMLObjectBuilderFactory builderFactory;

    private final Endpoint endpoint;

    private SignatureTrustEngine trustEngine;

    private Decrypter decrypter;

    public AbstractSAMLProfile(Endpoint endpoint) {
        this.endpoint = endpoint;
        this.builderFactory = Configuration.getBuilderFactory();
    }

    /**
     * @return the profile identifier (Uri).
     */
    abstract public String getProfileIdentifier();

    protected <T extends SAMLObject> T build(QName qName) {
        return (T) builderFactory.getBuilder(qName).buildObject(qName);
    }

    // VALIDATION

    protected void validateSignature(Signature signature, String IDPEntityID)
            throws ValidationException, org.opensaml.xml.security.SecurityException {

        if (trustEngine == null) {
            throw new SecurityException("Trust engine is not set, signature can't be verified");
        }

        SAMLSignatureProfileValidator validator = new SAMLSignatureProfileValidator();
        validator.validate(signature);
        CriteriaSet criteriaSet = new CriteriaSet();
        criteriaSet.add(new EntityIDCriteria(IDPEntityID));
        criteriaSet.add(new MetadataCriteria(IDPSSODescriptor.DEFAULT_ELEMENT_NAME, SAMLConstants.SAML20P_NS));
        criteriaSet.add(new UsageCriteria(UsageType.SIGNING));
        log.debug("Verifying signature: " + signature);

        if (!getTrustEngine().validate(signature, criteriaSet)) {
            throw new ValidationException("Signature is not trusted or invalid");
        }
    }

    protected void validateIssuer(Issuer issuer, SAMLMessageContext context) throws SAMLException {
        // Validate format of issuer
        if (issuer.getFormat() != null && !issuer.getFormat().equals(NameIDType.ENTITY)) {
            throw new SAMLException("Assertion invalidated by issuer type");
        }
        // Validate that issuer is expected peer entity
        if (!context.getPeerEntityMetadata().getEntityID().equals(issuer.getValue())) {
            throw new SAMLException("Assertion invalidated by unexpected issuer value");
        }
    }

    protected void validateEndpoint(Response response, Endpoint endpoint) throws SAMLException {
        // Verify that destination in the response matches one of the available endpoints
        String destination = response.getDestination();

        if (destination != null) {
            if (destination.equals(endpoint.getLocation())) {
            } else if (destination.equals(endpoint.getResponseLocation())) {
            } else {
                log.debug("Intended destination " + destination + " doesn't match any of the endpoint URLs");
                throw new SAMLException(
                        "Intended destination " + destination + " doesn't match any of the endpoint URLs");
            }
        }

        // Verify response to field if present, set request if correct
        AuthnRequest request = retrieveRequest(response);

        // Verify endpoint requested in the original request
        if (request != null) {
            AssertionConsumerService assertionConsumerService = (AssertionConsumerService) endpoint;
            if (request.getAssertionConsumerServiceIndex() != null) {
                if (!request.getAssertionConsumerServiceIndex().equals(assertionConsumerService.getIndex())) {
                    log.info("SAML response was received at a different endpoint " + "index than was requested");
                }
            } else {
                String requestedResponseURL = request.getAssertionConsumerServiceURL();
                String requestedBinding = request.getProtocolBinding();
                if (requestedResponseURL != null) {
                    String responseLocation;
                    if (assertionConsumerService.getResponseLocation() != null) {
                        responseLocation = assertionConsumerService.getResponseLocation();
                    } else {
                        responseLocation = assertionConsumerService.getLocation();
                    }
                    if (!requestedResponseURL.equals(responseLocation)) {
                        log.info("SAML response was received at a different endpoint URL " + responseLocation
                                + " than was requested " + requestedResponseURL);
                    }
                }
                /*
                 * if (requestedBinding != null) { if (!requestedBinding.equals(context.getInboundSAMLBinding())) {
                 * log.info("SAML response was received using a different binding {} than was requested {}",
                 * context.getInboundSAMLBinding(), requestedBinding); } }
                 */
            }
        }
    }

    protected void validateAssertion(Assertion assertion, SAMLMessageContext context) throws SAMLException,
            org.opensaml.xml.security.SecurityException, ValidationException, DecryptionException {

        validateIssuer(assertion.getIssuer(), context);

        Conditions conditions = assertion.getConditions();

        // validate conditions timestamps: notBefore, notOnOrAfter
        Date now = new DateTime().toDate();
        Date condition_notBefore = null;
        Date condition_NotOnOrAfter = null;
        if (conditions.getNotBefore() != null) {
            condition_notBefore = conditions.getNotBefore().toDate();
        }
        if (conditions.getNotOnOrAfter() != null) {
            condition_NotOnOrAfter = conditions.getNotOnOrAfter().toDate();
        }
        if (condition_notBefore != null && now.before(condition_notBefore)) {
            log.debug("Current time: [" + now + "] NotBefore: [" + condition_notBefore + "]");
            throw new SAMLException("Conditions are not yet active");
        } else if (condition_NotOnOrAfter != null
                && (now.after(condition_NotOnOrAfter) || now.equals(condition_NotOnOrAfter))) {
            log.debug("Current time: [" + now + "] NotOnOrAfter: [" + condition_NotOnOrAfter + "]");
            throw new SAMLException("Conditions have expired");
        }

        Signature signature = assertion.getSignature();

        if (signature != null) {
            validateSignature(signature, context.getPeerEntityMetadata().getEntityID());
        }

        // TODO(nfgs) : Check subject
    }

    protected AuthnRequest retrieveRequest(Response response) throws SAMLException {
        // TODO(nfgs) - Store SAML messages to validate response.getInResponseTo()
        return null;
    }

    public Endpoint getEndpoint() {
        return endpoint;
    }

    public SignatureTrustEngine getTrustEngine() {
        return trustEngine;
    }

    public void setTrustEngine(SignatureTrustEngine trustEngine) {
        this.trustEngine = trustEngine;
    }

    public Decrypter getDecrypter() {
        return decrypter;
    }

    public void setDecrypter(Decrypter decrypter) {
        this.decrypter = decrypter;
    }

    protected String newUUID() {
        return "_" + UUID.randomUUID().toString();
    }

    protected String getBaseURL(ServletRequest request) {
        return VirtualHostHelper.getBaseURL(request);
    }

    protected String getStartPageURL(ServletRequest request) {
        return getBaseURL(request) + LoginScreenHelper.getStartupPagePath();
    }
}