001/* 002 * (C) Copyright 2014 Nuxeo SA (http://nuxeo.com/) and contributors. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the GNU Lesser General Public License 006 * (LGPL) version 2.1 which accompanies this distribution, and is available at 007 * http://www.gnu.org/licenses/lgpl-2.1.html 008 * 009 * This library is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 012 * Lesser General Public License for more details. 013 * 014 * Contributors: 015 * Nelson Silva <nelson.silva@inevo.pt> 016 */ 017package org.nuxeo.ecm.platform.auth.saml.slo; 018 019import org.joda.time.DateTime; 020import org.nuxeo.ecm.platform.auth.saml.AbstractSAMLProfile; 021import org.nuxeo.ecm.platform.auth.saml.SAMLConfiguration; 022import org.nuxeo.ecm.platform.auth.saml.SAMLCredential; 023import org.opensaml.common.SAMLException; 024import org.opensaml.common.SAMLObject; 025import org.opensaml.common.SAMLVersion; 026import org.opensaml.common.binding.SAMLMessageContext; 027import org.opensaml.saml2.core.*; 028import org.opensaml.saml2.metadata.SingleLogoutService; 029import org.opensaml.xml.encryption.DecryptionException; 030import org.opensaml.xml.validation.ValidationException; 031 032/** 033 * WebSLO (Single Log Out) profile implementation. 034 * 035 * @since 6.0 036 */ 037public class SLOProfileImpl extends AbstractSAMLProfile implements SLOProfile { 038 039 public SLOProfileImpl(SingleLogoutService slo) { 040 super(slo); 041 } 042 043 @Override 044 public String getProfileIdentifier() { 045 return PROFILE_URI; 046 } 047 048 public LogoutRequest buildLogoutRequest(SAMLMessageContext context, SAMLCredential credential) throws SAMLException { 049 050 LogoutRequest request = build(LogoutRequest.DEFAULT_ELEMENT_NAME); 051 request.setID(newUUID()); 052 request.setVersion(SAMLVersion.VERSION_20); 053 request.setIssueInstant(new DateTime()); 054 request.setDestination(getEndpoint().getLocation()); 055 056 Issuer issuer = build(Issuer.DEFAULT_ELEMENT_NAME); 057 issuer.setValue(SAMLConfiguration.getEntityId()); 058 request.setIssuer(issuer); 059 060 // Add session indexes 061 if (credential.getSessionIndexes() == null || credential.getSessionIndexes().isEmpty()) { 062 throw new SAMLException("No session indexes found"); 063 } 064 for (String sessionIndex : credential.getSessionIndexes()) { 065 SessionIndex index = build(SessionIndex.DEFAULT_ELEMENT_NAME); 066 index.setSessionIndex(sessionIndex); 067 request.getSessionIndexes().add(index); 068 } 069 070 request.setNameID(credential.getNameID()); 071 072 return request; 073 074 } 075 076 public boolean processLogoutRequest(SAMLMessageContext context, SAMLCredential credential) throws SAMLException { 077 078 SAMLObject message = context.getInboundSAMLMessage(); 079 080 // Verify type 081 if (message == null || !(message instanceof LogoutRequest)) { 082 throw new SAMLException("Message is not of a LogoutRequest object type"); 083 } 084 085 LogoutRequest request = (LogoutRequest) message; 086 087 // Validate signature of the response if present 088 if (request.getSignature() != null) { 089 log.debug("Verifying message signature"); 090 try { 091 validateSignature(request.getSignature(), context.getPeerEntityId()); 092 } catch (ValidationException e) { 093 log.error("Error validating signature", e); 094 } catch (org.opensaml.xml.security.SecurityException e) { 095 e.printStackTrace(); 096 } 097 context.setInboundSAMLMessageAuthenticated(true); 098 } 099 100 // TODO - Validate destination 101 102 // Validate issuer 103 if (request.getIssuer() != null) { 104 log.debug("Verifying issuer of the message"); 105 Issuer issuer = request.getIssuer(); 106 validateIssuer(issuer, context); 107 } 108 109 // TODO - Validate issue time 110 111 // Get and validate the NameID 112 NameID nameID; 113 if (getDecrypter() != null && request.getEncryptedID() != null) { 114 try { 115 nameID = (NameID) getDecrypter().decrypt(request.getEncryptedID()); 116 } catch (DecryptionException e) { 117 throw new SAMLException("Failed to decrypt NameID", e); 118 } 119 } else { 120 nameID = request.getNameID(); 121 } 122 123 if (nameID == null) { 124 throw new SAMLException("The requested NameID is invalid"); 125 } 126 127 // If no index is specified do logout 128 if (request.getSessionIndexes() == null || request.getSessionIndexes().isEmpty()) { 129 return true; 130 } 131 132 // Else check if this is on of our session indexes 133 for (SessionIndex sessionIndex : request.getSessionIndexes()) { 134 if (credential.getSessionIndexes().contains(sessionIndex.getSessionIndex())) { 135 return true; 136 } 137 } 138 139 return false; 140 } 141 142 public void processLogoutResponse(SAMLMessageContext context) throws SAMLException { 143 144 SAMLObject message = context.getInboundSAMLMessage(); 145 146 if (!(message instanceof LogoutResponse)) { 147 throw new SAMLException("Message is not of a LogoutResponse object type"); 148 } 149 LogoutResponse response = (LogoutResponse) message; 150 151 // Validate signature of the response if present 152 if (response.getSignature() != null) { 153 log.debug("Verifying message signature"); 154 try { 155 validateSignature(response.getSignature(), context.getPeerEntityId()); 156 } catch (ValidationException e) { 157 log.error("Error validating signature", e); 158 } catch (org.opensaml.xml.security.SecurityException e) { 159 e.printStackTrace(); 160 } 161 context.setInboundSAMLMessageAuthenticated(true); 162 } 163 164 // TODO - Validate destination 165 166 // Validate issuer 167 if (response.getIssuer() != null) { 168 log.debug("Verifying issuer of the message"); 169 Issuer issuer = response.getIssuer(); 170 validateIssuer(issuer, context); 171 } 172 173 // TODO - Validate issue time 174 175 // Verify status 176 String statusCode = response.getStatus().getStatusCode().getValue(); 177 if (!statusCode.equals(StatusCode.SUCCESS_URI) && !statusCode.equals(StatusCode.PARTIAL_LOGOUT_URI)) { 178 log.warn("Invalid status code " + statusCode + ": " + response.getStatus().getStatusMessage()); 179 } 180 } 181}