Skip to content

Commit 3e05e0d

Browse files
committed
make SP NameIDPolicy configurable in RelyingPartyRegistration
closes spring-projectsgh-9115
1 parent 0541341 commit 3e05e0d

File tree

7 files changed

+688
-0
lines changed

7 files changed

+688
-0
lines changed
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
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+
}

saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolver.java

+14
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.opensaml.saml.saml2.metadata.AssertionConsumerService;
3232
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
3333
import org.opensaml.saml.saml2.metadata.KeyDescriptor;
34+
import org.opensaml.saml.saml2.metadata.NameIDFormat;
3435
import org.opensaml.saml.saml2.metadata.SPSSODescriptor;
3536
import org.opensaml.saml.saml2.metadata.SingleLogoutService;
3637
import org.opensaml.saml.saml2.metadata.impl.EntityDescriptorMarshaller;
@@ -86,7 +87,13 @@ private SPSSODescriptor buildSpSsoDescriptor(RelyingPartyRegistration registrati
8687
spSsoDescriptor.getKeyDescriptors()
8788
.addAll(buildKeys(registration.getDecryptionX509Credentials(), UsageType.ENCRYPTION));
8889
spSsoDescriptor.getAssertionConsumerServices().add(buildAssertionConsumerService(registration));
90+
<<<<<<< Upstream, based on upstream/main
8991
spSsoDescriptor.getSingleLogoutServices().add(buildSingleLogoutService(registration));
92+
=======
93+
if (registration.getNameIDFormat() != null) {
94+
spSsoDescriptor.getNameIDFormats().add(buildNameIDFormat(registration));
95+
}
96+
>>>>>>> 7056a31 make SP NameIDPolicy configurable in RelyingPartyRegistration
9097
return spSsoDescriptor;
9198
}
9299

@@ -125,12 +132,19 @@ private AssertionConsumerService buildAssertionConsumerService(RelyingPartyRegis
125132
return assertionConsumerService;
126133
}
127134

135+
<<<<<<< Upstream, based on upstream/main
128136
private SingleLogoutService buildSingleLogoutService(RelyingPartyRegistration registration) {
129137
SingleLogoutService singleLogoutService = build(SingleLogoutService.DEFAULT_ELEMENT_NAME);
130138
singleLogoutService.setLocation(registration.getSingleLogoutServiceLocation());
131139
singleLogoutService.setResponseLocation(registration.getSingleLogoutServiceResponseLocation());
132140
singleLogoutService.setBinding(registration.getSingleLogoutServiceBinding().getUrn());
133141
return singleLogoutService;
142+
=======
143+
private NameIDFormat buildNameIDFormat(RelyingPartyRegistration registration) {
144+
NameIDFormat nameIDFormat = build(NameIDFormat.DEFAULT_ELEMENT_NAME);
145+
nameIDFormat.setFormat(registration.getNameIDFormat());
146+
return nameIDFormat;
147+
>>>>>>> 7056a31 make SP NameIDPolicy configurable in RelyingPartyRegistration
134148
}
135149

136150
@SuppressWarnings("unchecked")

0 commit comments

Comments
 (0)