'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 |