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