Issue
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.
Solution
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;
}
Answered By - devatherock
Answer Checked By - Mary Flores (JavaFixing Volunteer)