2020import com .netflix .spinnaker .kork .annotations .NullableByDefault ;
2121import com .netflix .spinnaker .kork .exceptions .ConfigurationException ;
2222import java .io .IOException ;
23+ import java .io .InputStream ;
2324import java .nio .file .Files ;
2425import java .nio .file .Path ;
2526import java .security .GeneralSecurityException ;
2627import java .security .KeyStore ;
2728import java .security .PrivateKey ;
29+ import java .security .cert .CertificateFactory ;
2830import java .security .cert .X509Certificate ;
31+ import java .security .interfaces .RSAPrivateKey ;
2932import java .util .Enumeration ;
33+ import java .util .LinkedList ;
3034import java .util .List ;
3135import java .util .Set ;
3236import java .util .TreeSet ;
3337import javax .annotation .Nonnull ;
3438import javax .annotation .PostConstruct ;
3539import javax .validation .constraints .NotEmpty ;
40+ import lombok .Data ;
3641import lombok .Getter ;
42+ import lombok .NoArgsConstructor ;
3743import lombok .Setter ;
3844import org .springframework .boot .context .properties .ConfigurationProperties ;
3945import 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 ;
4050import org .springframework .security .saml2 .core .Saml2X509Credential ;
4151import org .springframework .util .StringUtils ;
4252import org .springframework .validation .annotation .Validated ;
4757@ ConfigurationProperties ("saml" )
4858@ NullableByDefault
4959public 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}
0 commit comments