|
| 1 | +/* |
| 2 | + * Copyright 2002-2020 the original author or authors. |
| 3 | + * |
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | + * you may not use this file except in compliance with the License. |
| 6 | + * You may obtain a copy of the License at |
| 7 | + * |
| 8 | + * https://www.apache.org/licenses/LICENSE-2.0 |
| 9 | + * |
| 10 | + * Unless required by applicable law or agreed to in writing, software |
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | + * See the License for the specific language governing permissions and |
| 14 | + * limitations under the License. |
| 15 | + */ |
| 16 | + |
| 17 | +package org.springframework.security.saml2.provider.service.authentication; |
| 18 | + |
| 19 | +import java.nio.charset.StandardCharsets; |
| 20 | +import java.security.PrivateKey; |
| 21 | +import java.security.cert.X509Certificate; |
| 22 | +import java.time.Clock; |
| 23 | +import java.time.Instant; |
| 24 | +import java.util.ArrayList; |
| 25 | +import java.util.Collections; |
| 26 | +import java.util.LinkedHashMap; |
| 27 | +import java.util.List; |
| 28 | +import java.util.Map; |
| 29 | +import java.util.UUID; |
| 30 | + |
| 31 | +import net.shibboleth.utilities.java.support.resolver.CriteriaSet; |
| 32 | +import net.shibboleth.utilities.java.support.xml.SerializeSupport; |
| 33 | +import org.joda.time.DateTime; |
| 34 | +import org.opensaml.core.config.ConfigurationService; |
| 35 | +import org.opensaml.core.xml.config.XMLObjectProviderRegistry; |
| 36 | +import org.opensaml.core.xml.io.MarshallingException; |
| 37 | +import org.opensaml.saml.common.SAMLObjectBuilder; |
| 38 | +import org.opensaml.saml.common.xml.SAMLConstants; |
| 39 | +import org.opensaml.saml.saml2.core.AuthnRequest; |
| 40 | +import org.opensaml.saml.saml2.core.Issuer; |
| 41 | +import org.opensaml.saml.saml2.core.NameIDPolicy; |
| 42 | +import org.opensaml.saml.saml2.core.impl.AuthnRequestBuilder; |
| 43 | +import org.opensaml.saml.saml2.core.impl.AuthnRequestMarshaller; |
| 44 | +import org.opensaml.saml.saml2.core.impl.IssuerBuilder; |
| 45 | +import org.opensaml.saml.security.impl.SAMLMetadataSignatureSigningParametersResolver; |
| 46 | +import org.opensaml.security.SecurityException; |
| 47 | +import org.opensaml.security.credential.BasicCredential; |
| 48 | +import org.opensaml.security.credential.Credential; |
| 49 | +import org.opensaml.security.credential.CredentialSupport; |
| 50 | +import org.opensaml.security.credential.UsageType; |
| 51 | +import org.opensaml.xmlsec.SignatureSigningParameters; |
| 52 | +import org.opensaml.xmlsec.SignatureSigningParametersResolver; |
| 53 | +import org.opensaml.xmlsec.criterion.SignatureSigningConfigurationCriterion; |
| 54 | +import org.opensaml.xmlsec.crypto.XMLSigningUtil; |
| 55 | +import org.opensaml.xmlsec.impl.BasicSignatureSigningConfiguration; |
| 56 | +import org.opensaml.xmlsec.signature.support.SignatureConstants; |
| 57 | +import org.opensaml.xmlsec.signature.support.SignatureSupport; |
| 58 | +import org.w3c.dom.Element; |
| 59 | + |
| 60 | +import org.springframework.core.convert.converter.Converter; |
| 61 | +import org.springframework.security.saml2.Saml2Exception; |
| 62 | +import org.springframework.security.saml2.core.OpenSamlInitializationService; |
| 63 | +import org.springframework.security.saml2.core.Saml2X509Credential; |
| 64 | +import org.springframework.security.saml2.provider.service.authentication.Saml2RedirectAuthenticationRequest.Builder; |
| 65 | +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; |
| 66 | +import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; |
| 67 | +import org.springframework.util.Assert; |
| 68 | +import org.springframework.util.StringUtils; |
| 69 | +import org.springframework.web.util.UriComponentsBuilder; |
| 70 | +import org.springframework.web.util.UriUtils; |
| 71 | + |
| 72 | +/** |
| 73 | + * @since 5.2 |
| 74 | + */ |
| 75 | +public class OpenSamlAuthenticationRequestFactory implements Saml2AuthenticationRequestFactory { |
| 76 | + |
| 77 | + static { |
| 78 | + OpenSamlInitializationService.initialize(); |
| 79 | + } |
| 80 | + |
| 81 | + private Clock clock = Clock.systemUTC(); |
| 82 | + |
| 83 | + private AuthnRequestMarshaller marshaller; |
| 84 | + |
| 85 | + private AuthnRequestBuilder authnRequestBuilder; |
| 86 | + |
| 87 | + private IssuerBuilder issuerBuilder; |
| 88 | + |
| 89 | + private SAMLObjectBuilder<NameIDPolicy> nameIDBuilder; |
| 90 | + |
| 91 | + private Converter<Saml2AuthenticationRequestContext, String> protocolBindingResolver = (context) -> { |
| 92 | + if (context == null) { |
| 93 | + return SAMLConstants.SAML2_POST_BINDING_URI; |
| 94 | + } |
| 95 | + return context.getRelyingPartyRegistration().getAssertionConsumerServiceBinding().getUrn(); |
| 96 | + }; |
| 97 | + |
| 98 | + private Converter<Saml2AuthenticationRequestContext, AuthnRequest> authenticationRequestContextConverter = this::createAuthnRequest; |
| 99 | + |
| 100 | + /** |
| 101 | + * Creates an {@link OpenSamlAuthenticationRequestFactory} |
| 102 | + */ |
| 103 | + public OpenSamlAuthenticationRequestFactory() { |
| 104 | + XMLObjectProviderRegistry registry = ConfigurationService.get(XMLObjectProviderRegistry.class); |
| 105 | + this.marshaller = (AuthnRequestMarshaller) registry.getMarshallerFactory() |
| 106 | + .getMarshaller(AuthnRequest.DEFAULT_ELEMENT_NAME); |
| 107 | + this.authnRequestBuilder = (AuthnRequestBuilder) registry.getBuilderFactory() |
| 108 | + .getBuilder(AuthnRequest.DEFAULT_ELEMENT_NAME); |
| 109 | + this.issuerBuilder = (IssuerBuilder) registry.getBuilderFactory().getBuilder(Issuer.DEFAULT_ELEMENT_NAME); |
| 110 | + this.nameIDBuilder = (SAMLObjectBuilder<NameIDPolicy>) registry.getBuilderFactory() |
| 111 | + .getBuilder(NameIDPolicy.DEFAULT_ELEMENT_NAME); |
| 112 | + } |
| 113 | + |
| 114 | + @Override |
| 115 | + @Deprecated |
| 116 | + public String createAuthenticationRequest(Saml2AuthenticationRequest request) { |
| 117 | + AuthnRequest authnRequest = createAuthnRequest(request.getIssuer(), request.getDestination(), |
| 118 | + request.getAssertionConsumerServiceUrl(), this.protocolBindingResolver.convert(null), null); |
| 119 | + for (org.springframework.security.saml2.credentials.Saml2X509Credential credential : request.getCredentials()) { |
| 120 | + if (credential.isSigningCredential()) { |
| 121 | + X509Certificate certificate = credential.getCertificate(); |
| 122 | + PrivateKey privateKey = credential.getPrivateKey(); |
| 123 | + BasicCredential cred = CredentialSupport.getSimpleCredential(certificate, privateKey); |
| 124 | + cred.setEntityId(request.getIssuer()); |
| 125 | + cred.setUsageType(UsageType.SIGNING); |
| 126 | + SignatureSigningParameters parameters = new SignatureSigningParameters(); |
| 127 | + parameters.setSigningCredential(cred); |
| 128 | + parameters.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256); |
| 129 | + parameters.setSignatureReferenceDigestMethod(SignatureConstants.ALGO_ID_DIGEST_SHA256); |
| 130 | + parameters.setSignatureCanonicalizationAlgorithm(SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS); |
| 131 | + return serialize(sign(authnRequest, parameters)); |
| 132 | + } |
| 133 | + } |
| 134 | + throw new IllegalArgumentException("No signing credential provided"); |
| 135 | + } |
| 136 | + |
| 137 | + @Override |
| 138 | + public Saml2PostAuthenticationRequest createPostAuthenticationRequest(Saml2AuthenticationRequestContext context) { |
| 139 | + AuthnRequest authnRequest = this.authenticationRequestContextConverter.convert(context); |
| 140 | + String xml = context.getRelyingPartyRegistration().getAssertingPartyDetails().getWantAuthnRequestsSigned() |
| 141 | + ? serialize(sign(authnRequest, context.getRelyingPartyRegistration())) : serialize(authnRequest); |
| 142 | + |
| 143 | + return Saml2PostAuthenticationRequest.withAuthenticationRequestContext(context) |
| 144 | + .samlRequest(Saml2Utils.samlEncode(xml.getBytes(StandardCharsets.UTF_8))).build(); |
| 145 | + } |
| 146 | + |
| 147 | + @Override |
| 148 | + public Saml2RedirectAuthenticationRequest createRedirectAuthenticationRequest( |
| 149 | + Saml2AuthenticationRequestContext context) { |
| 150 | + AuthnRequest authnRequest = this.authenticationRequestContextConverter.convert(context); |
| 151 | + String xml = serialize(authnRequest); |
| 152 | + Builder result = Saml2RedirectAuthenticationRequest.withAuthenticationRequestContext(context); |
| 153 | + String deflatedAndEncoded = Saml2Utils.samlEncode(Saml2Utils.samlDeflate(xml)); |
| 154 | + result.samlRequest(deflatedAndEncoded).relayState(context.getRelayState()); |
| 155 | + if (context.getRelyingPartyRegistration().getAssertingPartyDetails().getWantAuthnRequestsSigned()) { |
| 156 | + Map<String, String> parameters = new LinkedHashMap<>(); |
| 157 | + parameters.put("SAMLRequest", deflatedAndEncoded); |
| 158 | + if (StringUtils.hasText(context.getRelayState())) { |
| 159 | + parameters.put("RelayState", context.getRelayState()); |
| 160 | + } |
| 161 | + sign(parameters, context.getRelyingPartyRegistration()); |
| 162 | + return result.sigAlg(parameters.get("SigAlg")).signature(parameters.get("Signature")).build(); |
| 163 | + } |
| 164 | + return result.build(); |
| 165 | + } |
| 166 | + |
| 167 | + private AuthnRequest createAuthnRequest(Saml2AuthenticationRequestContext context) { |
| 168 | + return createAuthnRequest(context.getIssuer(), context.getDestination(), |
| 169 | + context.getAssertionConsumerServiceUrl(), this.protocolBindingResolver.convert(context), |
| 170 | + context.getRelyingPartyRegistration().getNameIDFormat()); |
| 171 | + } |
| 172 | + |
| 173 | + private AuthnRequest createAuthnRequest(String issuer, String destination, String assertionConsumerServiceUrl, |
| 174 | + String protocolBinding, String nameIDFormat) { |
| 175 | + AuthnRequest auth = this.authnRequestBuilder.buildObject(); |
| 176 | + auth.setID("ARQ" + UUID.randomUUID().toString().substring(1)); |
| 177 | + auth.setIssueInstant(new DateTime(this.clock.millis())); |
| 178 | + auth.setForceAuthn(Boolean.FALSE); |
| 179 | + auth.setIsPassive(Boolean.FALSE); |
| 180 | + auth.setProtocolBinding(protocolBinding); |
| 181 | + Issuer iss = this.issuerBuilder.buildObject(); |
| 182 | + iss.setValue(issuer); |
| 183 | + auth.setIssuer(iss); |
| 184 | + auth.setDestination(destination); |
| 185 | + auth.setAssertionConsumerServiceURL(assertionConsumerServiceUrl); |
| 186 | + |
| 187 | + if (nameIDFormat != null) { |
| 188 | + NameIDPolicy nameId = this.nameIDBuilder.buildObject(); |
| 189 | + nameId.setFormat(nameIDFormat); |
| 190 | + auth.setNameIDPolicy(nameId); |
| 191 | + } |
| 192 | + return auth; |
| 193 | + } |
| 194 | + |
| 195 | + /** |
| 196 | + * Set the {@link AuthnRequest} post-processor resolver |
| 197 | + * @param authenticationRequestContextConverter |
| 198 | + * @since 5.4 |
| 199 | + */ |
| 200 | + public void setAuthenticationRequestContextConverter( |
| 201 | + Converter<Saml2AuthenticationRequestContext, AuthnRequest> authenticationRequestContextConverter) { |
| 202 | + Assert.notNull(authenticationRequestContextConverter, "authenticationRequestContextConverter cannot be null"); |
| 203 | + this.authenticationRequestContextConverter = authenticationRequestContextConverter; |
| 204 | + } |
| 205 | + |
| 206 | + /** |
| 207 | + * ' Use this {@link Clock} with {@link Instant#now()} for generating timestamps |
| 208 | + * @param clock |
| 209 | + */ |
| 210 | + public void setClock(Clock clock) { |
| 211 | + Assert.notNull(clock, "clock cannot be null"); |
| 212 | + this.clock = clock; |
| 213 | + } |
| 214 | + |
| 215 | + /** |
| 216 | + * Sets the {@code protocolBinding} to use when generating authentication requests. |
| 217 | + * Acceptable values are {@link SAMLConstants#SAML2_POST_BINDING_URI} and |
| 218 | + * {@link SAMLConstants#SAML2_REDIRECT_BINDING_URI} The IDP will be reading this value |
| 219 | + * in the {@code AuthNRequest} to determine how to send the Response/Assertion to the |
| 220 | + * ACS URL, assertion consumer service URL. |
| 221 | + * @param protocolBinding either {@link SAMLConstants#SAML2_POST_BINDING_URI} or |
| 222 | + * {@link SAMLConstants#SAML2_REDIRECT_BINDING_URI} |
| 223 | + * @throws IllegalArgumentException if the protocolBinding is not valid |
| 224 | + * @deprecated Use |
| 225 | + * {@link org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration.Builder#assertionConsumerServiceBinding(Saml2MessageBinding)} |
| 226 | + * instead |
| 227 | + */ |
| 228 | + @Deprecated |
| 229 | + public void setProtocolBinding(String protocolBinding) { |
| 230 | + boolean isAllowedBinding = SAMLConstants.SAML2_POST_BINDING_URI.equals(protocolBinding) |
| 231 | + || SAMLConstants.SAML2_REDIRECT_BINDING_URI.equals(protocolBinding); |
| 232 | + if (!isAllowedBinding) { |
| 233 | + throw new IllegalArgumentException("Invalid protocol binding: " + protocolBinding); |
| 234 | + } |
| 235 | + this.protocolBindingResolver = (context) -> protocolBinding; |
| 236 | + } |
| 237 | + |
| 238 | + private AuthnRequest sign(AuthnRequest authnRequest, RelyingPartyRegistration relyingPartyRegistration) { |
| 239 | + SignatureSigningParameters parameters = resolveSigningParameters(relyingPartyRegistration); |
| 240 | + return sign(authnRequest, parameters); |
| 241 | + } |
| 242 | + |
| 243 | + private AuthnRequest sign(AuthnRequest authnRequest, SignatureSigningParameters parameters) { |
| 244 | + try { |
| 245 | + SignatureSupport.signObject(authnRequest, parameters); |
| 246 | + return authnRequest; |
| 247 | + } |
| 248 | + catch (Exception ex) { |
| 249 | + throw new Saml2Exception(ex); |
| 250 | + } |
| 251 | + } |
| 252 | + |
| 253 | + private void sign(Map<String, String> components, RelyingPartyRegistration relyingPartyRegistration) { |
| 254 | + SignatureSigningParameters parameters = resolveSigningParameters(relyingPartyRegistration); |
| 255 | + sign(components, parameters); |
| 256 | + } |
| 257 | + |
| 258 | + private void sign(Map<String, String> components, SignatureSigningParameters parameters) { |
| 259 | + Credential credential = parameters.getSigningCredential(); |
| 260 | + String algorithmUri = parameters.getSignatureAlgorithm(); |
| 261 | + components.put("SigAlg", algorithmUri); |
| 262 | + UriComponentsBuilder builder = UriComponentsBuilder.newInstance(); |
| 263 | + for (Map.Entry<String, String> component : components.entrySet()) { |
| 264 | + builder.queryParam(component.getKey(), UriUtils.encode(component.getValue(), StandardCharsets.ISO_8859_1)); |
| 265 | + } |
| 266 | + String queryString = builder.build(true).toString().substring(1); |
| 267 | + try { |
| 268 | + byte[] rawSignature = XMLSigningUtil.signWithURI(credential, algorithmUri, |
| 269 | + queryString.getBytes(StandardCharsets.UTF_8)); |
| 270 | + String b64Signature = Saml2Utils.samlEncode(rawSignature); |
| 271 | + components.put("Signature", b64Signature); |
| 272 | + } |
| 273 | + catch (SecurityException ex) { |
| 274 | + throw new Saml2Exception(ex); |
| 275 | + } |
| 276 | + } |
| 277 | + |
| 278 | + private String serialize(AuthnRequest authnRequest) { |
| 279 | + try { |
| 280 | + Element element = this.marshaller.marshall(authnRequest); |
| 281 | + return SerializeSupport.nodeToString(element); |
| 282 | + } |
| 283 | + catch (MarshallingException ex) { |
| 284 | + throw new Saml2Exception(ex); |
| 285 | + } |
| 286 | + } |
| 287 | + |
| 288 | + private SignatureSigningParameters resolveSigningParameters(RelyingPartyRegistration relyingPartyRegistration) { |
| 289 | + List<Credential> credentials = resolveSigningCredentials(relyingPartyRegistration); |
| 290 | + List<String> algorithms = relyingPartyRegistration.getAssertingPartyDetails().getSigningAlgorithms(); |
| 291 | + List<String> digests = Collections.singletonList(SignatureConstants.ALGO_ID_DIGEST_SHA256); |
| 292 | + String canonicalization = SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS; |
| 293 | + SignatureSigningParametersResolver resolver = new SAMLMetadataSignatureSigningParametersResolver(); |
| 294 | + CriteriaSet criteria = new CriteriaSet(); |
| 295 | + BasicSignatureSigningConfiguration signingConfiguration = new BasicSignatureSigningConfiguration(); |
| 296 | + signingConfiguration.setSigningCredentials(credentials); |
| 297 | + signingConfiguration.setSignatureAlgorithms(algorithms); |
| 298 | + signingConfiguration.setSignatureReferenceDigestMethods(digests); |
| 299 | + signingConfiguration.setSignatureCanonicalizationAlgorithm(canonicalization); |
| 300 | + criteria.add(new SignatureSigningConfigurationCriterion(signingConfiguration)); |
| 301 | + try { |
| 302 | + SignatureSigningParameters parameters = resolver.resolveSingle(criteria); |
| 303 | + Assert.notNull(parameters, "Failed to resolve any signing credential"); |
| 304 | + return parameters; |
| 305 | + } |
| 306 | + catch (Exception ex) { |
| 307 | + throw new Saml2Exception(ex); |
| 308 | + } |
| 309 | + } |
| 310 | + |
| 311 | + private List<Credential> resolveSigningCredentials(RelyingPartyRegistration relyingPartyRegistration) { |
| 312 | + List<Credential> credentials = new ArrayList<>(); |
| 313 | + for (Saml2X509Credential x509Credential : relyingPartyRegistration.getSigningX509Credentials()) { |
| 314 | + X509Certificate certificate = x509Credential.getCertificate(); |
| 315 | + PrivateKey privateKey = x509Credential.getPrivateKey(); |
| 316 | + BasicCredential credential = CredentialSupport.getSimpleCredential(certificate, privateKey); |
| 317 | + credential.setEntityId(relyingPartyRegistration.getEntityId()); |
| 318 | + credential.setUsageType(UsageType.SIGNING); |
| 319 | + credentials.add(credential); |
| 320 | + } |
| 321 | + return credentials; |
| 322 | + } |
| 323 | + |
| 324 | +} |
0 commit comments