Skip to content
This repository was archived by the owner on Dec 20, 2025. It is now read-only.

Commit 1192dc1

Browse files
fix(saml): With the upgrade to spring saml off of the DSL signing saml requests were broken. This restores that capability (#1898) (#1901)
(cherry picked from commit 0fb605c) Co-authored-by: Jason <jason.mcintosh@harness.io>
1 parent 1af7f70 commit 1192dc1

File tree

6 files changed

+237
-3
lines changed

6 files changed

+237
-3
lines changed

gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SAMLConfiguration.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,13 @@ public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() {
8787
if (decryptionCredential != null) {
8888
builder.decryptionX509Credentials(credentials -> credentials.add(decryptionCredential));
8989
}
90+
// This is used in some identity providers to sign the request. NOT the response - response
91+
// is handled via the certs in metadata or up above. This is keycloak and some others
92+
// TO USE THIS: The certificate should be uploaded to the IDP to allow it to decrypt these
93+
// requests
94+
if (properties.isSignRequests()) {
95+
builder.signingX509Credentials(c -> c.addAll(properties.getSigningCredentials()));
96+
}
9097
RelyingPartyRegistration registration = builder.build();
9198
return new InMemoryRelyingPartyRegistrationRepository(registration);
9299
}

gate-saml/src/main/java/com/netflix/spinnaker/gate/security/saml/SecuritySamlProperties.java

Lines changed: 147 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,23 +20,33 @@
2020
import com.netflix.spinnaker.kork.annotations.NullableByDefault;
2121
import com.netflix.spinnaker.kork.exceptions.ConfigurationException;
2222
import java.io.IOException;
23+
import java.io.InputStream;
2324
import java.nio.file.Files;
2425
import java.nio.file.Path;
2526
import java.security.GeneralSecurityException;
2627
import java.security.KeyStore;
2728
import java.security.PrivateKey;
29+
import java.security.cert.CertificateFactory;
2830
import java.security.cert.X509Certificate;
31+
import java.security.interfaces.RSAPrivateKey;
2932
import java.util.Enumeration;
33+
import java.util.LinkedList;
3034
import java.util.List;
3135
import java.util.Set;
3236
import java.util.TreeSet;
3337
import javax.annotation.Nonnull;
3438
import javax.annotation.PostConstruct;
3539
import javax.validation.constraints.NotEmpty;
40+
import lombok.Data;
3641
import lombok.Getter;
42+
import lombok.NoArgsConstructor;
3743
import lombok.Setter;
3844
import org.springframework.boot.context.properties.ConfigurationProperties;
3945
import org.springframework.boot.context.properties.NestedConfigurationProperty;
46+
import org.springframework.core.io.DefaultResourceLoader;
47+
import org.springframework.core.io.Resource;
48+
import org.springframework.core.io.ResourceLoader;
49+
import org.springframework.security.converter.RsaKeyConverters;
4050
import org.springframework.security.saml2.core.Saml2X509Credential;
4151
import org.springframework.util.StringUtils;
4252
import org.springframework.validation.annotation.Validated;
@@ -47,11 +57,37 @@
4757
@ConfigurationProperties("saml")
4858
@NullableByDefault
4959
public class SecuritySamlProperties {
60+
public static final String FILE_PREFIX = "file:";
5061
private Path keyStore;
5162
private String keyStoreType = "PKCS12";
5263
private String keyStorePassword;
5364
private String keyStoreAliasName = "mykey"; // default alias for keytool
5465

66+
// the privatekey/cert location files can be generated via
67+
// openssl req -new -x509 -nodes -keyout private_key.pem -out certificate.pem -subj
68+
// "/CN=Spinnaker" -days 3650
69+
@Data
70+
@NoArgsConstructor
71+
public static class Credential {
72+
private String privateKeyLocation;
73+
private String certificateLocation;
74+
75+
public Credential(String privateKeyLocation, String certificateLocation) {
76+
setCertificateLocation(certificateLocation);
77+
setPrivateKeyLocation(privateKeyLocation);
78+
}
79+
80+
public void setCertificateLocation(String certificateLocation) {
81+
this.certificateLocation = addFilePrefixIfNeeded(certificateLocation);
82+
}
83+
84+
public void setPrivateKeyLocation(String privateKeyLocation) {
85+
this.privateKeyLocation = addFilePrefixIfNeeded(privateKeyLocation);
86+
}
87+
}
88+
89+
private List<Credential> signingCredentials = new LinkedList<>();
90+
5591
public Saml2X509Credential getDecryptionCredential()
5692
throws IOException, GeneralSecurityException {
5793
if (keyStore == null) {
@@ -71,6 +107,82 @@ public Saml2X509Credential getDecryptionCredential()
71107
return Saml2X509Credential.decryption(privateKey, certificate);
72108
}
73109

110+
private static String addFilePrefixIfNeeded(String property) {
111+
if (StringUtils.hasLength(property) && property.startsWith("/")) {
112+
return FILE_PREFIX + property;
113+
}
114+
return property;
115+
}
116+
// @formatter:off
117+
/**
118+
* Try to match the standard spring saml config LIKE the below. This would be a keylcoak
119+
* configuration example. Note that this does NOT match, but at least the private key suff does. *
120+
* <br>
121+
* <!-- @formatter:off -->
122+
*
123+
* <pre>
124+
* spring:
125+
* security:
126+
* saml2:
127+
* ## We ARE a relying party.
128+
* relyingparty:
129+
* registration:
130+
* SSO:
131+
* ## This would be the SSO provider information. Asserting party would be who's sending the SAML payload (e.g. okta/keycloak/etc)
132+
* assertingparty:
133+
* metadata-uri: http://192.168.1.2:8080/realms/master/protocol/saml/descriptor
134+
* entityId: Spinnaker
135+
* ## This uses the signing credentials in a separate block. Why it's there vs. here is strange
136+
* singlesignon:
137+
* sign-request: true
138+
* url: http://192.168.1.2:8080/realms/master/protocol/saml
139+
* ## This IF metadata-uri is set SHOULD NOT be needed as it's PARSED as part of the operation
140+
* ## this is the decryptionCredentials
141+
* verification:
142+
* ## Nominally this could be in a JKS or a P12 keystore as well. REMINDER: this is basically
143+
* ## coming from the metadata in NORMAL cases.
144+
* credentials:
145+
* - certificate-location: encryptedFile:k8s!n:saml-verification-secret!k:certificate.pem
146+
* private-key-location: encryptedFile:k8s!n:saml-verification-secret!k:private_key.pem
147+
* signing:
148+
* credentials:
149+
* - certificate-location: encryptedFile:k8s!n:saml-signing-secret!k:certificate.pem
150+
* private-key-location: encryptedFile:k8s!n:saml-signing-secret!k:private_key.pem
151+
*
152+
* </pre>
153+
*
154+
* *
155+
* <!-- @formatter:on -->
156+
* NOTE: We ONLY support a single credential with this config.
157+
*
158+
* @return the credentials piece based upon privateKey/certificate
159+
*/
160+
// @formatter:on
161+
// TODO: Replace this entire thing with standard spring security saml loading vs. doing all this
162+
// ourselves
163+
// See:
164+
// https://docs.spring.io/spring-boot/api/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyProperties.Registration.html for the auto wiring based approach
165+
@Nonnull
166+
public List<Saml2X509Credential> getSigningCredentials() {
167+
if (this.signingCredentials != null && !signingCredentials.isEmpty()) {
168+
return this.signingCredentials.stream()
169+
.map(
170+
each ->
171+
getSaml2Credential(
172+
each.getPrivateKeyLocation(),
173+
each.getCertificateLocation(),
174+
Saml2X509Credential.Saml2X509CredentialType.SIGNING))
175+
.toList();
176+
}
177+
return new LinkedList<>();
178+
}
179+
180+
/**
181+
* Sign requests via the WantAuthnRequestsSigned XML flag. Defaults to false. Keycloak defaults to
182+
* true, okta DOES NOT support true. Uses the signing credentials
183+
*/
184+
private boolean signRequests = false;
185+
74186
/** URL pointing to the SAML metadata to use. */
75187
private String metadataUrl;
76188

@@ -112,9 +224,7 @@ public String getAssertionConsumerServiceLocation() {
112224

113225
@PostConstruct
114226
public void validate() throws IOException, GeneralSecurityException {
115-
if (StringUtils.hasLength(metadataUrl) && metadataUrl.startsWith("/")) {
116-
metadataUrl = "file:" + metadataUrl;
117-
}
227+
metadataUrl = addFilePrefixIfNeeded(metadataUrl);
118228
if (keyStore != null) {
119229
if (keyStoreType == null) {
120230
keyStoreType = "PKCS12";
@@ -159,4 +269,38 @@ public static class UserAttributeMapping {
159269
private String username;
160270
private String email;
161271
}
272+
273+
/// Filched DIRECTLY from spring-security private temporarily until all of this is replaced
274+
// directly with the autowired spring configuration stuff
275+
///
276+
// https://github.com/spring-projects/spring-security/blob/main/config/src/main/java/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParser.java
277+
private static final ResourceLoader resourceLoader = new DefaultResourceLoader();
278+
279+
private static Saml2X509Credential getSaml2Credential(
280+
String privateKeyLocation,
281+
String certificateLocation,
282+
Saml2X509Credential.Saml2X509CredentialType credentialType) {
283+
RSAPrivateKey privateKey = readPrivateKey(privateKeyLocation);
284+
X509Certificate certificate = readCertificate(certificateLocation);
285+
return new Saml2X509Credential(privateKey, certificate, credentialType);
286+
}
287+
288+
private static RSAPrivateKey readPrivateKey(String privateKeyLocation) {
289+
Resource privateKey = resourceLoader.getResource(privateKeyLocation);
290+
try (InputStream inputStream = privateKey.getInputStream()) {
291+
return RsaKeyConverters.pkcs8().convert(inputStream);
292+
} catch (Exception ex) {
293+
throw new IllegalArgumentException(ex);
294+
}
295+
}
296+
297+
private static X509Certificate readCertificate(String certificateLocation) {
298+
Resource certificate = resourceLoader.getResource(certificateLocation);
299+
try (InputStream inputStream = certificate.getInputStream()) {
300+
return (X509Certificate)
301+
CertificateFactory.getInstance("X.509").generateCertificate(inputStream);
302+
} catch (Exception ex) {
303+
throw new IllegalArgumentException(ex);
304+
}
305+
}
162306
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.netflix.spinnaker.gate.security.saml;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import java.nio.file.Path;
6+
import java.nio.file.Paths;
7+
import java.util.List;
8+
import org.junit.jupiter.api.Test;
9+
import org.springframework.security.saml2.core.Saml2X509Credential;
10+
11+
class SecuritySamlPropertiesTest {
12+
13+
@Test
14+
public void verifyCanLoadCerts() {
15+
SecuritySamlProperties properties = new SecuritySamlProperties();
16+
properties.setSigningCredentials(
17+
List.of(
18+
new SecuritySamlProperties.Credential(
19+
"classpath:private_key.pem", "classpath:certificate.pem")));
20+
List<Saml2X509Credential> signingCredentials = properties.getSigningCredentials();
21+
assertThat(signingCredentials.get(0).getPrivateKey().getAlgorithm()).isEqualTo("RSA");
22+
}
23+
24+
@Test
25+
public void verifyCanLoadCertsFromAFileLocation() {
26+
SecuritySamlProperties properties = new SecuritySamlProperties();
27+
Path currentDir = Paths.get("");
28+
properties.setSigningCredentials(
29+
List.of(
30+
new SecuritySamlProperties.Credential(
31+
currentDir.toAbsolutePath() + "/src/test/resources/private_key.pem",
32+
currentDir.toAbsolutePath() + "/src/test/resources/certificate.pem")));
33+
List<Saml2X509Credential> signingCredentials = properties.getSigningCredentials();
34+
assertThat(signingCredentials.get(0).getPrivateKey().getAlgorithm()).isEqualTo("RSA");
35+
}
36+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDCTCCAfGgAwIBAgIUOevToAcJr3FsI/MsoEq75EwMt5QwDQYJKoZIhvcNAQEL
3+
BQAwFDESMBAGA1UEAwwJU3Bpbm5ha2VyMB4XDTI1MDUwOTIwNTE0OFoXDTM1MDUw
4+
NzIwNTE0OFowFDESMBAGA1UEAwwJU3Bpbm5ha2VyMIIBIjANBgkqhkiG9w0BAQEF
5+
AAOCAQ8AMIIBCgKCAQEAryEo1o+YyVssgFuI5N5U3eQyvAoGbtXCeaCoR1LltAqq
6+
SrDGyrzJnB3kXtd0RieGK0AxBg+FgL/Xl8kw+p+72h7gW8XYi9gS92fGYVyoY9nz
7+
c9WueQpazzfuQjx5UuDqZn59f6EsAEAwzIAep9YHwysAuma8PYlNzqnhTWTu5tJG
8+
SvCD5kv11njYHJJ1ntIaUHGNlpHH2rAPWKcf8XeqBfvFgBHDLRpuj+2XOijwY6uz
9+
iOgnsViOG/NbC7zBbpkwELoWcxsh3msLs4HIs6DRULtQq6EUnvjx72pwM14GlQ+9
10+
ZrcnxtX7qQ4ODf2rNpobmoBkE2mfV72vNS4J5XpltwIDAQABo1MwUTAdBgNVHQ4E
11+
FgQU9I5Nl3n/D1gdrOapVfpRUTUSfPwwHwYDVR0jBBgwFoAU9I5Nl3n/D1gdrOap
12+
VfpRUTUSfPwwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAfW58
13+
p9z8T+OUbi284Eejvy+k4hDvCB3+90GoiXrVQvhBlfXr9eO4rzkuOQe9Qw4jVf47
14+
RBwvUuen+gOMo6zx+PK7wKTZqmluKzPf5ic1XOl7M9wy14g84bBWP5EniJdqzpup
15+
WQK58sYWZc0QrpwGbWIkr7QQGju0F4nte8at/MvK9uHmtK5j6Rg3axPjLInIAuXq
16+
d/+H+CKRbybwS/ZCp37rjD9pyVYYIQ7ZZt/0EZCAaAKPeZ6RFhIguIpx1Ww5mMrF
17+
JK1LnWPuzr8R30SoH/BRQwU/sM7cvYsmhVsKNPfaaEbakqfo+LtSD+yrtRvbCo69
18+
c1ZqBjsV5B1736V+CQ==
19+
-----END CERTIFICATE-----
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCvISjWj5jJWyyA
3+
W4jk3lTd5DK8CgZu1cJ5oKhHUuW0CqpKsMbKvMmcHeRe13RGJ4YrQDEGD4WAv9eX
4+
yTD6n7vaHuBbxdiL2BL3Z8ZhXKhj2fNz1a55ClrPN+5CPHlS4Opmfn1/oSwAQDDM
5+
gB6n1gfDKwC6Zrw9iU3OqeFNZO7m0kZK8IPmS/XWeNgcknWe0hpQcY2WkcfasA9Y
6+
px/xd6oF+8WAEcMtGm6P7Zc6KPBjq7OI6CexWI4b81sLvMFumTAQuhZzGyHeawuz
7+
gcizoNFQu1CroRSe+PHvanAzXgaVD71mtyfG1fupDg4N/as2mhuagGQTaZ9Xva81
8+
LgnlemW3AgMBAAECggEAAOs3ZIFN+b6p13ZMxTXacJWmyK9CVCfDA5pYqbyNgfdf
9+
15sfDUyrCf0xYqpBnnZIwKmpMwW/TwPHTCS7BB5gS0EWaPZmelm8ilF/KRz2h+op
10+
fz4+f6ynXLvLteyK+ibCEPkUVoMuQEXT4OBWsA+0NqhGfN0pccvqdTOZnOlFt/0A
11+
yv2GfYLlP13nreU+u4efE7/i+E8xvUxdwH4sLSuKMIB/0aYI6LbdwJbqh5KMBLVt
12+
u+ooXRk8atsK7OucpSuo5Wj5jzUJV31PfYtpcnVnatLMjPAA+LQAYDrZkHLMXvv8
13+
opuyHExZlJ5iWx6tViQz0B1hrWcCIHS7cvx5+D/a6QKBgQDb9Crz3AOeZH3IR6Ha
14+
Zws4rhlqtq5Co8DAyTHE62XWkdTY4hhtfhvQ+8YH9jzU+fIBS39QVlM73MmPR4ga
15+
GtSTtsRNN1Oukouq+QvsXx9qR0XEoRBofVmo5VQilosZmqlAjc1vGCyDHpszCUOG
16+
pMVP/2lEiknheSCZobK9XfhQlQKBgQDL1HUPKQaR2iBHPnri7tZOD2K4c8jrwwVX
17+
YjGuUxyzyrnQq8XPkNolatPXxRoqIyqDDfXCafIC8g/BkAY9TxLhoyQEnN6aRODV
18+
ds3wKy/Dn09YaH4BtPbXaNv5cz6JkHtgtl6z6EXXaInb5GATGaHEw9KpH4/eWwIH
19+
A92iZyrOGwKBgQCaleSKNxsj+ySb2hxazwkH8PRUF8gpdcVGuSCNcZPFVgDt3Rml
20+
+ne6TPlFJz5hwLjhSBpWcBVXgTj3xiJVln3Iwy77xeK+UqhupVJH8iK2IxlZtIk/
21+
prmZBnQ3Su7AM/64K/EyHx9Jl/0jxWL8Alnae3uUfEyoduT+lLJ2fNDEcQKBgAnc
22+
vsk7//BgsH0h/corKj1eqzUnjQozRnfi7Wp05QeiAHmjRg/z/0oeMB/ZjpmJWA49
23+
R63feHFCCxcfg93FjLFUNnLusCqguIw7kl1TiZ0agTlS3P3yJptnnHUmaVk4n2+f
24+
g1eLHo38peb41tk1vUkK/I9oUoq8to1mV3v7J+wPAoGBALbIYJ4O8FfclXR+63TK
25+
36FYlcHL2u0JjGXyByLM8x6xv9eHeFfij66k4OxWC9vpr3fO/AkbGWnceiztjca5
26+
lejCMjQkU7qbNRAr+XUuk9d6qD+sHd1XLYeXNY9WW9Ki8xbms9C7q/C/nq0uWyHu
27+
KyrlMN7DaB1PfNSRFDPBIEgv
28+
-----END PRIVATE KEY-----
2.49 KB
Binary file not shown.

0 commit comments

Comments
 (0)