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