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