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