'Spring Boot Azure Multiple HttpSecurity

Is it possible to mix two authentication modes?

  • Internal user: Azure ad
  • External user: form authentication

So far I have this:

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration {

    @Configuration
    @Order(1)
    public static class MfaAuthentication extends AadWebSecurityConfigurerAdapter {

        private final UserService userService;

        @Autowired
        public MfaAuthentication(UserService userService) {
            this.userService = userService;
        }

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            super.configure(http);
            http
                .antMatcher("/internal/**")
                .authorizeHttpRequests()
                    .anyRequest().authenticated()
                    .and()
                .oauth2Login()
                    .userInfoEndpoint(userInfoEndpointConfig -> {
                        userInfoEndpointConfig.oidcUserService(this.oidcUserService());
                    });
        }

        private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
            final OidcUserService delegate = new OidcUserService();
            return (userRequest) -> {
                // Delegate to the default implementation for loading a user
                OidcUser oidcUser = delegate.loadUser(userRequest);

                OAuth2AccessToken accessToken = userRequest.getAccessToken();
                Set<GrantedAuthority> mappedAuthorities = new HashSet<>();

                // TODO
                // 1) Fetch the authority information from the protected resource using accessToken
                // 2) Map the authority information to one or more GrantedAuthority's and add it to mappedAuthorities

                // 3) Create a copy of oidcUser but use the mappedAuthorities instead

                List<String> dummy = userService.fetchUserRoles("dummy");
                dummy.forEach(user -> mappedAuthorities.add((GrantedAuthority) () -> user));
                oidcUser = new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo());

                return oidcUser;
            };
        }
    }

    @Configuration
    public static class ExternalAuthentication extends WebSecurityConfigurerAdapter {

        private final ThdAuthenticationProvider thdAuthenticationProvider;

        @Autowired
        public ExternalAuthentication(ThdAuthenticationProvider thdAuthenticationProvider) {
            this.thdAuthenticationProvider = thdAuthenticationProvider;
        }

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                .antMatcher("/external/**")
                .authorizeRequests()
                    .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
                    .anyRequest().fullyAuthenticated()
                    .and()
                .formLogin()
                    .loginPage("/external/login").permitAll()
                    .defaultSuccessUrl("/external/index", true)
                    .failureUrl("/external/denied")
                    .and()
                .logout()
                    .invalidateHttpSession(true)
                    .and()
                    .authenticationProvider(thdAuthenticationProvider);
        }
    }
}

We have mixed accounts (external users/internal users) so we need to check which kind of account wants to have access in the first place.

My idea is to provide a dedicated login form for internal/external user where the routing is done like /internal/** goes to our Azure login and /external/** goes to a custom authentication provider.

When I travel to http://localhost:8080/internal it gets redirected to http://localhost:8080/oauth2/authorization/azure saying there is no mapping. I want to be redirected to our Azure login.

Is this makeable?

EDIT

application.properties

# Enable related features.
spring.cloud.azure.active-directory.enabled=true
# Specifies your Active Directory ID:
spring.cloud.azure.active-directory.profile.tenant-id=some-id
# Specifies your App Registration's Application ID:
spring.cloud.azure.active-directory.credential.client-id=some-client-id
# Specifies your App Registration's secret key:
spring.cloud.azure.active-directory.credential.client-secret=some-secret

Error Message:

Whitelabel Error Page

This application has no explicit mapping for /error, so you are seeing this as a fallback.
Fri May 06 12:41:41 CEST 2022
There was an unexpected error (type=Not Found, status=404).

EDIT 2

Thanks to the comments i figured out the right configuration - at least for the routing. I have this configuration at the moment:

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration {

    @Configuration

    public static class MfaAuthentication extends AadWebSecurityConfigurerAdapter {

        private final UserService userService;

        @Autowired
        public MfaAuthentication(UserService userService) {
            this.userService = userService;
        }

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            super.configure(http);
            http
                    .authorizeRequests()
                    .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
                    .antMatchers("/index").permitAll()
                    .antMatchers("/public/**").permitAll()
                    .antMatchers("/internal/**").hasAnyAuthority("Administrator")
                    .anyRequest()
                    .authenticated()
                    .and()
                    .oauth2Login()
                    .userInfoEndpoint(userInfoEndpointConfig -> {
                        userInfoEndpointConfig.oidcUserService(this.oidcUserService());
                    })
                    .defaultSuccessUrl("/internal/index", true);

        }

        private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
            final OidcUserService delegate = new OidcUserService();
            return (userRequest) -> {
                // Delegate to the default implementation for loading a user
                OidcUser oidcUser = delegate.loadUser(userRequest);

                OAuth2AccessToken accessToken = userRequest.getAccessToken();
                Set<GrantedAuthority> mappedAuthorities = new HashSet<>();

                // TODO
                // 1) Fetch the authority information from the protected resource using accessToken
                // 2) Map the authority information to one or more GrantedAuthority's and add it to mappedAuthorities

                // 3) Create a copy of oidcUser but use the mappedAuthorities instead

                List<String> dummy = userService.fetchUserRoles("dummy");
                dummy.forEach(user -> mappedAuthorities.add((GrantedAuthority) () -> user));
                oidcUser = new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo());

                return oidcUser;
            };
        }
    }

    @Configuration
    @Order(1)
    public static class ExternalAuthentication extends WebSecurityConfigurerAdapter {

        private final ThdAuthenticationProvider thdAuthenticationProvider;

        @Autowired
        public ExternalAuthentication(ThdAuthenticationProvider thdAuthenticationProvider) {
            this.thdAuthenticationProvider = thdAuthenticationProvider;
        }

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .antMatcher("/external/**")
                    .authorizeRequests()
                    .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
                    .antMatchers("/login").permitAll()

                    .anyRequest()
                    .fullyAuthenticated()
                    .and()
                    .formLogin()
                    .loginPage("/external/login").permitAll()
                    .loginProcessingUrl("/external/login").permitAll()
                    .defaultSuccessUrl("/external/index", true)
                    .failureUrl("/external/denied")
                    .and()
                    .logout()
                    .invalidateHttpSession(true)
                    .and()
                    .authenticationProvider(thdAuthenticationProvider);
        }
    }


}

Problem now:

When i travel to /external/index i get redirected to my custom login page. When i want to login (routed via POST to /login) i get redirected to a page where i can choose from oauth2 login which itself is targeted to http://localhost:8080/oauth2/authorization/azure

Here is an excerpt from my (thymeleaf) form:

 <form action="#" th:action="@{/login}" method="post" class="form-signin"
          accept-charset="utf-8">
</form> 

I know that /login is the fixed route for spring security and form based authentication. So is this intended to work with azure in a mixed environment?

Does this setup collide with each other in any way?

Thank you!



Solution 1:[1]

Thanks to the inputs from the commentators and some heavy googling i ended up with this working version:

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration {

    @Configuration

    public static class MfaAuthentication extends AadWebSecurityConfigurerAdapter {

        private final UserService userService;

        @Autowired
        public MfaAuthentication(UserService userService) {
            this.userService = userService;
        }

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            super.configure(http);
            http
                    .csrf()
                    .and()
                    .authorizeRequests(authorize -> authorize.antMatchers("/").permitAll()
                            .antMatchers("/index").permitAll()
                            .antMatchers("/public/**").permitAll()
                            .antMatchers("/internal/**").hasAnyAuthority("Administrator")
                            .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
                            .anyRequest().authenticated())
                    .oauth2Login()
                    .userInfoEndpoint(userInfoEndpointConfig -> userInfoEndpointConfig.oidcUserService(this.oidcUserService()))
                    .defaultSuccessUrl("/internal/index", true);

        }

        private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
            final OidcUserService delegate = new OidcUserService();
            return (userRequest) -> {
                // Delegate to the default implementation for loading a user
                OidcUser oidcUser = delegate.loadUser(userRequest);

                Set<GrantedAuthority> mappedAuthorities = new HashSet<>();

                List<String> dummy = userService.fetchUserRoles("dummy");
                dummy.forEach(user -> mappedAuthorities.add((GrantedAuthority) () -> user));
                oidcUser = new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo());

                return oidcUser;
            };
        }
    }

    @Configuration
    @Order(1)
    public static class ExternalAuthentication extends WebSecurityConfigurerAdapter {

        private final ThdAuthenticationProvider thdAuthenticationProvider;

        @Autowired
        public ExternalAuthentication(ThdAuthenticationProvider thdAuthenticationProvider) {
            this.thdAuthenticationProvider = thdAuthenticationProvider;
        }

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .antMatcher("/external/**")
                    .authorizeRequests(authorize -> authorize.antMatchers("/").permitAll()
                            .antMatchers("/index").permitAll()
                            .antMatchers("/public/**").permitAll()
                            .antMatchers("/external/**").hasAnyAuthority("External")
                            .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
                            .anyRequest().authenticated())
                    .formLogin()
                    .loginPage("/external/login").permitAll()
                    .loginProcessingUrl("/external/login").permitAll()
                    .defaultSuccessUrl("/external/index", true)
                    .failureUrl("/external/denied")
                    .and()
                    .logout()
                    .invalidateHttpSession(true)
                    .and()
                    .authenticationProvider(thdAuthenticationProvider);
        }
    }
}

Here is my custom external login form - at least an excerpt of it:

<form accept-charset="utf-8" action="#" class="form-signin" method="post"
          th:action="@{/external/login}">
</form>

All /internal/** routings go to our Azure AD login.
Please note that there is a custom oidc user service to load additional roles for the given user.

All /external/** routings go to our custom AuthenticationProvider

I donĀ“t know if we will implement this in production ready code.
Personally i have a bad feeling about this mix up of various authentication scenarios.

I think it is better to seperate both (when having external/internal user) into individual apps with individual SecurityConfiguration

Any help/comments/tips on mixing external/internal users is very welcome!

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Thomas Lang