Search code examples
spring-bootspring-securitysaml-2.0spring-saml

Creating SAML repository registrations dynamically in Spring Security (Spring Boot)


I have created a sample project which can demonstrate SAML 2 SSO capabilities with saml providers such as Azure AD and Okta. I was able to configure both of above providers at once in spring configuration by using RelyingPartyRegistrationRepository and both of them are working as expected.

@Bean
protected RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception {
    RelyingPartyRegistration oktaRegistration = RelyingPartyRegistrations.fromMetadataLocation("https://trial-27.okta.com/app/e/sso/saml/metadata").registrationId("okta").build();
    RelyingPartyRegistration azureRegistration = RelyingPartyRegistrations.fromMetadataLocation("file:D:\\saml-test-5.xml").registrationId("azure-saml-test").build();
    List<RelyingPartyRegistration> registrationList = new ArrayList<>();
    registrationList.add(oktaRegistration);
    registrationList.add(azureRegistration);
    return new InMemoryRelyingPartyRegistrationRepository(registrationList);
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
            .authorizeRequests(authorize ->
                    authorize.antMatchers("/").permitAll().anyRequest().authenticated()
            ).saml2Login();

    
    RelyingPartyRegistrationResolver relyingPartyRegistrationResolver = new DefaultRelyingPartyRegistrationResolver(relyingPartyRegistrations());
    Saml2MetadataFilter filter = new Saml2MetadataFilter(relyingPartyRegistrationResolver, new OpenSamlMetadataResolver());
    http.addFilterBefore(filter, Saml2WebSsoAuthenticationFilter.class);
    return http.build();
}

I would like to know whether there is any way to create RelyingPartyRegistrationRepository dynamically once the application fully started. The requirement is to take the SAML metadata file from user in some sort of a form upload and then create RelyingPartyRegistrationRepository based on it. The issue is, RelyingPartyRegistrationRepository is a Spring bean which is used by the Spring security internals. In this case even though we could create new RelyingPartyRegistrationRepository instances, will Spring security take them dynamically?


Solution

  • You will not create multiple RelyingPartyRegistrationRepository, you will create your custom implementation of RelyingPartyRegistrationRepository that accepts adding new entries to it. A simple example:

    @Service
    public class MyRelyingPartyRegistrationRepository implements RelyingPartyRegistrationRepository {
    
        private final List<RelyingPartyRegistration> registrations = new ArrayList<>();
    
        public MyRelyingPartyRegistrationRepository() {
            addDefaultRegistrations();
        }
    
        private void addDefaultRegistrations() {
            RelyingPartyRegistration oktaRegistration = RelyingPartyRegistrations.fromMetadataLocation("https://trial-27.okta.com/app/e/sso/saml/metadata").registrationId("okta").build();
            RelyingPartyRegistration azureRegistration = RelyingPartyRegistrations.fromMetadataLocation("file:D:\\saml-test-5.xml").registrationId("azure-saml-test").build();
            add(oktaRegistration);
            add(azureRegistration);
        }
    
        @Override
        public RelyingPartyRegistration findByRegistrationId(String registrationId) {
            for (RelyingPartyRegistration registration : this.registrations) {
                if (registration.getRegistrationId().equals(registrationId)) {
                    return  registration;
                }
            }
            return null;
        }
    
        public void add(RelyingPartyRegistration newRegistration) {
            this.registrations.add(newRegistration);
        }
    
    }
    

    And then in a Controller, for example, you can autowire this dependency and add new registrations to it:

    @RestController
    public class SamlController {
    
        private final MyRelyingPartyRegistrationRepository repository;
    
        @PostMapping("/registration")
        public void addRegistration(/* receive it somehow */) {
            this.repository.add(theRegistration);
        }
    
    }