We have hundreds of customers registered their ADFS with our application. At the time of login based on their EMail domain the application should automatically fetch the federated xml and redirects the user to the ADFS login page / URL belongs to the customer for the authentication. This is already implemented using spring-security-extensions library. Now we are migrating to spring-security-service-provider library. Is there any examples available that how a federated metadata xml can be loaded dynamically using RelyingPartyRegistration class

Comment From: abinesh-s

@jzheaux - could you please help me by pointing to right direction

Comment From: jzheaux

Happy to help, @abinesh-s. I think you can achieve this with minimal customization.

First, create a class that implements RelyingPartyRegistrationRepository, including a method for lookup by email domain. This will replace the Spring Boot default RelyingPartyRegistrationRepository like so:

@Component 
public class DynamicRelyingPartyRegistrationRepository implements RelyingPartyRegistrationRepository {
    private final Map<String, RelyingPartyRegistration> registrations = new HashMap<>();

    @Override 
    public RelyingPartyRegistration findByRegistrationId(String registrationId) {
        return this.registrations.get(registrationId);
    }

    public RelyingPartyRegistration computeByEmailDomain(String domain) {
        String metadtaLocation = computeMetadataLocation(domain);
        RelyingPartyRegistration registration = RelyingPartyRegistrations
                .fromMetadataLocation(metadataLocation).registrationId(domain).build();
        this.registrations.put(domain, registration);
        return registration;
    }    
}

Then wire it into a custom controller:

@Controller 
public class LoginController {
    @Autowired
    private DynamicRelyingPartyRegistrationRepository registrations;

    @PostMapping("/discovery")
    public String discovery(@RequestParam("email") String email) {
        String domain = computeDomain(email);
        RelyingPartyRegistration registration = this.registrations.computeByEmailDomain(domain);
        return "redirect:/saml2/authenticate/" + registration.getRegistrationId();
    }
}

This will invoke Spring Securiy's AuthnRequest filter for the registration tied to that domain.

Would this approach work for you?

Comment From: abinesh-s

Hi @jzheaux , thanks for the reply. That was helpful to make some progress. But, I stuck with below issue and have added my code here

My controller

package com.sample.test;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class HelloController {

    @Autowired
    private DynamicRelyingPartyRegistrationRepository registrations;

    private static final Logger logger = LoggerFactory.getLogger(HelloController.class);

    @GetMapping("/samlLogin")
    public String samlLogin(Model model, @AuthenticationPrincipal Saml2AuthenticatedPrincipal principal) {
        RelyingPartyRegistration registration = this.registrations.computeByEmailDomain(null);
        logger.info("Asserting Party Details - Single Sign on Service");
        logger.info(registration.getAssertingPartyDetails().getSingleSignOnServiceLocation());
//      return "redirect:"+registration.getAssertingPartyDetails().getSingleSignOnServiceLocation();
        return "redirect:/saml2/authenticate/" + registration.getRegistrationId();
    }

    @GetMapping("/loginPage")
    public String index(Model model, @AuthenticationPrincipal Saml2AuthenticatedPrincipal principal) {
        String emailAddress = principal.getFirstAttribute("email");
        model.addAttribute("emailAddress", emailAddress);
        model.addAttribute("userAttributes", principal.getAttributes());
        return "index";
    }

    @GetMapping("/")
    public ModelAndView welcomePage() {
        ModelAndView model = new ModelAndView();
        model.setViewName("welcomePage");
        return model;
    }

}

DynamicRelyingPartyRegistrationRepository

package com.sample.test;

import java.util.HashMap;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrations;
import org.springframework.stereotype.Component;

@Component
public class DynamicRelyingPartyRegistrationRepository implements RelyingPartyRegistrationRepository {

    private final Map<String, RelyingPartyRegistration> registrations = new HashMap<>();

    private static final Logger logger = LoggerFactory.getLogger(HelloController.class);

    @Override
    public RelyingPartyRegistration findByRegistrationId(String registrationId) {
        return this.registrations.get(registrationId);
    }

    public RelyingPartyRegistration computeByEmailDomain(String domain) {   
        String metadataLocation = "http://localhost:8080/getMetadata";
        String assertionConsumerServiceLocation = "https://w10cvslfg3.blr.apac.com:8443/SpringMVCNew/login/saml2/sso/SpringMVCNew";
        RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
                .fromMetadataLocation(metadataLocation)
                .registrationId("SpringMVCNew")
                .assertionConsumerServiceLocation(assertionConsumerServiceLocation)
                .build();
        logger.info("Inside Dynamic Relying Party Registration : "+relyingPartyRegistration.getAssertionConsumerServiceLocation());
        return relyingPartyRegistration;
    }

}

Security.xml

<b:beans xmlns="http://www.springframework.org/schema/security"
         xmlns:b="http://www.springframework.org/schema/beans"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
                        http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd">

    <http auto-config="true">
        <intercept-url pattern="/samlLogin" access="authenticated"/>
        <saml2-login />
        <saml2-logout />
    </http>

    <user-service>
        <user name="user" password="{noop}password" authorities="ROLE_USER" />
    </user-service>

</b:beans>

spring-servlet.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <!-- Provide support for component scanning -->
    <context:component-scan base-package="com.sample.test" />

    <!--Provide support for conversion, formatting and validation -->
    <mvc:annotation-driven/>
    <mvc:annotation-driven>
        <mvc:argument-resolvers>
            <bean class="org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver"/>
        </mvc:argument-resolvers>
    </mvc:annotation-driven>
    <bean
        class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/templates/" />
        <property name="suffix" value=".jsp" />
    </bean>
</beans>

Web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">
  <display-name>SpringMVCNew</display-name>
  <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
            /WEB-INF/spring-servlet.xml
            /WEB-INF/spring/*.xml
        </param-value>
    </context-param>
    <filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>springSecurityFilterChain</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <servlet>  
        <servlet-name>spring</servlet-name>  
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>  
        <load-on-startup>1</load-on-startup>    
    </servlet>  
    <servlet-mapping>  
        <servlet-name>spring</servlet-name>  
        <url-pattern>/</url-pattern>  
    </servlet-mapping>  
</web-app>

Facing 2 issues

  1. Dont want to see the below login page. Instead the application should redirect to the ADFS login page.

login-page

  1. As per the suggestion provided and the page redirect is failing and getting below screen

redirect-failure

Any help will be appreciated.