/*
* (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.slo;
import org.joda.time.DateTime;
import org.nuxeo.ecm.platform.auth.saml.AbstractSAMLProfile;
import org.nuxeo.ecm.platform.auth.saml.SAMLConfiguration;
import org.nuxeo.ecm.platform.auth.saml.SAMLCredential;
import org.opensaml.common.SAMLException;
import org.opensaml.common.SAMLObject;
import org.opensaml.common.SAMLVersion;
import org.opensaml.common.binding.SAMLMessageContext;
import org.opensaml.saml2.core.*;
import org.opensaml.saml2.metadata.SingleLogoutService;
import org.opensaml.xml.encryption.DecryptionException;
import org.opensaml.xml.validation.ValidationException;
/**
* WebSLO (Single Log Out) profile implementation.
*
* @since 6.0
*/
public class SLOProfileImpl extends AbstractSAMLProfile implements SLOProfile {
public SLOProfileImpl(SingleLogoutService slo) {
super(slo);
}
@Override
public String getProfileIdentifier() {
return PROFILE_URI;
}
public LogoutRequest buildLogoutRequest(SAMLMessageContext context, SAMLCredential credential) throws SAMLException {
LogoutRequest request = build(LogoutRequest.DEFAULT_ELEMENT_NAME);
request.setID(newUUID());
request.setVersion(SAMLVersion.VERSION_20);
request.setIssueInstant(new DateTime());
request.setDestination(getEndpoint().getLocation());
Issuer issuer = build(Issuer.DEFAULT_ELEMENT_NAME);
issuer.setValue(SAMLConfiguration.getEntityId());
request.setIssuer(issuer);
// Add session indexes
if (credential.getSessionIndexes() == null || credential.getSessionIndexes().isEmpty()) {
throw new SAMLException("No session indexes found");
}
for (String sessionIndex : credential.getSessionIndexes()) {
SessionIndex index = build(SessionIndex.DEFAULT_ELEMENT_NAME);
index.setSessionIndex(sessionIndex);
request.getSessionIndexes().add(index);
}
request.setNameID(credential.getNameID());
return request;
}
public boolean processLogoutRequest(SAMLMessageContext context, SAMLCredential credential) throws SAMLException {
SAMLObject message = context.getInboundSAMLMessage();
// Verify type
if (message == null || !(message instanceof LogoutRequest)) {
throw new SAMLException("Message is not of a LogoutRequest object type");
}
LogoutRequest request = (LogoutRequest) message;
// Validate signature of the response if present
if (request.getSignature() != null) {
log.debug("Verifying message signature");
try {
validateSignature(request.getSignature(), context.getPeerEntityId());
} catch (ValidationException e) {
log.error("Error validating signature", e);
} catch (org.opensaml.xml.security.SecurityException e) {
e.printStackTrace();
}
context.setInboundSAMLMessageAuthenticated(true);
}
// TODO - Validate destination
// Validate issuer
if (request.getIssuer() != null) {
log.debug("Verifying issuer of the message");
Issuer issuer = request.getIssuer();
validateIssuer(issuer, context);
}
// TODO - Validate issue time
// Get and validate the NameID
NameID nameID;
if (getDecrypter() != null && request.getEncryptedID() != null) {
try {
nameID = (NameID) getDecrypter().decrypt(request.getEncryptedID());
} catch (DecryptionException e) {
throw new SAMLException("Failed to decrypt NameID", e);
}
} else {
nameID = request.getNameID();
}
if (nameID == null) {
throw new SAMLException("The requested NameID is invalid");
}
// If no index is specified do logout
if (request.getSessionIndexes() == null || request.getSessionIndexes().isEmpty()) {
return true;
}
// Else check if this is on of our session indexes
for (SessionIndex sessionIndex : request.getSessionIndexes()) {
if (credential.getSessionIndexes().contains(sessionIndex.getSessionIndex())) {
return true;
}
}
return false;
}
public void processLogoutResponse(SAMLMessageContext context) throws SAMLException {
SAMLObject message = context.getInboundSAMLMessage();
if (!(message instanceof LogoutResponse)) {
throw new SAMLException("Message is not of a LogoutResponse object type");
}
LogoutResponse response = (LogoutResponse) message;
// Validate signature of the response if present
if (response.getSignature() != null) {
log.debug("Verifying message signature");
try {
validateSignature(response.getSignature(), context.getPeerEntityId());
} catch (ValidationException e) {
log.error("Error validating signature", e);
} catch (org.opensaml.xml.security.SecurityException e) {
e.printStackTrace();
}
context.setInboundSAMLMessageAuthenticated(true);
}
// TODO - Validate destination
// Validate issuer
if (response.getIssuer() != null) {
log.debug("Verifying issuer of the message");
Issuer issuer = response.getIssuer();
validateIssuer(issuer, context);
}
// TODO - Validate issue time
// Verify status
String statusCode = response.getStatus().getStatusCode().getValue();
if (!statusCode.equals(StatusCode.SUCCESS_URI) && !statusCode.equals(StatusCode.PARTIAL_LOGOUT_URI)) {
log.warn("Invalid status code " + statusCode + ": " + response.getStatus().getStatusMessage());
}
}
}