SpringSecurity: How to Continue Forwarding Request to RestController After a Successful Authentication? - spring-boot

I am doing a pure backend project with REST APIs (not MVC) and would like to use SpringSecurity with JWT token to project these APIs. The implementation is good and all APIs are successfully protected with the token, and I can post a JSON string with username and password to "/login" path to get token
My problem is:
The SpringSecurity will return the response with token directly in successfulAuthentication() rather than keep forwarding to RestController (RestController's "/login" path gets no data)
And my question is:
What should I do, after a successful authentication, to allow SpringSecurity can keep forwarding the request to RestController's "/login" path so that I can do something else on the request and the newly built token beside the security in the path?
Appreciate any helps, Thank you!
My code:
#Component
public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter {
// ...
#Override
protected void configure(HttpSecurity http) throws Exception {
// ...
http.authorizeRequests()
.antMatchers("/registry").permitAll() // allow path /registry
.antMatchers("/login").permitAll() // allow path /login
.antMatchers("/verify").permitAll() // allow path /verify
.anyRequest().authenticated();
// ...
}
}
#RestController
public class EntranceEndpoint {
#RequestMapping(path = "/login", method = RequestMethod.POST)
public RestResponse<String> login(LoginMetaInfo login) {
System.out.println(login); // no output here when login
// some further operations for a successful login, and return a REST response
}
}
And this is what the SpringSecurity do on a successful login
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
// ...
/**
* on login success
*/
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication auth) throws IOException {
// here build the token and insert into response for commitment
// - the SpringSecurity soon returns the response directly, rather then keep forwarding to RestController
String token = xxxx;
response.setStatus(StatusCode.SUCCESS().getCode());
RestResponse<String> body = RestResponse.succeeded(StatusCode.SUCCESS().withMsg(LoginResponseCode.LOGIN), token);
response.setContentType(MediaType.APPLICATION_JSON);
response.setCharacterEncoding(MediaType.CHARSET);
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(response.getWriter(), body );
}
}

What about simply using HttpServletResponse's sendRedirect instead of writing to the response?
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication auth) throws IOException {
// do what you want here
response.sendRedirect("/login");
// response.sendRedirect("https://yoururl");
}

Related

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

Spring boot authorization returns 403 for any authorization request using #RolesAllowed, #Secured or #PreAuthorize

I've been working from this article (and a few other similar ones): https://medium.com/omarelgabrys-blog/microservices-with-spring-boot-authentication-with-jwt-part-3-fafc9d7187e8
The client is an Angular 8 app which acquires a Jwt from an independent microservice. Trying to add filter(s) to a different microservice to require specific authorization via jwt roles.
Consistently receiving 403 errors.
Security Config:
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled=true,
securedEnabled = true,
jsr250Enabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private BCryptPasswordEncoder bCryptPasswordEncoder;
public WebSecurityConfig() {}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors().and().csrf().disable()
// make sure we use stateless session; session won't be used to store user's state.
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// Add a filter to validate the tokens with every request
.addFilterAfter(new JwtAuthorizationFilter2(), UsernamePasswordAuthenticationFilter.class)
// authorization requests config
.authorizeRequests()
// Any other request must be authenticated
.anyRequest().authenticated();
}
}
Filter:
public class JwtAuthorizationFilter2 extends OncePerRequestFilter {
private final String HEADER = "Authorization";
private final String PREFIX = "Bearer ";
private final String SECRET = "foo";
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
String token = request.getHeader(SecurityConstants.HEADER_STRING);
if (token != null) {
// parse the token.
DecodedJWT decoded = JWT.require(Algorithm.HMAC512(SecurityConstants.SECRET.getBytes()))
.build()
.verify(token.replace(SecurityConstants.TOKEN_PREFIX, ""));
String user = decoded.getSubject();
List<SimpleGrantedAuthority> sgas = Arrays.stream(
decoded.getClaim("roles").asArray(String.class))
.map( s -> new SimpleGrantedAuthority(s))
.collect( Collectors.toList());
if (sgas != null) {
sgas.add(new SimpleGrantedAuthority("FOO_Admin"));
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
user,
null,
sgas);
SecurityContextHolder.getContext().setAuthentication(auth);
}
else {
SecurityContextHolder.clearContext();
}
chain.doFilter(request, response);
}
}
}
This code works fine without any authorization requirements defined, but if an authorization is defined in WebSecurityConfig, or by decorating a controller method, http 403 is received for all requests in scope.
Examples:
.authorizeRequests().antMatchers("/**").hasRole("FOO_Admin")
// or any of these
#PreAuthorize("hasRole('FOO_Admin')")
#RolesAllowed({"FOO_Admin"})
#Secured({"FOO_Admin"})
Device get(#PathVariable String id) {
// some code
}
When code is halted at SecurityContextHolder.getContext().setAuthentication(auth),
auth.authenticated = true
and
auth.authorities includes a SimpleGrantedAuthority for "FOO_Admin"
So I'm wondering whether:
The FilterChain needs an Authentication Filter (or does authentication occur in JwtAuthorizationFilter2?)?
There is not a spelling or formatting or capitalization difference to role name.
I'm stupefied. Any help would be greatly appreciated.
#PreAuthorize("hasRole('FOO_Admin')) expects the user has an authority ROLE_FOO_Admin, which will be prefixed by ROLE_. However, the user only has the authority FOO_Admin , hence it fails to access the method.
You have several options:
(1) Change the prefix by declaring a GrantedAuthorityDefaults bean:
#Bean
GrantedAuthorityDefaults grantedAuthorityDefaults() {
return new GrantedAuthorityDefaults("FOO");
}
And use #PreAuthorize(hasRole('Admin')) to secure the method.
(2) Or more simpler is to use #PreAuthorize("hasAuthority('FOO_Admin')") , which will directly check if the user has the authority FOO_Admin , without adding any prefix to it.
P.S JwtAuthorizationFilter2 only verifies if an user is valid and get the related user information which prepare for the authorization user later. It is an authentication and I would rename it to JwtAuthenticationFilter2 to describe more exactly what it does actually.

Spring boot basic authentication with token for a RESTAPI

I need to provide user login with SpringBoot application.
User login request will be a Rest request having payload comprise of "username" and "password".
I need to validate those credentials first time from DB and generate a token having validity for specific time.
Then after login all the subsequent requests will have that token, and that token will be verified each time.
I have done the token verification part but I am really confused about first time login, I have no clue how to do it.
Even on first time login request, system is going to check for token authentication which obviously getting failed.
I want system to simply generate token on first time after validating name and password from db.
This is the first time I am implementing User login with Spring Boot Security, so I am pretty clueless about it. Although I have researched and read a lot online but still not able to figure out this part.
EDIT:
Following is the security config class which extends WebSecurityConfigurerAdapter
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(getPasswordEncoder());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable();
http.authorizeRequests()
.antMatchers("/","**/firstPage").authenticated()
.anyRequest().permitAll()
.and()
.formLogin().loginPage("/login").
permitAll()
.and().logout().permitAll();
}
Following is the request that will be called after login.How to authenticate user in it using the token already generated? Token is being sent in Header of the request.
#PostMapping(value = "/home")
public ResponseEntity<ConsolidateResponse> TestReques(#RequestBody TestParam testParam)
throws Exception {
//Some logic
}
If you disable form login from spring security configuration class and expose one rest endpoint (/auth) you can handle login and generate token.Here i used jwt for token generation.
#RequestMapping(value = "/auth", method = RequestMethod.POST)
public ResponseEntity<?> createAuthenticationToken(#RequestBody JwtAuthenticationRequest authenticationRequest) throws AuthenticationException, IOException {
// Perform the security
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
authenticationRequest.getUsername(), authenticationRequest.getPassword());
final Authentication authentication = authManager.authenticate(token);
if (!authentication.isAuthenticated()) {
throw new BadCredentialsException("Unknown username or password");
}
// Reload password post-security so we can generate token
final UserDetails userDetails = userDetailsService.loadUserByUsername(authenticationRequest.getUsername());
final String jwtoken = jwtTokenUtil.generateToken(userDetails);
return ResponseEntity.ok(responseBean);
}
When use stateless authentication we can pass token parameter explicitly to controller and validate it.In case session based authentication is on we can also use #AuthenticationPrincipal for to retrieve current logged in user.
//Stateless authentication
#PostMapping(value = "/home")
public ResponseEntity<ConsolidateResponse> test(#RequestBody TestParam testParam,String token)
throws Exception {
Boolean isValidToken = jwtTokenUtil.validateToken(token);
if(isValidToken) {
//Some logic
}else {
//invalid request
}
}
#PostMapping(value = "/home")
public ResponseEntity<ConsolidateResponse> test(#RequestBody TestBean requestToken,
#AuthenticationPrincipal User contextPrincipal, HttpServletRequest req) {
Optional.ofNullable(contextPrincipal).orElseThrow(InvalidUserSession::new);
//some logic
}

Spring Boot Redirect to requested URL after login

I have a Spring Boot UI application. I am trying to redirect users to the originally requested URL after login.
When a user requests http://www.example.com/myapp/user/22, the application aptly redirects to http://www.example.com/myapp/login. Once the user logs in, the application redirects to http://www.example.com/myapp/dashboard. I would like the application to redirect to http://www.example.com/myapp/user/22.
I have gone through several links and feel I have a proper configuration, yet, redirection is not working as expected.
My Security Config is
public class SecurityConfig extends WebSecurityConfigurerAdapter {
.....
....
#Autowired
private MyAuthenticationSuccessHandler authenticationSuccessHandler;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.
authorizeRequests()
.antMatchers("/user/**").authenticated()
.and().csrf().disable().formLogin()
.successHandler(authenticationSuccessHandler)
......
and My Success Handler is
#Component
public class MyAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
...
public MyAuthenticationSuccessHandler() {
super();
this.setDefaultTargetUrl("/myapp/dashboard");
this.setUseReferer(true);
}
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
//Do something ..........
........
.........
super.onAuthenticationSuccess(request, response, authentication);
}
I tried using SavedRequestAwareAuthenticationSuccessHandler too.
I notice that my success handler is invoked, but the target URL is always /user/login and my login controller is invoked..
#RequestMapping("/login")
public ModelAndView login(#ModelAttribute() {
if(!userIdentified) {
//go to login page
} else {
new ModelAndView("redirect:/myapp/dashboard");
}
}
and the user is redirected to "dashboard".
What else am I missing?
Use "Referer" from session attribute to get the latest request URL. On my app, i use this one
public class CustomAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
public static final String REDIRECT_URL_SESSION_ATTRIBUTE_NAME = "REDIRECT_URL";
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
Object redirectURLObject = request.getSession().getAttribute(REDIRECT_URL_SESSION_ATTRIBUTE_NAME);
if(redirectURLObject != null)
setDefaultTargetUrl(redirectURLObject.toString());
else{
setDefaultTargetUrl("/");
}
request.getSession().removeAttribute(REDIRECT_URL_SESSION_ATTRIBUTE_NAME);
super.onAuthenticationSuccess(request, response, authentication);
}
}
Edit :
Sorry i forgot to show the login controller
#RequestMapping(method = RequestMethod.GET, value = {"/login"})
String login(Model model, Principal principal, HttpServletRequest request) throws Exception{
String referer = request.getHeader("Referer"); //Get previous URL before call '/login'
//save referer URL to session, for later use on CustomAuthenticationSuccesshandler
request.getSession().setAttribute(CustomAuthenticationSuccessHandler.REDIRECT_URL_SESSION_ATTRIBUTE_NAME, referer);
return principal == null ? "login" : "redirect:/";
}
Although Singgih S answer works, BUT there is a better way as below :
Ref:
https://www.baeldung.com/spring-security-redirect-login
There is no magic in these easy to use features in Spring Security.
When a secured resource is being requested, the request will be
filtered by a chain of various filters. Authentication principals and
permissions will be checked. If the request session is not
authenticated yet, AuthenticationException will be thrown.
The AuthenticationException will be caught in the
ExceptionTranslationFilter, in which an authentication process will be
commenced, resulting in a redirection to the login page.
Therefore :
1. When redirection to the "/login" page occurs, your secured request url is saved in the session as DefaultSavedRequest object.
2. Also we know when a successful form based login occurs, one of the implementations of AuthenticationSuccessHandler is called.
so we can create a custom class and get DefaultSavedRequest in it as below :
public class CustomAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
DefaultSavedRequest defaultSavedRequest = (DefaultSavedRequest) request.getSession().getAttribute("SPRING_SECURITY_SAVED_REQUEST");
if(defaultSavedRequest != null){
getRedirectStrategy().sendRedirect(request, response, defaultSavedRequest.getRedirectUrl());
}else{
super.onAuthenticationSuccess(request, response, authentication);
}
}
}
3. We have to introduce this class in WebSecurityConfigurerAdapter :
#Override
protected void configure(HttpSecurity http) throws Exception {
http.(...).anyRequest().authenticated().and()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/login")
.successHandler(new CustomAuthenticationSuccessHandler());
So you can implement your logic in the above onAuthenticationSuccess method.
Best wishes
The Spring route, ala extending SavedRequestAwareAuthenticationSuccessHandler or SimpleUrlAuthenticationSuccessHandler can be a bit clunky to implement. In the controller (ex. a POST method that processes logins), you can do the header request yourself; ex:
HttpServletRequest request =null;
String priorUrl = request.getHeader("Referer");
You will notice that you will have the URL prior to either a manual (initiated by user) logout or a session timeout (as handled by Spring session): you'll get an https://iAmPriorUrl.com/.... Then you can do whatever you want with it.

SSO with Spring security

I have an application, where user is pre-authorized by SSO and lands to my page, now I need to make a call to another rest api to get some data, which is running on another server, but it will be use the same authentication. So I just wanted to know, how I can provide the authentication process? Do I need to set the cookie what I am getting from the incoming request.
When the request lands on your page it should have a token or key, in the http AUTHORIZATION header, this should be used with a filter
public class AuthFilter extends OncePerRequestFilter {
private String failureUrl;
private SimpleUrlAuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
try {
// check your SSO token here
chain.doFilter(request, response);
} catch (OnlineDriverEnquiryException ode) {
failureHandler.setDefaultFailureUrl(failureUrl);
failureHandler.onAuthenticationFailure(request, response, new BadCredentialsException("Captcha invalid!"));
}
}
public String getFailureUrl() {
return failureUrl;
}
public void setFailureUrl(String failureUrl) {
this.failureUrl = failureUrl;
}
}
Also read this post on how to set up the auto config. Spring security without form login

Resources