Implement logout in Spring for Single Page Application - java

I'm using Java Web Token (JWT) for authentication in my web app. I want to create a /logout Rest endpoint that deletes the JSession cookie on the client side, invalidates the session AND does whatever else is necessary.
The front end is a SPA written in React.
I have the following configure method in the WebSecurityConfig class that extends WebSecurityConfigurerAdapter:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/login").permitAll()
.and()
.authorizeRequests()
.antMatchers("/signup").permitAll()
.and()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.addFilterBefore(
new JWTAuthenticationFilter(userDetailsServiceBean()),
UsernamePasswordAuthenticationFilter.class);
}
I had the following code, but it returns 404 Not found error with the path set to /login. I want to get a HTTP response of 200 and some cleanups instead. what should I do ?
.logout().logoutUrl("/logout").logoutSuccessHandler(logoutHandler).logoutSuccessUrl("/login").invalidateHttpSession(true)

After some research I found out that if I want to use stateless Rest API design, I shouldn't use any cookie, including JSESSIONID.
Therefore I changed my code to:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf().disable()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.addFilterBefore(
new JWTAuthenticationFilter(userDetailsServiceBean()),
UsernamePasswordAuthenticationFilter.class);
}

Could you first try for login request like this. First add JWTLoginFilter in your WebSecurityConfig.
Pasting code from my sample project:
http.csrf().disable() // disable csrf for our requests.
.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers(HttpMethod.POST,"/login").permitAll()
.anyRequest().authenticated()
.and()
// We filter the api/login requests
.addFilterBefore(new JWTLoginFilter("/login", authenticationManager()), UsernamePasswordAuthenticationFilter.class)
// And filter other requests to check the presence of JWT in header
.addFilterBefore(new JWTAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(new CORSFilter(), ChannelProcessingFilter.class);
You wouldn't require CORSFilter if your front end and back end are on same server.
Also find below JWTLoginFilter class
public class JWTLoginFilter extends AbstractAuthenticationProcessingFilter
{
private TokenAuthenticationService tokenAuthenticationService;
public JWTLoginFilter(String url, AuthenticationManager authenticationManager) {
super(new AntPathRequestMatcher(url));
setAuthenticationManager(authenticationManager);
tokenAuthenticationService = new TokenAuthenticationService();
}
#Override
public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse)
throws AuthenticationException, IOException, ServletException {
AccountCredentials credentials = new ObjectMapper().readValue(httpServletRequest.getInputStream(), AccountCredentials.class);
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(credentials.getUsername(), credentials.getPassword());
return getAuthenticationManager().authenticate(token);
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication)
throws IOException, ServletException {
String name = authentication.getName();
tokenAuthenticationService.addAuthentication(response, name);
}
}
AccountCredential class is simple POJO class containing two fields username and password, which I was using to receive the request.
Also please note that UsernamePasswordAuthenticationFilter that we are using require two fields in login request 'username' and 'password'. Like {"username":"user1","password":"secret1"}

Related

Spring security filter chain is not working

I am using Spring Security to validate my GET and POST requests. The auth mechanisms for GET and POST are not the same. The below snippet is from my SecurityConfigs configure method.
FilterA is for GET request and I have defined a customBAuthenticationManager bean which implements AuthenticationManager for it.
FilterB is for POST requests and I have defined customAuthProvider with UserDetails service. These GET and POST requests work fine when added alone. But when both these filters are added one after the other, first request in the filter chain fails but the second request works fine.
For instance, with the below code, my POST request works fine but GET request (1st in the chain) throws 401 error. If I swap the order of GET and POST, then the GET would work fine but POST (1st in the chain) throws 403 error.
But all the cases, I could see that the custom authentication manager/provider work fine.
Can someone help me understand what's going wrong here?
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
FilterA filtera = new FilterA();
filtera.setCheckForPrincipalChanges(true);
filtera.setAuthenticationManager(customBAuthenticationManager());
FilterB filterb = new FilterB();
filterb.setCheckForPrincipalChanges(true);
filterb.setAuthenticationManager(authenticationManager());
httpSecurity
.headers()
.frameOptions()
.disable()
.and()
.mvcMatcher("/**")
.csrf()
.disable()
.requestCache()
.requestCache(getHttpSessionRequestCache())
.and()
.sessionManagement()
.maximumSessions(1)
.and()
.and()
.addFilter(filtera)
.authorizeRequests()
.mvcMatchers("/getrequest/**").authenticated()
.and()
.addFilter(filterb)
.authenticationProvider(customAauthProvider())
.authorizeRequests()
.mvcMatchers("/postrequest/**").authenticated()
.and()
.authorizeRequests()
.mvcMatchers("/different-open-request/**").permitAll()
.and()
.httpBasic();
Tried changing the order of the filters in filter chain. Tried removing one of the request from the filter chain, and that works fine.
i suggest you to you use AuthenticationEntryPoint
and then add filters before or after entryPoint.
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.headers().frameOptions().disable()
.and().requestCache().requestCache(getHttpSessionRequestCache())
.and().exceptionHandling().authenticationEntryPoint(authenticationEntrypoint)
.and().authorizeRequests().anyRequest().authenticated().and()
.addFilterAfter(new myAFilter(), BasicAuthenticationFilter.class).addFilter(new Filter() {
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
filterChain.doFilter(servletRequest,servletResponse);
}
});
}

Spring Web Security entry point triggers

I am getting started with Spring Web Security for my application and I am trying to implement stateless JWT authentication. Curretly, the configure method in the Web Security config is the following
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.csrf().disable()
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/login", "/register", "/authenticate/{uuid}").permitAll()
.anyRequest().authenticated()
;
httpSecurity.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}
My jwtAuthenticationEntryPoint is the following:
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
log.warn("Responding with unauthorized error. Message - {}", authException.getMessage());
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Sorry, You're not authorized to access this resource.");
}
}
The authentication works correctly besides for the "/authenticate/{uuid}" endpoint. The request is allowed (Status 200 and correct return of the function) but I keep getting the warning from the jwtAuthenticationEntryPoint class ("Responding with unauthorized error") in the console.
Why is the EntryPoint getting triggered for that specific request and how can I resolve it?
EDIT:
AuthenticationController:
#RestController
#CrossOrigin
public class AuthenticationController {
#RequestMapping(value = "/authenticate/{uuid}", method = RequestMethod.GET)
public ResponseEntity<?> authenticate(#PathVariable String uuid){
return ResponseEntity.ok(uuid);
}
}
Pls use web.ignoring() to try as the below:
public void configure(WebSecurity web) throws Exception {
String [] notauthlist = new String[]{"/login", "/register","/authenticate/**"};
web.ignoring().antMatchers(notauthlist);
}
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.csrf().disable()
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests().anyRequest().authenticated()
;
httpSecurity.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}

Custom failureHandler throws 401 in Spring security login

I'm working on a custom login failureHandler in my app, and I noticed that if I type anyRequest().authenticated() in authorizeRequest() everything works, but I have no CSS on my login page, but if I type anyRequest().permitAll(), I have CSS on my site, and logging with wrong credentials throws 401 - unauthorized error. It only happens in my custom failureHandler. Can you tell me why it happens?
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/admin/**")
.hasRole("ADMIN")
.antMatchers("/user/**")
.hasAnyRole("USER", "ADMIN")
.antMatchers("/guest*")
.permitAll()
.anyRequest()
.permitAll()
// .anyRequest()
// .authenticated()
.and()
.formLogin()
.loginPage("/guest/login")
.permitAll()
.defaultSuccessUrl("/user/all-tasks", true)
.failureUrl("/guest/login")
.failureHandler(new MyAuthenticationFailureHandler())
.and()
.logout()
.logoutUrl("/user/logout")
.deleteCookies("JSESSIONID")
.and()
.cors()
.and()
.csrf()
.disable();
}
#Component
public class MyAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
#Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
super.onAuthenticationFailure(request, response, exception);
request.getSession().setAttribute("error", true);
}
}
logging with wrong credentials throws 401 - unauthorized error
Because you are doing :
.failureUrl("/guest/login")
.failureHandler(new MyAuthenticationFailureHandler())
and what are done by failureUrl() will be reset by the subsequent failureHandler().So the customised SimpleUrlAuthenticationFailureHandler do not configure with failureUrl yet and hence it will send 401 if the authentication fails since it does know which URL to redirect to.Change it to :
.failureHandler(new MyAuthenticationFailureHandler("/guest/login"))
should redirect to "/guest/login" if authentication fails.
I noticed that if I type anyRequest().authenticated() in
authorizeRequest() everything works, but I have no CSS on my login
page, but if I type anyRequest().permitAll(), I have CSS on my site,
Because in case of anyRequest().authenticated() , the CSS 's URL also required an authenticated user to access. But in login page , the user must not be authenticated. Because if they are authenticated , it does not make sense that they can go to login page.So no CSS will be shown in login page since only unauthenticated users can go to it.
You have to exclude all the related url resources required by login page to work from any protections by configuring WebSecurity. Everyone should access them :
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers("/css/**")
.antMatchers("/anyThingRequiredByLoginPageToWork/**");
}

OAuth2 with Google - CORS Error (Angular + Spring boot) [duplicate]

This question already has an answer here:
CORS issue while making an Ajax request for oauth2 access token
(1 answer)
Closed 3 years ago.
I have problem with CORS error. I do request for Google oAuth2 and i get a CORS ERROR:
I want to get google authentication and generate a JWT token. When I do it without using the client everything is fine. When I send angular requests this is a problem with CORS. I allow all types of CORS. Why am I getting this error?
Access to XMLHttpRequest at 'https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=1020159669873-d9r35ssmnejud852bam87d8gqtcj5qf1.apps.googleusercontent.com&scope=openid%20profile%20email&state=8nizHP1X2z9sA8m0vqM4Lzd6VT24R15eSw5flteTywM%3D&redirect_uri=http://localhost:8080/oauth2/callback/google' (redirected from 'http://localhost:8080/oauth2/authorization/google')
from origin 'null' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Cross-Origin Read Blocking (CORB) blocked cross-origin response https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=1020159669873-d9r35ssmnejud852bam87d8gqtcj5qf1.apps.googleusercontent.com&scope=openid%20profile%20email&state=8nizHP1X2z9sA8m0vqM4Lzd6VT24R15eSw5flteTywM%3D&redirect_uri=http://localhost:8080/oauth2/callback/google with MIME type text/html. See https://www.chromestatus.com/feature/5629709824032768 for more details.
My Angular request:
googleLogin(): Observable<LoginResponse> {
return this.http.get<LoginResponse>
(environment.baseUrl + '/oauth2/authorization/google')
.pipe(tap(response => {
localStorage.setItem('access_token', response.accessToken);
}));
}
//...
public onGoogleLogin(): void {
this.authService.googleLogin().subscribe();
}
//...
CORS CONFIG:
#Override
public void addCorsMappings(CorsRegistry registry) {
registry
.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("HEAD", "OPTIONS", "GET", "POST", "PUT", "PATCH", "DELETE")
.maxAge(MAX_AGE_SECS);
}
Security configuration:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.and()
.csrf()
.disable()
.exceptionHandling()
.authenticationEntryPoint(unauthorizedHandler)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/",
"/favicon.ico",
"/**/*.png",
"/**/*.gif",
"/**/*.svg",
"/**/*.jpg",
"/**/*.html",
"/**/*.css",
"/**/*.js")
.permitAll()
.antMatchers("/api/v1/oauth0/**")
.permitAll()
.antMatchers("/api/v1/oauth2/**")
.permitAll()
.anyRequest()
.authenticated()
.and()
// włączenie obslugi oauth2
.oauth2Login()
.successHandler(this.successHandler)
.redirectionEndpoint()
.baseUri("/oauth2/callback/*")
.and()
.userInfoEndpoint()
.oidcUserService(customOidcUserService);
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
Success Handler:
#Autowired
private UserRepository userRepository;
#Autowired
private JwtTokenProvider tokenProvider;
private final static String URL = "http://localhost:8080/api/v1/oauth2/authenticate";
#Override
public void onAuthenticationSuccess(
HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
if (response.isCommitted()) {
return; }
DefaultOidcUser oidcUser = (DefaultOidcUser) authentication.getPrincipal();
System.out.println(oidcUser);
Map attributes = oidcUser.getAttributes();
String email = attributes.get("email").toString();
User user = userRepository.findByEmail(email).orElseThrow(
() -> new ResourceNotFoundException("User", "email", email)
);
String token = tokenProvider.generateToken(user);
String redirectionUrl = UriComponentsBuilder.fromUriString(URL).queryParam("token", token)
.build().toUriString();
getRedirectStrategy().sendRedirect(request, response, redirectionUrl);
}
}
Controller:
#RestController
#RequestMapping("/api/v1/oauth2")
public class OAuth2Controller {
#GetMapping("/authenticate")
public ResponseEntity<?> authenticateUser(#RequestParam String token) {
return ResponseEntity.ok(new JwtAuthenticationResponse(token));
}
}
You cannot get the token in this example as you need to make actual redirects. There are couple of ways you could circumvent this requirement which is detailed in RFC https://www.rfc-editor.org/rfc/rfc6749#section-1.2
Initiate authorization flow in a popup and pass back the token received by server via postMessage() API provided in the browser, from the popup window back to the webapp.
Save the state, whatever it is, redirect to server which will initiate authorization flow and after token is exchanged for a grant, redirect back to the webapp with a token as a query string parameter. Then use it and restore the state.

Maximum user sessions not working when user signs in Springboot/Security

I'm trying to add maximum sessions to my spring security context, however the settings are not taking effect. I've been following this code to implement JWT tokens on a spring backend using this tutorial.
I've added the relevant code I believe to the security config:
#Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
JwtAuthenticationFilter filter = new JwtAuthenticationFilter();
return filter;
}
#Override
protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authProvider());
}
http
.cors()
.and()
.csrf()
.disable()
.exceptionHandling()
.authenticationEntryPoint(unauthorizedHandler)
.and()
.authorizeRequests()
.antMatchers("/",
"/favicon.ico",
"/**/*.png",
"/**/*.gif",
"/**/*.svg",
"/**/*.jpg",
"/**/*.html",
"/**/*.css",
"/**/*.js")
.permitAll()
.antMatchers("/api/auth/**")
.permitAll()
.antMatchers("/api/user/checkUsernameAvailability", "/api/user/checkEmailAvailability")
.permitAll()
.antMatchers(HttpMethod.GET, "/api/polls/**", "/api/users/**")
.permitAll()
.antMatchers("/api/configuration/**")
.permitAll()
.anyRequest()
.authenticated()
.and().sessionManagement()
.maximumSessions(1)
.sessionRegistry(sessionRegistry());
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
#Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
#Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
However when I try and login with a user - the user can login however many times they wish.
I tried looking in the user registry to see if there were multiple user principles being created for each login but there isn't.
I also tried to step through ConcurrentSessionControlAuthenticationStrategy.onAuthentication however that doesn't seem to be being called - but I have no idea why.
Edit:
Adding the customer AuthenticationEntryPoint:
#Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final Logger log = LogManager.getLogger(AuthController.class);
#Override
public void commence(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
AuthenticationException e) throws IOException, ServletException {
log.error("Responding with unauthorized error. Message - {}", e.getMessage());
httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED,
"Sorry, You're not authorized to access this resource.");
}
}
EDIT:
The session is setting set correctly in the session registry now. However because my login endpoint has permitAll() (I think)
ConcurrentSessionControlAuthenticationStrategy.onAuthentication() is not being called and adding the newly created session to the registry.
I believe it might be something to do with the way my CustomAuthenticationProvider is setup:
#PostMapping("/signin")
public ResponseEntity<?> authenticateUser(#Valid #RequestBody LoginRequest loginRequest) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsernameOrEmail(),
loginRequest.getPassword()
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
String jwt = tokenProvider.generateToken(authentication);
return ResponseEntity.ok(new JwtAuthenticationResponse(jwt));
}
So signin is always allowing new logins as ConcurrentSessionControlAuthenticationStrategy.onAuthentication() is not being called after authentication.

Categories

Resources