/*
* (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();
}
}