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