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; 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 @Override 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 @Override 079 public boolean processLogoutRequest(SAMLMessageContext context, SAMLCredential credential) throws SAMLException { 080 081 SAMLObject message = context.getInboundSAMLMessage(); 082 083 // Verify type 084 if (message == null || !(message instanceof LogoutRequest)) { 085 throw new SAMLException("Message is not of a LogoutRequest object type"); 086 } 087 088 LogoutRequest request = (LogoutRequest) message; 089 090 // Validate signature of the response if present 091 if (request.getSignature() != null) { 092 log.debug("Verifying message signature"); 093 validateSignature(request.getSignature(), context.getPeerEntityId()); 094 context.setInboundSAMLMessageAuthenticated(true); 095 } 096 097 // TODO - Validate destination 098 099 // Validate issuer 100 if (request.getIssuer() != null) { 101 log.debug("Verifying issuer of the message"); 102 Issuer issuer = request.getIssuer(); 103 validateIssuer(issuer, context); 104 } 105 106 // TODO - Validate issue time 107 108 // Get and validate the NameID 109 NameID nameID; 110 if (getDecrypter() != null && request.getEncryptedID() != null) { 111 try { 112 nameID = (NameID) getDecrypter().decrypt(request.getEncryptedID()); 113 } catch (DecryptionException e) { 114 throw new SAMLException("Failed to decrypt NameID", e); 115 } 116 } else { 117 nameID = request.getNameID(); 118 } 119 120 if (nameID == null) { 121 throw new SAMLException("The requested NameID is invalid"); 122 } 123 124 // If no index is specified do logout 125 if (request.getSessionIndexes() == null || request.getSessionIndexes().isEmpty()) { 126 return true; 127 } 128 129 // Else check if this is on of our session indexes 130 for (SessionIndex sessionIndex : request.getSessionIndexes()) { 131 if (credential.getSessionIndexes().contains(sessionIndex.getSessionIndex())) { 132 return true; 133 } 134 } 135 136 return false; 137 } 138 139 @Override 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 validateSignature(response.getSignature(), context.getPeerEntityId()); 153 context.setInboundSAMLMessageAuthenticated(true); 154 } 155 156 // TODO - Validate destination 157 158 // Validate issuer 159 if (response.getIssuer() != null) { 160 log.debug("Verifying issuer of the message"); 161 Issuer issuer = response.getIssuer(); 162 validateIssuer(issuer, context); 163 } 164 165 // TODO - Validate issue time 166 167 // Verify status 168 String statusCode = response.getStatus().getStatusCode().getValue(); 169 if (!statusCode.equals(StatusCode.SUCCESS_URI) && !statusCode.equals(StatusCode.PARTIAL_LOGOUT_URI)) { 170 log.warn("Invalid status code " + statusCode + ": " + response.getStatus().getStatusMessage()); 171 } 172 } 173}