1

I'm using Spring Boot 2.4. In my WebSecurityConfig (extends WebSecurityConfigurerAdapter) I have the following piece of code for our dev environment:

@Bean
RelyingPartyRegistrationRepository replyingPartyRegistrationRepository() {
    System.err.println("Metadatalocation is ||" + metadataLocation + "||");
    RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
            .fromMetadataLocation(metadataLocation)
            .registrationId("my-id-here")
            .build();
    return new InMemoryRelyingPartyRegistrationRepository(relyingPartyRegistration);
}

and for staging/production I'd need this version (with key and certificate) based off https://github.com/spring-projects/spring-security-samples/blob/b2310d91fe198138d07bad17b5c86a2f13b698ae/servlet/spring-boot/java/saml2/login/src/main/java/example/SecurityConfiguration.java#L76 because we'll connect with the real idp there.

@Bean
RelyingPartyRegistrationRepository replyingPartyRegistrationRepository(
    @Value("classpath:credentials/tls.key") RSAPrivateKey privateKey) {
        RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
            .fromMetadataLocation(metadataLocation)
            .registrationId("my-other-id-here")
            .signingX509Credentials(
                (c) -> c.add(Saml2X509Credential.signing(privateKey, relyingPartyCertificate())))
            .build();
        return new InMemoryRelyingPartyRegistrationRepository(relyingPartyRegistration);
}

My issue is how to integrate this into one method that can work for both environments. Executing the right code for either version (with and without key/certificate) can be done using an if statement based on application.properties value indicating the environment (or perhaps simply by checking if the RSAPrivateKey exists in this environment or not)

The issue, however, is the injection @Value("classpath:credentials/tls.key") RSAPrivateKey privateKey which will throw an error if that classpath location does not exist.

attempted solutions

  • I've tried passing a default value using @Value("#{\"classpath:credentials/tls.key\" ?: null}") (and many variations of that) hoping it would insert 'null' if the file cannot be found, but I guess the File Not Found exception occurs as soon as the attempt is made so this doesn't work.

  • I've also tried varations of Resource resource = resourceLoader.getResource("classpath:credentials/tls.key"); but then how do I convert that Resource into a RSAPrivateKey?

  • I suppose a workaround is to put a fake/empty RSA key file in that location in the dev environment, have it be loaded, and then ignore it, but that feels very hacky..

PS: Part of my problem is that I don't understand how Spring Boot can even 'cast'/convert a classpath file to an RSAPrivateKey object? It works because the injection does succeed if an RSA file is present. I can't seem to find any documentation on how the classpath:-prefix actually does its magic. All examples of @Value("classpath:...") involve loading a Resource

Thank you for any insights you could offer.

RoninTDK
  • 33
  • 4

1 Answers1

1

You can keep both bean definition methods separate and activate them conditionally based on the environment using ConditionalOnProperty annotation. The code would look like below:

@Bean
@ConditionalOnProperty(name = "env.name", havingValue = "dev")
RelyingPartyRegistrationRepository replyingPartyRegistrationRepositoryDev() {
    System.err.println("Metadatalocation is ||" + metadataLocation + "||");
    RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
            .fromMetadataLocation(metadataLocation)
            .registrationId("my-id-here")
            .build();
    return new InMemoryRelyingPartyRegistrationRepository(relyingPartyRegistration);
}

@Bean
@ConditionalOnMissingBean
RelyingPartyRegistrationRepository replyingPartyRegistrationRepositoryNonDev(
    @Value("classpath:credentials/tls.key") RSAPrivateKey privateKey) {
        RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
            .fromMetadataLocation(metadataLocation)
            .registrationId("my-other-id-here")
            .signingX509Credentials(
                (c) -> c.add(Saml2X509Credential.signing(privateKey, relyingPartyCertificate())))
            .build();
        return new InMemoryRelyingPartyRegistrationRepository(relyingPartyRegistration);
}

When the environment is dev, the env.name property needs to be set to dev and then the bean will be initialized using the replyingPartyRegistrationRepositoryDev method which doesn't require a RSAPrivateKey. When the environment is not dev, the bean will be initialized using the replyingPartyRegistrationRepositoryNonDev method with RSAPrivateKey. This way there won't be a need to add an empty RSA key file in dev.

UPDATE:

A bean definition like below could be used to initialize a bean one way when a file exists and another when it doesn't, all in the same method:

@Bean
public Resource resourceDefault(@Value("file:logback.xml") Resource inputResource) {
    Resource resource = null;

    if (inputResource.exists()) {
        resource = inputResource;
    } else {
        resource = new ClassPathResource("logback-dev.xml");
    }

    return resource;
}
devatherock
  • 2,423
  • 1
  • 8
  • 23
  • This solves this particular use-case (for which many thanks!) but I imagine there are other cases where one might want to inject a file IF it exists, and its existence cannot be determined ahead of time (and thus cannot be configured in a property). Would you happen to know a solution / pattern for that more general use-case too? – RoninTDK Aug 16 '22 at 00:03
  • Looks like Spring doesn't throw an exception when injecting a file that doesn't exist, at least not when injecting it to a `Resource` variable. Also `Resource` class offers an `exists()` method. So using both, we could add some logic for when the file doesn't exist. Will add a sample code snippet – devatherock Aug 16 '22 at 00:54
  • I tried to go the Resource route but then I couldn't convert that Resource into the RSAPrivateKey. I have now found that you can do it manually with some extra code (e.g. https://stackoverflow.com/questions/11410770/load-rsa-public-key-from-file, though that example uses File and PrivateKey and I have not tested with Resource and RSAPrivateKey). Using @Value the conversion appears to somehow happen automatically, but I suppose having to convert manually is just a mild inconvenience. Thank you for the additional update. – RoninTDK Aug 17 '22 at 11:18