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.SAMLCredential; 022import org.opensaml.common.SAMLException; 023import org.opensaml.common.SAMLObject; 024import org.opensaml.common.SAMLVersion; 025import org.opensaml.common.binding.SAMLMessageContext; 026import org.opensaml.saml2.core.*; 027import org.opensaml.saml2.metadata.SingleLogoutService; 028import org.opensaml.xml.encryption.DecryptionException; 029import org.opensaml.xml.validation.ValidationException; 030 031/** 032 * WebSLO (Single Log Out) profile implementation. 033 * 034 * @since 6.0 035 */ 036public class SLOProfileImpl extends AbstractSAMLProfile implements SLOProfile { 037 038 public SLOProfileImpl(SingleLogoutService slo) { 039 super(slo); 040 } 041 042 @Override 043 public String getProfileIdentifier() { 044 return PROFILE_URI; 045 } 046 047 public LogoutRequest buildLogoutRequest(SAMLMessageContext context, SAMLCredential credential) throws SAMLException { 048 049 LogoutRequest request = build(LogoutRequest.DEFAULT_ELEMENT_NAME); 050 request.setID(newUUID()); 051 // TODO(nfgs) Build issuer 052 // request.setIssuer(issuer); 053 request.setVersion(SAMLVersion.VERSION_20); 054 request.setIssueInstant(new DateTime()); 055 056 request.setDestination(getEndpoint().getLocation()); 057 058 // Add session indexes 059 if (credential.getSessionIndexes() == null || credential.getSessionIndexes().isEmpty()) { 060 throw new SAMLException("No session indexes found"); 061 } 062 for (String sessionIndex : credential.getSessionIndexes()) { 063 SessionIndex index = build(SessionIndex.DEFAULT_ELEMENT_NAME); 064 index.setSessionIndex(sessionIndex); 065 request.getSessionIndexes().add(index); 066 } 067 068 request.setNameID(credential.getNameID()); 069 070 return request; 071 072 } 073 074 public boolean processLogoutRequest(SAMLMessageContext context, SAMLCredential credential) throws SAMLException { 075 076 SAMLObject message = context.getInboundSAMLMessage(); 077 078 // Verify type 079 if (message == null || !(message instanceof LogoutRequest)) { 080 throw new SAMLException("Message is not of a LogoutRequest object type"); 081 } 082 083 LogoutRequest request = (LogoutRequest) message; 084 085 // Validate signature of the response if present 086 if (request.getSignature() != null) { 087 log.debug("Verifying message signature"); 088 try { 089 validateSignature(request.getSignature(), context.getPeerEntityId()); 090 } catch (ValidationException e) { 091 log.error("Error validating signature", e); 092 } catch (org.opensaml.xml.security.SecurityException e) { 093 e.printStackTrace(); 094 } 095 context.setInboundSAMLMessageAuthenticated(true); 096 } 097 098 // TODO - Validate destination 099 100 // Validate issuer 101 if (request.getIssuer() != null) { 102 log.debug("Verifying issuer of the message"); 103 Issuer issuer = request.getIssuer(); 104 validateIssuer(issuer, context); 105 } 106 107 // TODO - Validate issue time 108 109 // Get and validate the NameID 110 NameID nameID; 111 if (getDecrypter() != null && request.getEncryptedID() != null) { 112 try { 113 nameID = (NameID) getDecrypter().decrypt(request.getEncryptedID()); 114 } catch (DecryptionException e) { 115 throw new SAMLException("Failed to decrypt NameID", e); 116 } 117 } else { 118 nameID = request.getNameID(); 119 } 120 121 if (nameID == null) { 122 throw new SAMLException("The requested NameID is invalid"); 123 } 124 125 // If no index is specified do logout 126 if (request.getSessionIndexes() == null || request.getSessionIndexes().isEmpty()) { 127 return true; 128 } 129 130 // Else check if this is on of our session indexes 131 for (SessionIndex sessionIndex : request.getSessionIndexes()) { 132 if (credential.getSessionIndexes().contains(sessionIndex.getSessionIndex())) { 133 return true; 134 } 135 } 136 137 return false; 138 } 139 140 public void processLogoutResponse(SAMLMessageContext context) throws SAMLException { 141 142 SAMLObject message = context.getInboundSAMLMessage(); 143 144 if (!(message instanceof LogoutResponse)) { 145 throw new SAMLException("Message is not of a LogoutResponse object type"); 146 } 147 LogoutResponse response = (LogoutResponse) message; 148 149 // Validate signature of the response if present 150 if (response.getSignature() != null) { 151 log.debug("Verifying message signature"); 152 try { 153 validateSignature(response.getSignature(), context.getPeerEntityId()); 154 } catch (ValidationException e) { 155 log.error("Error validating signature", e); 156 } catch (org.opensaml.xml.security.SecurityException e) { 157 e.printStackTrace(); 158 } 159 context.setInboundSAMLMessageAuthenticated(true); 160 } 161 162 // TODO - Validate destination 163 164 // Validate issuer 165 if (response.getIssuer() != null) { 166 log.debug("Verifying issuer of the message"); 167 Issuer issuer = response.getIssuer(); 168 validateIssuer(issuer, context); 169 } 170 171 // TODO - Validate issue time 172 173 // Verify status 174 String statusCode = response.getStatus().getStatusCode().getValue(); 175 if (!statusCode.equals(StatusCode.SUCCESS_URI) && !statusCode.equals(StatusCode.PARTIAL_LOGOUT_URI)) { 176 log.warn("Invalid status code " + statusCode + ": " + response.getStatus().getStatusMessage()); 177 } 178 } 179}