Disable multiple login with same user in spring boot and OAuth2 application - spring-boot

I have microservice architecture application working with zuul api-gateway added with Oauth2 security feature. Now, I can able to login with same user in multiple session(I mean multiple browser and multiple machine). So I want to restrict multiple login of same user.
I used below code to restrict same user login. This code works perfectly when I'm doing oauth logout. But I'm facing problem when user logged in and close their browser or clear their browser cookies.
static SessionRegistry sessionRegistry;
#Override
protected void configure(HttpSecurity http) throws Exception
{
http.csrf().disable().authorizeRequests().antMatchers("/login", "/logout").permitAll().anyRequest()
.authenticated().and().formLogin().loginPage("/login")
.failureHandler(loginAuthenticationFailureHandler).permitAll().and().logout().and().authorizeRequests()
.anyRequest().authenticated()
.and()
.sessionManagement()
.maximumSessions(1)
.maxSessionsPreventsLogin(true)
.sessionRegistry(sessionRegistry);
}
#Bean
public ServletListenerRegistrationBean<HttpSessionEventPublisher> httpSessionEventPublisher() {
return new ServletListenerRegistrationBean<HttpSessionEventPublisher>(new HttpSessionEventPublisher());
}
Can any one help me how to implement this single user session logout when browser close and cookie clear or is there any separate procedure to develop this functionality.

I tried to make an implementation using this same approach to use the sessionManagement configuration, but it didn't work for me, in my case I was just needing to remove the multiple login, make either the new login go off or the previous login, do this with an extension of InMemoryTokenStore.
#Component
public class ResetTokenStore extends InMemoryTokenStore {
#Override
public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) {
OAuth2AccessToken accessToken = super.getAccessToken(authentication);
if(accessToken != null) {
removeAccessToken(accessToken);
removeRefreshToken(accessToken.getRefreshToken());
}
return null;
}
}
Basically what I do is force the token renewal, every time a new token is generated and logged in, and the previously generated accesstoken and refreshtoken are deleted.
In the class that extends AuthorizationServerConfigurerAdapter:
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.tokenStore(tokenStore())
.accessTokenConverter(accessTokenConverter())
.reuseRefreshTokens(false)
.userDetailsService(userDetailsService)
.authenticationManager(authenticationManager);
}
#Bean
public TokenStore tokenStore() {
return new ResetTokenStore();
}

Related

Spring Boot REST API disable Form Login redirect

I have been banging my head against the wall for hours trying to figure out something that I would expect to work out of the box these days.
I am building an API with Spring Boot backend and I will create a react front end.
I only have one server so I dont need to use tokens. I want the same normal server side sessions and cookies.
I managed to get the Authentication to work but for some reason it keeps redirecting after success to the default / endpoint.
I really do not want this to happen and can't figure out why this is the case. I also can't find any decent resources on the internet of people that have encountered this issue.
I have seen a few videos where I have seen people handling the login in a Rest Controller end point rather than using filters. I assume this could work but then how would I implement session management?
Here is the code so far:
#Configuration
#EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {
#Autowired
private AuthUserService authUserService;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(authUserService);
}
#Bean
public CorsConfigurationSource corsConfigurationSource(){
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedOrigins(Arrays.asList("http://localhost:3000"));
corsConfiguration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfiguration);
return source;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.mvcMatchers("/api/**").hasRole("AUTH_USER")
.mvcMatchers("/**").permitAll();
http.cors();
http.addFilterAfter(new CsrfHandlerFilter(), CsrfFilter.class);
AuthenticationFilter filter = new AuthenticationFilter();
filter.setAuthenticationManager(authenticationManager());
http.addFilterAt(filter, UsernamePasswordAuthenticationFilter.class);
}
#Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
Authentication Filter:
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
public AuthenticationFilter(){
super.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/login", "POST"));
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
System.out.println("Custom Authentication Filter fired!");
ObjectMapper mapper = new ObjectMapper();
Login login = new Login();
try {
login = mapper.readValue(request.getInputStream(), Login.class);
} catch (StreamReadException e) {
e.printStackTrace();
} catch (DatabindException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
login.getUsername(),
login.getPassword()
);
return this.getAuthenticationManager().authenticate(token);
}
}
Login Model class:
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
public class Login {
private String username;
private String password;
}
I want a normal server side session. I am not using JWT just because it is a JavaScript client. But all I want is for it to not redirect. Is this possible?
Any advice would be appreciated
There are a few ways to approach this, depending on your preference.
Certainly, you can stand up your own Spring MVC endpoint and set the SecurityContext yourself. Spring Security's SecurityContextPersistenceFilter will store the SecurityContext in an HttpSessionSecurityContextRepository by default, which induces the container to write a JSESSIONID session cookie that can be used on subsequent requests.
The main reason to go this route is if you want to have access to the MVC feature set when writing this endpoint.
One downside of this route is that Spring Security 6 will no longer save the security context for you when it comes to custom MVC endpoints, so you would need to be aware of that when upgrading.
HTTP Basic
That said, it doesn't seem like your requirements are so sophisticated that you can't use Spring Security's OOTB behavior.
One way to do this is with HTTP Basic. Note that for simplicity, I'll publish the SecurityFilterChain as a #Bean instead of using the now-deprecated WebSecurityConfigurerAdapter:
#Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.mvcMatchers("/api/**").hasRole("AUTH_USER")
.mvcMatchers("/**").permitAll()
)
.httpBasic(Customizer.withDefaults())
.cors(Customizer.witHDefaults())
.addFilterAfter(new CsrfHandlerFilter(), CsrfFilter.class);
return http.build();
}
This will allow you to send the username/password using the Authorization: Basic header. There's no need in this case for you to stand up anything custom. The filter chain will store the security
context in the session, and your Javascript can call endpoints using the JSESSIONID or by resending the username/password creds.
AuthenticationSuccessHandler
If for some reason you want to use form login (what your sample is customizing right now), instead of creating a custom filter, you can configure the existing form login filter with an AuthenticationSuccessHandler that does not redirect:
#Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.mvcMatchers("/api/**").hasRole("AUTH_USER")
.mvcMatchers("/**").permitAll()
)
.formLogin((form) -> form
.successHandler((request, response, authentication) ->
response.setStatusCode(200)
)
)
.cors(Customizer.witHDefaults())
.addFilterAfter(new CsrfHandlerFilter(), CsrfFilter.class);
return http.build();
}
Once again, the filter chain will save the subsequent UsernamePasswordAuthenticationToken to the session and issue a JSESSIONID for subsequent requests.

What happen if there are too many session in the spring-mvc + tomcat application

I am having a project using spring-mvc + tomcat. It has a keycloak adapator and a CMS systems which use the traditional session to manage the user login,
i.e. the spring-mvc checks if the session has permission, and if not it will redirect to keycloak login page.
I need to write some new routes (RESTful, using #RestController) for their new mobile app. These api will accept the access_token and return data if they have permission/ the token is valid.
Because this backend needs to also support the old CMS system, so I can't set the spring to stateless or disable the session usage.
As I am not in controll of who is using these new RESTful API, some api users are just calling these api without passing the session cookies, so that the backend makes a new session for them every time they called (these api will be called very frequently to update the data, say 30 every mins)
So, will the server having memory usage problem if there are too many sessions? I know that the default of session timeout should be 30mins, is this timeout enough?. I have done a lot of searching but seems no one talks about this
Each session will consume some memory, to the total needed memory for sessions is number of sessions (in parallel) x size per session. - I know this is not helpful, so the help full part comes next.
If you have many (huge) sessions, Tomcat can persist them on disk, instead of hold them in memory. You just need to configure an other Manger Implementation for sessions: switch to org.apache.catalina.session.PersistentManager and you need to configure the idle parameters: https://tomcat.apache.org/tomcat-9.0-doc/config/manager.html
Important: all the stuff that is stored in your session must be Serializable!.
I finally figured out a way to actually enables sessions in routes for web cms while disabling sessions on RESTful routes, i will post it here
you can define a MultiHttpSecurityConfig
#Configuration
public class MultiHttpSecurityConfig{
#KeycloakConfiguration
#Order(1)
public class SecurityConfig1 extends KeycloakWebSecurityConfigurerAdapter
{
/**
* Registers the KeycloakAuthenticationProvider with the authentication manager.
*/
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(keycloakAuthenticationProvider());
}
/**
* Defines the session authentication strategy.
*/
#Bean
#Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new NullAuthenticatedSessionStrategy();
}
#Override
protected void configure(HttpSecurity http) throws Exception
{
super.configure(http);
http.antMatcher("/api/v1/external/**") // these routes disabled session
.authorizeRequests()
.anyRequest()
.permitAll()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
#KeycloakConfiguration
#Order(2)
public class SecurityConfig2 extends KeycloakWebSecurityConfigurerAdapter
{
/**
* Registers the KeycloakAuthenticationProvider with the authentication manager.
*/
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(keycloakAuthenticationProvider());
}
/**
* Defines the session authentication strategy.
*/
#Bean
#Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(buildSessionRegistry());
}
#Bean
protected SessionRegistry buildSessionRegistry() {
return new SessionRegistryImpl();
}
#Override
protected void configure(HttpSecurity http) throws Exception
{
super.configure(http);
http
.antMatcher("/")
.authorizeRequests()
.anyRequest().permitAll();
}
}
}

Spring security requires login for a URL even though it is permitted without authentication in Configure method

I am using Spring Boot and Spring Security. I have multiple URLs, I want some of them to be accessed with authentication and some of them to be allowed access without authentication. I have written Configure method in such a manner. But browser is redirecting me to a login page even for URL that has been allowed access without authentication. For example, I am asked for login when I try to access localhost:8080/emailExists. What could me wrong?
My Controller class :
#RestController
class AppController {
#Autowired
private ShopSearchRepository searchRepository;
#Autowired
private UserRepository userRepository;
#GetMapping("/emailExists")
public Boolean emailExists(#RequestParam String email) {
User user = userRepository.findByEmail(email);
if (user == null) return false;
else return true;
}
#GetMapping("/ShopsSearch")
List<Shop> search(#RequestParam String name) {
return searchRepository.findByName(name);
}
}
My Configure method :
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests()
.antMatchers("/ShopsSearch/").authenticated()
.anyRequest().permitAll()
.and()
.httpBasic();
}
You need to tell which URL to bypass
#Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests()
.antMatchers(HttpMethod.POST,"/emailExists").permitAll() // allow without authentication
.antMatchers(HttpMethod.POST, "/login").permitAll() // allow without authentication
.antMatchers(HttpMethod.POST,"/newuser/*").permitAll() // allow without authentication
.antMatchers(HttpMethod.GET,"/master/*").permitAll() // allow without authentication
.antMatchers(HttpMethod.GET,"/exploreCourse").permitAll() // allow without authentication
.anyRequest().authenticated() //all other need auth
}

How to scale horizontally a spring-boot oauth2 server with JDBC implementation

I have a spring boot oauth2 server that uses a JDBC implementation. It is configured as an authorization server with #EnableAuthorizationServer.
I'd like to scale that application horyzontally but it doesn't seem to work properly.
I can connect only if I have one instance (pods) of the server.
I use autorisation_code_client grant from another client service to get the token.
So first the client service redirect the user to the oauth2 server form, then once the user is authenticated he is supposed to be redirect to the client-service with a code attached to the url, finally the client use that code to request the oauth2 server again and obtain the token.
Here the user is not redirected at all if I have several instance of the oauth2-server. With one instance it works well.
When I check the log of the two instances in real time, I can see that the authentication works on one of them. I don't have any specific error the user is just not redirected.
Is there a way to configure the oauth2-server to be stateless or other way to fix that issue ?
Here is my configuration, the AuthorizationServerConfigurerAdapter implementation.
#Configuration
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
#Bean
#ConfigurationProperties(prefix = "spring.datasource")
public DataSource oauthDataSource() {
return DataSourceBuilder.create().build();
}
#Autowired
#Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;
#Bean
public JdbcClientDetailsService clientDetailsSrv() {
return new JdbcClientDetailsService(oauthDataSource());
}
#Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(oauthDataSource());
}
#Bean
public ApprovalStore approvalStore() {
return new JdbcApprovalStore(oauthDataSource());
}
#Bean
public AuthorizationCodeServices authorizationCodeServices() {
return new JdbcAuthorizationCodeServices(oauthDataSource());
}
#Bean
public TokenEnhancer tokenEnhancer() {
return new CustomTokenEnhancer();
}
#Bean
#Primary
public AuthorizationServerTokenServices tokenServices() {
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(tokenStore());
tokenServices.setTokenEnhancer(tokenEnhancer());
return tokenServices;
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetailsSrv());
}
#Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
oauthServer
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()")
.allowFormAuthenticationForClients();
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.authenticationManager(authenticationManager)
.approvalStore(approvalStore())
//.approvalStoreDisabled()
.authorizationCodeServices(authorizationCodeServices())
.tokenStore(tokenStore())
.tokenEnhancer(tokenEnhancer());
}
}
The main class
#SpringBootApplication
#EnableResourceServer
#EnableAuthorizationServer
#EnableConfigurationProperties
#EnableFeignClients("com.oauth2.proxies")
public class AuthorizationServerApplication {
public static void main(String[] args) {
SpringApplication.run(AuthorizationServerApplication.class, args);
}
}
The Web Security Configuration
#Configuration
#Order(1)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Bean
#Override
public UserDetailsService userDetailsServiceBean() throws Exception {
return new JdbcUserDetails();
}
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
protected void configure(HttpSecurity http) throws Exception { // #formatter:off
http.requestMatchers()
.antMatchers("/",
"/login",
"/login.do",
"/registration",
"/registration/confirm/**",
"/registration/resendToken",
"/password/forgot",
"/password/change",
"/password/change/**",
"/oauth/authorize**")
.and()
.authorizeRequests()//autorise les requetes
.antMatchers(
"/",
"/login",
"/login.do",
"/registration",
"/registration/confirm/**",
"/registration/resendToken",
"/password/forgot",
"/password/change",
"/password/change/**")
.permitAll()
.and()
.requiresChannel()
.anyRequest()
.requiresSecure()
.and()
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/login.do")
.usernameParameter("username")
.passwordParameter("password")
.and()
.userDetailsService(userDetailsServiceBean());
} // #formatter:on
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsServiceBean()).passwordEncoder(passwordEncoder());
}
}
Client side the WebSecurityConfigurerAdapter
#EnableOAuth2Sso
#Configuration
public class UiSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**")
.authorizeRequests()
.antMatchers(
"/",
"/index.html",
"/login**",
"/logout**",
//resources
"/assets/**",
"/static/**",
"/*.ico",
"/*.js",
"/*.json").permitAll()
.anyRequest()
.authenticated()
.and()
.csrf().csrfTokenRepository(csrfTokenRepository())
.and()
.addFilterAfter(csrfHeaderFilter(), SessionManagementFilter.class);
}
}
the oauth2 configuration properties
oauth2-server is the service name (load balancer) on kubernetes and also the server path that is why it appears twice.
security:
oauth2:
client:
clientId: **********
clientSecret: *******
accessTokenUri: https://oauth2-server/oauth2-server/oauth/token
userAuthorizationUri: https://oauth2.mydomain.com/oauth2-server/oauth/authorize
resource:
userInfoUri: https://oauth2-server/oauth2-server/me
Here an important detail, the value of userAuthorizationUri is the address to access the oauth2-server from the outside of the k8s cluster. The client-service send back that address into the response with a 302 http code if the user is not connected and tries to access to the /login path of the client-service. then the user is redirected to the /login path of the oauth2-server.
https://oauth2.mydomain.com target an Nginx Ingress controller that handle the redirection to the load balancer service.
Here is a solution to this problem. It's not a Spring issue at all but a bad configuration of the Nginx Ingress controller.
The authentication process is done in several stages :
1 - the user clic on a login button that target the /login path of the client-server
2 - the client-server, if the user is not authenticated yet, send a response to the
browser with a 302 http code to redirect the user to the oauth2-server, the value of
the redirection is composed with the value of the
security.oauth2.client.userAuthorizationUri property
and the redirection url that will be used by the browser to allow the client-server to get the Token once the user is authenticated.
That url look like this :
h*tps://oauth2.mydomain.com/oauth2-server/oauth/authorize?client_id=autorisation_code_client&redirect_uri=h*tps://www.mydomain.com/login&response_type=code&state=bSWtGx
3 - the user is redirected to the previous url
4 - the oauth2-server send a 302 http code to the browser with the login url of the
oauth2-server, h*tps://oauth2.mydomain.com/oauth2-server/login
5 - the user submit his credentials and the token is created if they are correct.
6 - the user is redirected to the same address as at the step two, and the oauth-server
add informations to the redirect_uri value
7 - the user is redirected to the client-server. The redirection part of the response look like this :
location: h*tps://www.mydomain.com/login?code=gnpZ0r&state=bSWtGx
8 - the client-server contact the oauth2-server and obtain the token from the code and the state that authenticates it. It doesn't matter if the instance of the oauth2
server is different than the one used by the user to authenticate himself. Here the
client-server use the value of security.oauth2.client.accessTokenUri to get the
token, this is the internal load balancing service address that targets the oauth2 server
pods, so it doesn't pass through any Ingress controller.
So at the steps 3 to 6 the user must communicate with the same instance of the oauth2-server throught the Ingress controller in front of the load balancer service.
Its is possible by configuring the Nginx Ingress controller with a few annotations :
"annotations": {
...
"nginx.ingress.kubernetes.io/affinity": "cookie",
"nginx.ingress.kubernetes.io/session-cookie-expires": "172800",
"nginx.ingress.kubernetes.io/session-cookie-max-age": "172800",
"nginx.ingress.kubernetes.io/session-cookie-name": "route"
}
That way we ensure that the user will be redirected to the same pods/instance of the oauth2-server during the authentication process as long he's identified with the same cookie.
The affinity session mecanism is a great way to scale the authentication server and also the client-server. Once the user is authenticated he will always use the same instance of the client and keep his session informations.
Thanks to Christian Altamirano Ayala for his help.
By default an in-memory TokenStore is used.
The default InMemoryTokenStore is perfectly fine for a single server
If you want multiple pods, you probably should go for JdbcTokenStore
The JdbcTokenStore is the JDBC version of the same thing, which stores token data in a relational database. Use the JDBC version if you can share a database between servers, either scaled up instances of the same server if there is only one, or the Authorization and Resources Servers if there are multiple components. To use the JdbcTokenStore you need "spring-jdbc" on the classpath.
Source Spring Security: OAuth 2 Developers Guide

How to redirect UsernameNotFoundException from PreAuthenticatedAuthenticationProvider when using multiple AuthenticationProviders?

Using Spring Security 4.02, can anyone help with some tips on how I can handle UsernameNotFoundException from PreAuthenticatedAuthenticationProvider when using multiple AuthenticationProviders so that authenticated requests, with the correct header, but which are unauthorized, are sent to a specific URL instead of the forms-login page?
Let me explain further what I'm trying to accomplish for accessing a web app being secured by SSO behind a proxy. Not all users who are authenticated by SSO will have access to this app. So I need to account for 3 access scenarios:
authenticated user (header is present) is authorized (username/roles are present in app's db)
authenticated user (header is present) is unauthorized (username/roles are not present in app's db)
unauthenticated user with username/roles present in app's db
The actions when accessing the website should be:
authenticated/authorized user proceeds directly to target URL
authenticated/unauthorized user is redirected to error/info page
unauthenticated user is redirected to forms-login page for authentication
With my current configuration, scenarios 1 & 3 appear to be working as desired. For scenario 2 I've tried setting RequestHeaderAuthenticationFilter#setExceptionIfHeaderMissing to both true and false.
If setExceptionIfHeaderMissing=false, authenticated/unauthorized request is handled by ExceptionTranslationFilter where AccessDeniedException is thrown and user is redirected to forms-login page.
If setExceptionIfHeaderMissing=true, authenticated/unauthorized request encounters PreAuthenticatedCredentialsNotFoundException from AbstractPreAuthenticatedProcessingFilter.doAuthenticate and HTTP 500 is returned.
So I've read and reread the Spring Security reference and api documents and scoured the web and just can't quite figure out what I need to do. I think I somehow need to enable some kind of filter or handler to trap the PreAuthenticatedCredentialsNotFoundException with a redirected response. But I can't seem to wrap my head around how to implement that with all the spring tools available. Can someone please offer some specifics? Many thanks in advance!!
Here is my configuration:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled=true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private static final String AUTHENTICATION_HEADER_NAME = "PKE_SUBJECT";
#Autowired
CustomUserDetailsServiceImpl customUserDetailsServiceImpl;
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(preAuthenticatedAuthenticationProvider());
auth.inMemoryAuthentication()
.withUser("user").password("password").roles("USER").and()
.withUser("admin").password("password").roles("USER", "ADMIN");
auth.userDetailsService(customUserDetailsServiceImpl);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().and()
.authorizeRequests()
.antMatchers("/javax.faces.resource/**", "/resources/**", "/templates/**", "/public/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.permitAll()
.and()
.logout()
.logoutSuccessUrl("/public/welcome.xhtml")
.and()
.addFilter(requestHeaderAuthenticationFilter());
}
#Bean PreAuthenticatedAuthenticationProvider preAuthenticatedAuthenticationProvider() throws Exception {
PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider();
provider.setPreAuthenticatedUserDetailsService(userDetailsServiceWrapper());
return provider;
}
#Bean
public RequestHeaderAuthenticationFilter requestHeaderAuthenticationFilter() throws Exception {
RequestHeaderAuthenticationFilter filter = new RequestHeaderAuthenticationFilter();
filter.setPrincipalRequestHeader(AUTHENTICATION_HEADER_NAME);
filter.setAuthenticationManager(authenticationManagerBean());
filter.setExceptionIfHeaderMissing(true);
return filter;
}
#Bean
public UserDetailsByNameServiceWrapper<PreAuthenticatedAuthenticationToken>
userDetailsServiceWrapper() throws Exception {
UserDetailsByNameServiceWrapper<PreAuthenticatedAuthenticationToken> wrapper
= new UserDetailsByNameServiceWrapper<PreAuthenticatedAuthenticationToken>();
wrapper.setUserDetailsService(customUserDetailsServiceImpl);
return wrapper;
}
}
My customized UserDetailsService:
#Service("customUserDetailsService")
public class CustomUserDetailsServiceImpl implements UserDetailsService {
#Autowired
UserRepo userRepo;
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDetailDO userDetail = userRepo.getUserDetailById(username);
if(userDetail == null) {
throw new UsernameNotFoundException("user is not authorized for this application");
}
List<UserRoleDO> roles = userRepo.getRolesByUsername(username);
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
if(CollectionUtils.isNotEmpty(roles)) {
for(UserRoleDO role : roles) {
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(role.getRole());
authorities.add(authority);
}
}
UserDetails user = new User(username, "N/A", authorities);
return user;
}
}
I realized that I did not need to handle the exception. What I did was to shift my thinking on this. I realized that even if the username was not found by the customUserDetailsService, the request was still an authenticated request since the request is trusted to be authenticated by the SSO and the proxy server.
So instead of returning a UsernameNotFoundException I returned the org.springframework.security.core.userdetails.User with an empty Authorities collection. And because the RequestHeaderAuthenticationFilter.setExceptionIfHeaderMissing = false by default, no exception is thrown and then the authenticated request is passed to the access filter where it is determined that the request has no authorization to access any resources. So instead of redirecting to the next authentication filter which would be the forms login provider, a 403 Access Denied http status is returned which I can then override to redirect to a user-friendly error page.

Resources