Select an Oauth2 identity provider programmatically at runtime in Spring Security reactive

58 Views Asked by At

I have a reactive Spring Boot 2 application, which acts as a web agent that initiates user authentication using Spring Security. The end users, who reach this application, come from either public or private domains and the web agent should accordingly redirect them to either identity-provider.com (public) or identity-provider.biz (private) for login. The redirect_uri post authentication remains the same in both cases.

We have configured 2 Oauth2 identity providers and 2 corresponding Oauth2 clients inside the application.yaml.

spring:
  security:
    oauth2:
      client:
        provider:
          idpcom:
            issuer-uri: https://identity-provider.com
          idpbiz:
            issuer-uri: https://identity-provider.biz
        
        registration:
          idpcom:
            provider: idpcom
            client-id: clientid
            client-secret: clientsecret
            authorization-grant-type: authorization_code
            redirect-uri: https://my-domain.com/login/oauth2/code/idpcom
            scope:
              - openid

          idpbiz:
            provider: idpbiz
            client-id: clientid
            client-secret: clientsecret
            authorization-grant-type: authorization_code
            redirect-uri: https://my-domain.com/login/oauth2/code/idpbiz
            scope:
              - openid

With this configuration, when application receives a request, the end user is presented with links to both identity providers. The user is forced to choose one of them and start the authentication flow. what end user sees

We found that following code in Spring security is building the HTML page shown above

ServerHttpSecurity.setDefaultEntryPoints(ServerHttpSecurity http)

Our requirement is to programmatically resolve the identity provider at runtime and directly show the corresponding login page to the end user. The decision to use identity-provider.com or identity-provider.biz is based on end users IP address (captured in X-Forwarded-For header).

We need suggestions to implement this functionality.

1

There are 1 best solutions below

4
ch4mp On

According to the manual, when defining

http.oauth2Login(oauth2 -> {
    oauth2.loginPage("/login/oauth2");
});

You need to provide a @Controller with a @RequestMapping("/login/oauth2") that is capable of rendering the custom login page.

You could write something like that in a servlet:

@Controller
public class LoginController {

    private final Pattern bizIpsPattern;
    
    public LoginController (@Value("biz-ips-pattern") String pattern) {
        this.bizIpsPattern = Pattern.compile(pattern);
    }
    
    @GetMapping("/login/oauth2")
    public RedirectView getAuthorizationCodeInitiationUrl(HttpServletRequest request) {
        // Instead of throwing, you could as well decide to redirect to "com" IDP
        final var header = Optional.ofNullable(request.getHeader("X-Forwarded-For")).orElseThrow(() -> new MissingForwardedForException());
        final var matcher = bizIpsPattern.matcher(header);
        return matcher.matches() ? new RedirectView("/oauth2/authorization/idpbiz") : new RedirectView("/oauth2/authorization/idpcom");
    }
    
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    static final class MissingForwardedForException extends RuntimeException {
        private static final long serialVersionUID = 8215954933514492489L;

        MissingForwardedForException() {
        super("X-Forwarded-For header is missing");
        }
    }
}

In a reactive application, the controller method would use ServerWebExchange instead of HttpServletRequest:

@GetMapping("/login/oauth2")
public Mono<RedirectView> getAuthorizationCodeInitiationUrl(ServerWebExchange exchange) {
    final var header = exchange.getRequest().getHeaders().getOrEmpty("X-Forwarded-For");
    if (header.size() < 1) {
        throw new MissingForwardedForException();
    }
    final var matcher = bizIpsPattern.matcher(header.get(0));
    return Mono.just(matcher.matches() ? new RedirectView("/oauth2/authorization/idpbiz") : new RedirectView("/oauth2/authorization/idpcom"));
}

the SecurityWebFilterChain would be configured slightly differently too:

http.exceptionHandling(exceptionHandling -> {
    exceptionHandling.authenticationEntryPoint(new RedirectServerAuthenticationEntryPoint("/login/oauth2"));
});