Summary: I want users who are not logged in to be able to make a post request to a certain endpoint, /users/registration/create specifically. I've tried to configure my security filter chain to permit requests to /users/registration/**. I can make a GET request to /users/registration/form, fill out the fields, and then click submit. When I submit the form (to /users/registration/create), the browser responds with a 403.
I have the following configuration:
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((requests) -> requests
.requestMatchers( "/users/registration/**").permitAll()
);
return http.build();
}
I can load /users/registration/form in my browser just fine, but once I submit the form, which sends the data to /users/registration/create, I get a 403 as a response.
In case it's needed...
Here's my controller:
#Controller
public class UserRegistrationsController {
#Autowired
private AppUserService appUserService;
#GetMapping("/users/registration/form")
public String newRegistration(WebRequest request, Model model) {
AppUserDto appUserDto = new AppUserDto();
model.addAttribute("user", appUserDto);
return "users/registration";
}
#PostMapping("/users/registration/create")
public void createRegistration(
#ModelAttribute("user") #Valid AppUserDto appUserDto,
HttpServletRequest request,
Errors errors
) {
try {
AppUser registered = appUserService.registerNewUserAccount(appUserDto);
} catch (AppUserAlreadyExistsException ex) {
}
}
}
Not sure if this is the right way to do it, but I tried disabling CSRF and doing the same request again:
#Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((authorize) -> {
authorize.requestMatchers("/", "/users/registration/**").permitAll();
});
http.csrf().disable();
return http.build();
}
Related
somewhat related to this other stackoverflow topic which doesn't give a proper solution nor is applicable to Spring 6 (Spring Boot 3).
I came up with a basic spring-boot app to make my case.
There is a controller with two end-points, where one must be secured and the other accessible.
#RestController
public class TestController {
#GetMapping("/secured-api")
public String securedApi() {
return "secured";
}
#GetMapping("/public/open-api")
public String openApi() {
return "open";
}
}
Security context as follow, imagine that MyFilter is doing something fancy, e.g: validating a JWT token and firing an exception if the token is invalid / expired.
#Configuration
public class ComponentSecurityContext {
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.addFilterAt(new MyFilter(), BasicAuthenticationFilter.class)
.authorizeHttpRequests(customizer -> customizer
.requestMatchers(new AntPathRequestMatcher("/public/**"))
.permitAll()
.anyRequest()
.authenticated())
.build();
}
public static class MyFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
System.out.println("Filter is called for uri: " + request.getRequestURI());
// performs some authentication
filterChain.doFilter(request, response);
}
}
}
Executing the following two curls on the server
curl http://localhost:9003/public/open-api
curl http://localhost:9003/secured-api
is triggering MyFilter
Filter is called for uri: /public/open-api
Filter is called for uri: /secured-api
I would expect MyFilter to be called only for secured end-points, I don't care if an expired token is used to access an unprotected end-point.
Any advise on how to properly wire spring-security to achieve just that?
Working solution where the filter is scoped by the securityMatcher:
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.securityMatcher(new NegatedRequestMatcher(new AntPathRequestMatcher("/public/**")))
.addFilterAt(new MyFilter(), BasicAuthenticationFilter.class)
.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated())
.build();
}
I've been following a spring security example but I cannot make sense of it. A simple RestController replying hello on a GetMapping("/hello") with a 200 status code. Once I change it to a PostMapping I receive a 401 for the same credentials sent.
Seems I am missing something fundamental here as I would expect both requests to return a 200 status code.
The security config:
#Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
#Override
#Bean
public UserDetailsService userDetailsService() {
return new InMemoryUserDetailsManager(
List.of(
User.withUsername("john")
.password("12345")
.authorities("ROLE_ADMIN")
.build(),
User.withUsername("jane")
.password("12345")
.authorities("ROLE_MANAGER")
.build()
)
);
}
#Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic()
.and()
.authorizeRequests()
.anyRequest()
.hasRole("ADMIN");
}
}
The RestController with the following get mapping returns 200 for this call:
curl -v -u john:12345 localhost:8080/hello
and this mapping:
#RestController
public class HelloController {
#GetMapping("/hello")
public String hello() {
return "Hello!";
}
}
The RestController with the following post mapping returns 401 for this call:
curl -X POST -v -u john:12345 localhost:8080/hello
and this mapping:
#RestController
public class HelloController {
#PostMapping("/hello")
public String hello() {
return "Hello!";
}
}
Spring's CSRF protection comes enabled by default in Spring Security. POST requests are affected by this behavior.
Disable it by doing:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
}
As pointed out by #Matheus Cirillo, CSRF protection is enabled by default.
It's tempting to want to disable CSRF protection since we're using a restful api, but consider what happens if you're using a browser-based single page application to interact with the server. The same authenticated session is still available in the browser, and the application is still vulnerable to a CSRF attack.
You can find some examples of how to work with csrf protection in your own application in the docs. In a restful api, you can also provide an endpoint that returns the csrf token in a header or a response parameter.
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.
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());
}
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");
}