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.sso; 018 019import org.apache.commons.lang.StringUtils; 020import org.joda.time.DateTime; 021import org.nuxeo.ecm.platform.auth.saml.AbstractSAMLProfile; 022import org.nuxeo.ecm.platform.auth.saml.SAMLConfiguration; 023import org.nuxeo.ecm.platform.auth.saml.SAMLCredential; 024import org.opensaml.common.SAMLException; 025import org.opensaml.common.SAMLObject; 026import org.opensaml.common.SAMLVersion; 027import org.opensaml.common.binding.SAMLMessageContext; 028import org.opensaml.saml2.core.*; 029import org.opensaml.saml2.metadata.SPSSODescriptor; 030import org.opensaml.saml2.metadata.SingleSignOnService; 031import org.opensaml.xml.encryption.DecryptionException; 032import org.opensaml.xml.signature.Signature; 033import org.opensaml.xml.validation.ValidationException; 034 035import javax.servlet.http.HttpServletRequest; 036import java.io.Serializable; 037import java.util.ArrayList; 038import java.util.LinkedList; 039import java.util.List; 040 041/** 042 * WebSSO (Single Sign On) profile implementation. 043 * 044 * @since 6.0 045 */ 046public class WebSSOProfileImpl extends AbstractSAMLProfile implements WebSSOProfile { 047 048 public WebSSOProfileImpl(SingleSignOnService sso) { 049 super(sso); 050 } 051 052 @Override 053 public String getProfileIdentifier() { 054 return PROFILE_URI; 055 } 056 057 @Override 058 public SAMLCredential processAuthenticationResponse(SAMLMessageContext context) throws SAMLException { 059 SAMLObject message = context.getInboundSAMLMessage(); 060 061 // Validate type 062 if (!(message instanceof Response)) { 063 log.debug("Received response is not of a Response object type"); 064 throw new SAMLException("Received response is not of a Response object type"); 065 } 066 Response response = (Response) message; 067 068 // Validate status 069 String statusCode = response.getStatus().getStatusCode().getValue(); 070 if (!StringUtils.equals(statusCode, StatusCode.SUCCESS_URI)) { 071 log.debug("StatusCode was not a success: " + statusCode); 072 throw new SAMLException("StatusCode was not a success: " + statusCode); 073 } 074 075 // Validate signature of the response if present 076 if (response.getSignature() != null) { 077 log.debug("Verifying message signature"); 078 try { 079 validateSignature(response.getSignature(), context.getPeerEntityId()); 080 } catch (ValidationException e) { 081 log.error("Error validating signature", e); 082 } catch (org.opensaml.xml.security.SecurityException e) { 083 e.printStackTrace(); 084 } 085 context.setInboundSAMLMessageAuthenticated(true); 086 } 087 088 // TODO(nfgs) - Verify issue time ?! 089 090 // TODO(nfgs) - Verify endpoint requested 091 // Endpoint endpoint = context.getLocalEntityEndpoint(); 092 // validateEndpoint(response, ssoService); 093 094 // Verify issuer 095 if (response.getIssuer() != null) { 096 log.debug("Verifying issuer of the message"); 097 Issuer issuer = response.getIssuer(); 098 validateIssuer(issuer, context); 099 } 100 101 List<Attribute> attributes = new LinkedList<>(); 102 List<Assertion> assertions = response.getAssertions(); 103 104 // Decrypt encrypted assertions 105 List<EncryptedAssertion> encryptedAssertionList = response.getEncryptedAssertions(); 106 for (EncryptedAssertion ea : encryptedAssertionList) { 107 try { 108 log.debug("Decrypting assertion"); 109 assertions.add(getDecrypter().decrypt(ea)); 110 } catch (DecryptionException e) { 111 log.debug("Decryption of received assertion failed, assertion will be skipped", e); 112 } 113 } 114 115 Subject subject = null; 116 List<String> sessionIndexes = new ArrayList<>(); 117 118 // Find the assertion to be used for session creation, other assertions are ignored 119 for (Assertion a : assertions) { 120 121 // We're only interested in assertions with AuthnStatement 122 if (a.getAuthnStatements().size() > 0) { 123 try { 124 // Verify that the assertion is valid 125 validateAssertion(a, context); 126 127 // Store session indexes for logout 128 for (AuthnStatement statement : a.getAuthnStatements()) { 129 sessionIndexes.add(statement.getSessionIndex()); 130 } 131 132 } catch (SAMLException | org.opensaml.xml.security.SecurityException | ValidationException 133 | DecryptionException e) { 134 log.debug("Validation of received assertion failed, assertion will be skipped", e); 135 continue; 136 } 137 } 138 139 subject = a.getSubject(); 140 141 // Process all attributes 142 for (AttributeStatement attStatement : a.getAttributeStatements()) { 143 for (Attribute att : attStatement.getAttributes()) { 144 attributes.add(att); 145 } 146 // Decrypt attributes 147 for (EncryptedAttribute att : attStatement.getEncryptedAttributes()) { 148 try { 149 attributes.add(getDecrypter().decrypt(att)); 150 } catch (DecryptionException e) { 151 log.error("Failed to decrypt assertion"); 152 } 153 } 154 } 155 156 break; 157 } 158 159 // Make sure that at least one storage contains authentication statement and subject with bearer confirmation 160 if (subject == null) { 161 log.debug("Response doesn't have any valid assertion which would pass subject validation"); 162 throw new SAMLException("Error validating SAML response"); 163 } 164 165 // Was the subject confirmed by this confirmation data? If so let's store the subject in the context. 166 NameID nameID = null; 167 if (subject.getEncryptedID() != null) { 168 // TODO(nfgs) - Decrypt NameID 169 } else { 170 nameID = subject.getNameID(); 171 } 172 173 if (nameID == null) { 174 log.debug("NameID element must be present as part of the Subject in " 175 + "the Response message, please enable it in the IDP configuration"); 176 throw new SAMLException("NameID element must be present as part of the Subject " 177 + "in the Response message, please enable it in the IDP configuration"); 178 } 179 180 // Populate custom data, if any 181 Serializable additionalData = null; // processAdditionalData(context); 182 183 // Create the credential 184 return new SAMLCredential(nameID, sessionIndexes, context.getPeerEntityMetadata().getEntityID(), 185 context.getRelayState(), attributes, context.getLocalEntityId(), additionalData); 186 187 } 188 189 @Override 190 public AuthnRequest buildAuthRequest(HttpServletRequest httpRequest) throws SAMLException { 191 192 AuthnRequest request = build(AuthnRequest.DEFAULT_ELEMENT_NAME); 193 request.setID(newUUID()); 194 request.setVersion(SAMLVersion.VERSION_20); 195 request.setIssueInstant(new DateTime()); 196 // Let the IdP pick a protocol binding 197 //request.setProtocolBinding(SAMLConstants.SAML2_POST_BINDING_URI); 198 199 // Fill the assertion consumer URL 200 request.setAssertionConsumerServiceURL(getStartPageURL(httpRequest)); 201 202 Issuer issuer = build(Issuer.DEFAULT_ELEMENT_NAME); 203 issuer.setValue(SAMLConfiguration.getEntityId()); 204 request.setIssuer(issuer); 205 206 NameIDPolicy nameIDPolicy = build(NameIDPolicy.DEFAULT_ELEMENT_NAME); 207 nameIDPolicy.setFormat(NameIDType.UNSPECIFIED); 208 request.setNameIDPolicy(nameIDPolicy); 209 210 RequestedAuthnContext requestedAuthnContext = build(RequestedAuthnContext.DEFAULT_ELEMENT_NAME); 211 requestedAuthnContext.setComparison(AuthnContextComparisonTypeEnumeration.EXACT); 212 request.setRequestedAuthnContext(requestedAuthnContext); 213 214 AuthnContextClassRef authnContextClassRef = build(AuthnContextClassRef.DEFAULT_ELEMENT_NAME); 215 authnContextClassRef.setAuthnContextClassRef(AuthnContext.PPT_AUTHN_CTX); 216 requestedAuthnContext.getAuthnContextClassRefs().add(authnContextClassRef); 217 218 return request; 219 220 } 221 222 @Override 223 protected void validateAssertion(Assertion assertion, SAMLMessageContext context) throws SAMLException, 224 org.opensaml.xml.security.SecurityException, ValidationException, DecryptionException { 225 super.validateAssertion(assertion, context); 226 Signature signature = assertion.getSignature(); 227 if (signature == null) { 228 SPSSODescriptor roleMetadata = (SPSSODescriptor) context.getLocalEntityRoleMetadata(); 229 230 if (roleMetadata != null && roleMetadata.getWantAssertionsSigned()) { 231 if (!context.isInboundSAMLMessageAuthenticated()) { 232 throw new SAMLException("Metadata includes wantAssertionSigned, " 233 + "but neither Response nor included Assertion is signed"); 234 } 235 } 236 } 237 } 238}