Spring Boot application generate new SecurityContextHolder in every request - spring

We have a tomcat application which works fine in IE7/8 and Firefox. The only browser we are having issues with (that we care about at this point) is google Chrome(above 47 version).
Users can navigate to the application fine and log in and do whatever they need to do in Firefox and IE. However, when trying to log in with Chrome, the session is apparently lost immediately after log in and when the authenticated user tries to navigate to another page they are bumped back to the log in page. This happens consistently.
I have looked at the JSESSIONID that is set in the cookie, which is sent back to mozila and IE on every request, while it does not for Chrome(above 47 version).
It's clear Spring SecurityContextHolder in which we set after the login. and take new SecurityContextHolder in every request.
here I am atteched my code where I am spring security is configure.
Any ideas are welcome!
We are using tomcat 8.0.33 and spring boot 4.2.4.RELEASE
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
public void configure(WebSecurity web) throws Exception {
web
.ignoring()
.antMatchers("/resources/**");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/**","/restServiceFromKwh360/**","/cloudSSO/ssoLogin","/cloudSSO/ssoLogout","/cloudSSO/ssoCallback",
"/cloudSSO/ssoLoginError","/cloudSSO/ssoReturnFromKwh360Services").permitAll()
.antMatchers("/energyAudit/**").access("hasRole('ROLE_CUSTOMER_ADMIN')")
.and().formLogin().loginPage("/cloudSSO/ssoLogin").permitAll()
.and().exceptionHandling().accessDeniedPage("/cloudSSO/ssoLoginError")
.and()
.csrf().disable();
}
}
and here I am set Authentication object manually
#Override
public void AutoLoginUser(String username, HttpServletRequest request) {
try{
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if(authentication.getPrincipal() == "anonymousUser"){
User user;
try {
user = userDao.getUserByEmail(username);
} catch (KWH360DAOException e) {
e.printStackTrace();
logger.error("Request user is not registred with the system >>" + username);
throw new UsernameNotFoundException("You are not registered");
}
if (user == null){
logger.error("Request user is not registred with the system >>" + username);
throw new UsernameNotFoundException("You are not registered");
}
List<GrantedAuthority> authorities = buildUserAuthority(user.getRole().getName());
authentication = new UsernamePasswordAuthenticationToken(user,user.getPassword().toString(), authorities);
SecurityContextHolder.getContext().setAuthentication(authentication);
request.getSession().setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY,
SecurityContextHolder.getContext());
}
}catch(Exception e){
e.printStackTrace();
}
}

Related

JWT Authentication causes conflicts with regular authentication. I can't log in from the login page

I'm a newbie when talking about Spring Security, specially with JWT and CORS, so I apologise in advance if I don't speak clearly about the matter.
We were asked to make an application which simulates a private clinic website, on which patients can make an appointment with a doctor and buy products from the pharmacy. Doctors can introduce information in the database about their patients. Our project has a Restful API as well, which can be accessed through a mobile app (or Postman). What the API does is showing a list of products we have stored in the database.
All users can log in through a log in form, which uses Spring Security. On the other hand, if we wanted to retrieve the information of our API, CORS and JWT are used in addition to Spring Security.
The problem comes when I set up a custom authorization filter our teacher gave us to do this (I have commented the line that does this). We can access our API using Postman perfectly: we log in with the admin user and pass the authorization token to our API route, and in return we get the list of products. But when the filter is working, we can no longer use the log in form of our website to authenticate. The whole proccess goes like this:
The application starts at the main page (localhost:8080/inicio).
In the main page there is a 'Login' button which appears when the user is not authenticated . Clicking it takes us to the log in form.
Once in the log in form (localhost:8080/auth/login) we fill all the fields neccesary for us to log in as an user from the database (in this case, username: admin, password: admin).
We submit the form, which takes us to the petition in charge of the authentication proccess (localhost:8080/login/login-post).
At the end of the proccess, we are redirected back to the main page. The "Login" button should appear as "Logout" when the user is authenticated. But it doesn't. We cannot navigate to other pages the authenticated user should have access to neither.
No error messages are provided by the console, and all it does is taking me back to the main page without having the user authenticated.
This is my Spring Security configuration class:
#Autowired
#Qualifier("userService")
private UserService userService;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
// .addFilterAfter(new JWTAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/", "/css/**", "/img/**", "/js/**", "/vendor/**", "/inicio/**", "/pacientes/altaPaciente/**", "/pacientes/addPaciente/**", "/auth/**").permitAll()
.antMatchers(HttpMethod.GET, "/authRest/**").permitAll()
.antMatchers(HttpMethod.POST, "/authRest/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/auth/login")
.defaultSuccessUrl("/inicio/", true)
.loginProcessingUrl("/auth/login-post")
.permitAll()
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/auth/login?logout")
.permitAll();
}
And my JWT Authorization filter:
private final String HEADER = "Authorization";
private final String PREFIX = "Bearer ";
private final String SECRET = "mySecretKey";
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
try {
if (checkJWTToken(request, response)) {
Claims claims = validateToken(request);
if (claims.get("authorities") != null) {
setUpSpringAuthentication(claims);
} else {
SecurityContextHolder.clearContext();
}
} else {
SecurityContextHolder.clearContext();
}
chain.doFilter(request, response);
} catch(ExpiredJwtException | UnsupportedJwtException | MalformedJwtException e) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
((HttpServletResponse) response).sendError(HttpServletResponse.SC_FORBIDDEN, e.getMessage());
return;
}
}
private Claims validateToken(HttpServletRequest request) {
String jwtToken = request.getHeader(HEADER).replace(PREFIX, "");
return Jwts.parser().setSigningKey(SECRET.getBytes()).parseClaimsJws(jwtToken).getBody();
}
private void setUpSpringAuthentication(Claims claims) {
#SuppressWarnings("unchecked")
List<String> authorities = (List<String>) claims.get("authorities");
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
claims.getSubject(),
null,
authorities.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList())
);
SecurityContextHolder.getContext().setAuthentication(auth);
}
private boolean checkJWTToken(HttpServletRequest request, HttpServletResponse res) {
String authenticationHeader = request.getHeader(HEADER);
if (authenticationHeader == null || !authenticationHeader.startsWith(PREFIX)) {
return false;
}
return true;
}
EDIT: As requested, here are the logs I get when I try to log in as an existing user in the database using the web form: https://pastebin.com/7SYX2MZF
the fault is probably (after discussion)
somewhere here:
if (checkJWTToken(request, response)) {
Claims claims = validateToken(request);
if (claims.get("authorities") != null) {
setUpSpringAuthentication(claims);
} else {
SecurityContextHolder.clearContext();
}
} else {
SecurityContextHolder.clearContext();
}
a check is done in checkJWTToken for the presence of a Authorization header and if there is none, the current SecurityContext is cleared, meaning it will remove whatever principal present.
This removes whomever is previously logged in, which in turn the principal that is constructed when logging in initially.
So you login, the securitycontext is populated by the principal, then it's suddenly removed in the next filter.

Authentication object is null after bypassing the particular request

The below Code I used in webConfigSecurity class to bypass some requests from the client
#Override
public void configure(WebSecurity webSecurity) throws Exception
{
webSecurity.ignoring().antMatchers("/adminSettings/get/**")
.antMatchers("/cases/sayHello/**").antMatchers("/cases/**/downloadPdfFolderPBC/**");
}
In the controller api method requires the user details for further execution, while getting the user details the authentication object is null, so it throws an exception that "user is not authenticated"
public static User get() {
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null) {
UserPrincipal principal = (UserPrincipal) authentication.getPrincipal();
if (principal == null) {
throw new InsufficientAuthenticationException("User not authenticated");
}
return principal.getUser();
}
throw new AuthenticationCredentialsNotFoundException("User not authenticated");
}
I'm new to spring security, In this case, what I should do to get logged user details
Instead of ignoring(), which causes Spring Security to skip those requests, I believe you want to use permitAll() instead like so:
#Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests((requests) -> requests
.antMatcher("/adminSettings/get/**").permitAll()
.antMatcher("/cases/sayHello/**").permitAll()
// ... etc
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults());
}
In this way, Spring Security will still populate the SecurityContextHolder with the logged-in user, but none of the permitAll endpoints will require authentication.

Spring Security - when get login page, security try to authenticate and return 401 error

I am developing Spring boot application with microservices architecture. I am using JWT authentication.
1-http://localhost:8762/auth {"username":"admin", "password":"12345"} (POST request)
2-http://localhost:8762/auth/loginPage (GET request for page)
When i try first request, authentication is working well and i get login info and jwt token.
But when i try second request for getting login page, spring is trying to authenticate and returns 401 error.
How can i ignore authentication for login page.
I have zull project as gateway and authentication project as auth.
if(header == null || !header.startsWith(jwtConfig.getPrefix())) {
chain.doFilter(request, response); // If not valid, go to the next filter.
return;
}
I think at this point, i have to override filter. But i don't know how i write filter.
Here is my code for authentication.
auth project -> WebSecurityConfigurerAdapter
#EnableWebSecurity
public class SecurityCredentialsConfig extends WebSecurityConfigurerAdapter {
#Autowired
private JwtConfig jwtConfig;
#Autowired
private UserDetailsService userDetailsService;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
// make sure we use stateless session; session won't be used to store user's state.
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// handle an authorized attempts
.exceptionHandling().authenticationEntryPoint((req, rsp, e) -> rsp.sendError(HttpServletResponse.SC_UNAUTHORIZED))
.and()
// Add a filter to validate user credentials and add token in the response header
// What's the authenticationManager()?
// An object provided by WebSecurityConfigurerAdapter, used to authenticate the user passing user's credentials
// The filter needs this auth manager to authenticate the user.
.addFilter(new JwtUsernameAndPasswordAuthenticationFilter(authenticationManager(), jwtConfig()))
.authorizeRequests()
// allow all POST requests
.antMatchers("/auth/**").permitAll()
.antMatchers("/user/register").permitAll()
// any other requests must be authenticated
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/auth/loginPage");
}
// Spring has UserDetailsService interface, which can be overriden to provide our implementation for fetching user from database (or any other source).
// The UserDetailsService object is used by the auth manager to load the user from database.
// In addition, we need to define the password encoder also. So, auth manager can compare and verify passwords.
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
#Bean
public JwtConfig jwtConfig() {
return new JwtConfig();
}
}
auth -> UsernamePasswordAuthenticationFilter
public class JwtUsernameAndPasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authManager;
private final JwtConfig jwtConfig;
public JwtUsernameAndPasswordAuthenticationFilter(AuthenticationManager authManager, JwtConfig jwtConfig) {
this.authManager = authManager;
this.jwtConfig = jwtConfig;
// By default, UsernamePasswordAuthenticationFilter listens to "/login" path.
// In our case, we use "/auth". So, we need to override the defaults.
//this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher(jwtConfig.getUri(), "POST"));
this.setRequiresAuthenticationRequestMatcher(new OrRequestMatcher(
new AntPathRequestMatcher("/auth/**")
, new AntPathRequestMatcher("/user/register")
));
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
try {
// 1. Get credentials from request
UserDTO creds = new ObjectMapper().readValue(request.getInputStream(), UserDTO.class);
// 2. Create auth object (contains credentials) which will be used by auth manager
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
creds.getUsername(), creds.getPassword(), Collections.emptyList());
// 3. Authentication manager authenticate the user, and use UserDetialsServiceImpl::loadUserByUsername() method to load the user.
return authManager.authenticate(authToken);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// Upon successful authentication, generate a token.
// The 'auth' passed to successfulAuthentication() is the current authenticated user.
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication auth) throws IOException, ServletException {
Long now = System.currentTimeMillis();
String token = Jwts.builder()
.setSubject(auth.getName())
// Convert to list of strings.
// This is important because it affects the way we get them back in the Gateway.
.claim("authorities", auth.getAuthorities().stream()
.map(GrantedAuthority::getAuthority).collect(Collectors.toList()))
.setIssuedAt(new Date(now))
.setExpiration(new Date(now + jwtConfig.getExpiration() * 1000)) // in milliseconds
.signWith(SignatureAlgorithm.HS512, jwtConfig.getSecret().getBytes())
.compact();
// Add token to header
response.addHeader(jwtConfig.getHeader(), jwtConfig.getPrefix() + token);
}
}
Controller
#GetMapping("/auth/loginPage")
public String loginPage() {
return "login";
}
I think your problem is here in JwtUsernameAndPasswordAuthenticationFilter
You also have this point commented out. You are triggering this filter on POST and GET. You only want to trigger it for POST.
Current method
this.setRequiresAuthenticationRequestMatcher(new OrRequestMatcher(
new AntPathRequestMatcher("/auth/**")
, new AntPathRequestMatcher("/user/register")
));
Updated
this.setRequiresAuthenticationRequestMatcher(new OrRequestMatcher(
new AntPathRequestMatcher("/auth/**", "POST")
, new AntPathRequestMatcher("/user/register", "POST")
));
By doing this:
this.setRequiresAuthenticationRequestMatcher(new OrRequestMatcher(
new AntPathRequestMatcher("/auth/**")
, new AntPathRequestMatcher("/user/register")
));
the filter will authenticate any request to /auth/** (thus /auth/loginPage) and because you set your authentication entry point to just return 401 status you will have that issue.
just comment this:
.and()
// handle an authorized attempts
.exceptionHandling().authenticationEntryPoint((req, rsp, e) -> rsp.sendError(HttpServletResponse.SC_UNAUTHORIZED))
and it should redirect you to the login page.
PS: Based on your configuration if I'm not authenticated and trying to access /auth/loginPage I'll be redirected to /auth/LoginPage, and once I enter the creds I'll be authenticated successfully and redirected again to the same page /auth/loginPage
How can i ignore authentication for login page.
OncePerRequestFilter has a method shouldNotFilter that you can override.
For example:
#Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
return new AntPathMatcher().match("/auth/loginPage", request.getServletPath());
}

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

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();
}

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