Technologies: Spring Security, Spring Boot on backend and ReactJs and axios on front-end.
What I want to have: When hitting a logout button on my front-end I want to log out the user. In order to do so I make a call to backend using delete. Then I want my backend to log out.
My issue: When I call Spring Security logout endpoint from my front-end I receive the following message:
Failed to load http://localhost:8080/logout: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:8888' is therefore not allowed access. The response had HTTP status code 403. I do understand why I have this error - backend is on localhost:8080 and front-end on localhost:8888. But what I don't understand is why my configuration doesn't work for logout, while it works perfectly fine for all other situations (e.g. calling spring security login endpoint or some of my custom endpoints).
How I make the call from the front-end
const endpoint = 'logout';
return axios.delete(
'http://localhost:8080/' + `${endpoint}`,
{withCredentials: true}
)
.then(response => {
let resp = {
httpCode: response.status,
data: response.data
};
return {response: resp};
})
.catch(error => {
let err = {
httpStatusCode: error.response.status,
message: `Error calling endpoint ${endpoint}`
};
return {error: err};
});
Enabling CORS from front-end
#Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurerAdapter() {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedOrigins("http://localhost:8888");
}
};
}
SecurityConfig.java - here you might notice some parts are commented - they are other solutions I tried but they didn't work.
#EnableGlobalMethodSecurity(prePostEnabled = true)
#EnableWebSecurity
#EnableJpaRepositories(basePackageClasses = UsersRepository.class)
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private CustomUserDetailsService userDetailsService;
#Autowired
private RESTAuthenticationEntryPoint restAuthenticationEntryPoint;
#Autowired
private RESTAuthenticationSuccessHandler restAuthenticationSuccessHandler;
#Autowired
private RESTAuthenticationFailureHandler restAuthenticationFailureHandler;
#Bean
public CustomLogoutHandler customLogoutHandler(){
return new CustomLogoutHandler();
}
#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()
.authorizeRequests()
.antMatchers("**/anna/**").authenticated()
.anyRequest().permitAll()
.and()
.logout()
.addLogoutHandler(customLogoutHandler())
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"));
http.exceptionHandling().authenticationEntryPoint(restAuthenticationEntryPoint);
http.formLogin().successHandler(restAuthenticationSuccessHandler);
http.formLogin().failureHandler(restAuthenticationFailureHandler);
// http
// .logout()
// .logoutUrl("/logout")
// .addLogoutHandler(new CustomLogoutHandler())
// .invalidateHttpSession(true);
// http
// .cors()
// .and()
// .csrf().disable()
// .logout()
// .logoutRequestMatcher(new AntPathRequestMatcher("/logout"));
}
private PasswordEncoder getPasswordEncoder() {
return new PasswordEncoder() {
#Override
public String encode(CharSequence charSequence) {
return charSequence.toString();
}
#Override
public boolean matches(CharSequence charSequence, String s) {
return true;
}
};
}
}
CustomLogoutHandler.java I read somewhere about the solution with setting the header here. I guess it's bad practice and would prefer not to do it, but basically I'd be happy to see the log working. Currently it's not logging anything so my guess it that it's not called on logout.
//#Slf4j
public class CustomLogoutHandler implements LogoutHandler{
#Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication){
response.setHeader("Access-Control-Allow-Origin", "*");
System.out.println("TodosLogoutHandler logging you out of the back-end app.");
}
}
I checked the source code of CorsRegistration,
public CorsConfiguration applyPermitDefaultValues() {
[...]
if (this.allowedMethods == null) {
this.setAllowedMethods(Arrays.asList(
HttpMethod.GET.name(), HttpMethod.HEAD.name(), HttpMethod.POST.name()));
}
[...]
return this;
}
As you can see, the Delete method is NOT ALLOWED by default, So you need add the delete method.
#Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurerAdapter() {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:8888")
.addAllowedMethod(HttpMethod.DELETE);
}
};
}
Related
I am currently mapping pages using Page Controller.
Every page needs to check for Session, which is a duplicate code.
How do I avoid duplicating this code?
#Controller
public class PageController {
...
#RequestMapping("/view/List")
public String list(Map<String, Object> model) {
String session_chk = Utils.SessionCheck();
if(session_chk.equals("none")){
return "/view/manager/Login";
}
return "/view/member/List";
}
#RequestMapping("/view/Detail")
public String detail(Map<String, Object> model) {
String session_chk = Utils.SessionCheck();
if(session_chk.equals("none")){
return "/view/manager/Login";
}
return "/view/member/Detail";
}
...
You could use Spring Security to avoid all duplicate code related with securing your web application and also it provides buit-in protection against attacks such as session fixation, clickjacking or cross site request forgery. It is the de-facto standard for securing Spring-based applications.
Here you can find a nice series of tutorials to learn Spring Security.
Here you can find an small example in where you'll see how I handled a similar situation to yours using Spring Security configuration only.
This is my Spring Security configuration class
#Configuration
#EnableWebSecurity
public class SecSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user1").password(passwordEncoder().encode("user1Pass")).roles("USER")
.and()
.withUser("user2").password(passwordEncoder().encode("user2Pass")).roles("USER")
.and()
.withUser("admin").password(passwordEncoder().encode("adminPass")).roles("ADMIN");
}
#Override
protected void configure(final HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
// Only users with admin role will access views starting with /admin
.antMatchers("/admin/**").hasRole("ADMIN")
// Anonymous users (users without sessions) will access this URL
.antMatchers("/anonymous*").anonymous()
// Allowing all users to access login page
.antMatchers("/login*").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/perform_login")
.defaultSuccessUrl("/homepage.html", true)
.failureHandler(authenticationFailureHandler())
.and()
.logout()
.logoutUrl("/perform_logout")
.deleteCookies("JSESSIONID")
.logoutSuccessHandler(logoutSuccessHandler())
.and()
.exceptionHandling().accessDeniedPage("/accessDenied");
}
#Bean
public LogoutSuccessHandler logoutSuccessHandler() {
return new CustomLogoutSuccessHandler();
}
#Bean
public AuthenticationFailureHandler authenticationFailureHandler() {
return new CustomAuthenticationFailureHandler();
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Add a filter which will redirect, and add check
#Component
public class SessionFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (!request.getRequestURI().contains("view/manager/Login") && "none".equals(Utils.SessionCheck())) {
httpResponse.sendRedirect("view/manager/Login.jsp");
//....
} else {
doFilterInternal(request, response, filterChain);
}
}
You can add it to all urls:
FilterRegistration sessionFilter = servletContext.addFilter("SessionFilter", SessionFilter.class);
sessionFilter.addMappingForUrlPatterns(null, false, "/*");
So I've looked around for the answer to my problem for quite a while now and tried many suggestions but I can't seem to find an answer.
The problem is, when I use Postman to check if basic auth works I get a 200 code back and it's all good, but as soon as I try to authenticate using my Login Component I get the code 401 back and says "Full authentication is required to access this resource".
I'm fairly new to Angular and completely new to using Basic Auth so I have no idea why does it work with Postman and why doesn't it work from the app.
Any help is appreciated
Below are the relevant codes
log-in.component.ts:
onLogin(form: NgForm) {
/* ... */
let headers = new Headers();
let userCredentials = user.userName + ":" + user.password;
headers.append("Origin", "http://localhost:8080");
headers.append("Authorization", "Basic " + btoa(userCredentials));
return this.http.post('http://localhost:8080/api/users/login', headers).subscribe(
(response) => {
/* ... */
},
(error) => {
console.log(error);
}
);
}
Endpoint on the server side:
#PostMapping(LOG_IN)
public ResponseEntity<User> login() {
return ResponseEntity.ok().build();
}
WebSecurityConfig:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.and()
.csrf().disable()
.authorizeRequests()
.antMatchers("/h2/**").permitAll()
.anyRequest().authenticated()
.and()
.httpBasic()
.authenticationEntryPoint(getBasicAuthEntryPoint())
.and()
.headers()
.frameOptions().disable()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
#Autowired
protected void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("admin").password("1234").roles("ADMIN");
}
#Autowired
private UserDetailsService userDetailsService;
#Autowired
protected void configureAuthentication(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
#Bean
public CustomBasicAuthenticationEntryPoint getBasicAuthEntryPoint(){
return new CustomBasicAuthenticationEntryPoint();
}
#Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
CustomBasicAuthenticationEntryPoint:
public class CustomBasicAuthenticationEntryPoint extends BasicAuthenticationEntryPoint {
#Override
public void commence(final HttpServletRequest request,
final HttpServletResponse response,
final AuthenticationException authException) throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.addHeader("WWW-Authenticate", "Basic realm=" + getRealmName() + "");
PrintWriter writer = response.getWriter();
writer.println("HTTP Status 401 : " + authException.getMessage());
}
#Override
public void afterPropertiesSet() throws Exception {
setRealmName("MY REALM");
super.afterPropertiesSet();
}
}
MyUserDetailsService:
#Service
public class MyUserDetailsService implements UserDetailsService {
#Autowired
private UserRepository userRepository;
#Autowired
private AuthenticatedUser authenticatedUser;
#Override
#Transactional(readOnly = true)
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<User> oUser = userRepository.findByUserName(username);
if (!oUser.isPresent()) {
throw new UsernameNotFoundException(username);
}
User user = oUser.get();
authenticatedUser.setUser(user);
Set<GrantedAuthority> grantedAuthorities = new HashSet<>();
grantedAuthorities.add(new SimpleGrantedAuthority(user.getRole().toString()));
return new org.springframework.security.core.userdetails.User(user.getUserName(), user.getPassword(), grantedAuthorities);
}
}
You need to pass the headers as 3rd parameter for the post method. The 2nd one is the body
return this.http.post('http://localhost:8080/api/users/login', {}, {headers}).subscribe(
(response) => {
If you are using angular 6, you should really be using the new HttpClient class, the old Http class being deprecated
This is because the browser send OPTION method to the server before send your request, , try to update your security configuration by allowing OPTION method. like this
protected void configure(HttpSecurity http) throws Exception
{
http
.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS,"/path/to/allow").permitAll()//allow CORS option calls
.antMatchers("/resources/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.httpBasic();
}
I've been having an issue with CORS and I have tried everything I could find on Stack Overflow and basically anything that I found on Google and have had no luck.
So I have user authentication on my backend and I have a login page on my frontend. I hooked up the login page with Axios so I could make a post request and tried to login but I kept getting errors like "Preflight request" so I fixed that then I started getting the "Post 403 Forbidden" error.
It appeared like this:
POST http://localhost:8080/api/v1/login/ 403 (Forbidden)
Even trying to login using Postman doesn't work so something is clearly wrong. Will be posting class files below
On my backend, I have a class called WebSecurityConfig which deals with all the CORS stuff:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private UserDetailsServiceImpl userDetailsService;
#Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurerAdapter() {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedMethods("GET", "POST", "HEAD", "PUT", "DELETE", "OPTIONS");
}
};
}
#Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*"); // TODO: lock down before deploying
config.addAllowedHeader("*");
config.addExposedHeader(HttpHeaders.AUTHORIZATION);
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.headers().frameOptions().disable();
http
.cors()
.and()
.csrf().disable().authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/h2/**").permitAll()
.antMatchers(HttpMethod.POST, "/api/v1/login").permitAll()
.anyRequest().authenticated()
.and()
// We filter the api/login requests
.addFilterBefore(new JWTLoginFilter("/api/v1/login", authenticationManager()),
UsernamePasswordAuthenticationFilter.class);
// And filter other requests to check the presence of JWT in header
//.addFilterBefore(new JWTAuthenticationFilter(),
// UsernamePasswordAuthenticationFilter.class);
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// Create a default account
auth.userDetailsService(userDetailsService);
// auth.inMemoryAuthentication()
// .withUser("admin")
// .password("password")
// .roles("ADMIN");
}
}
On our frontend which is written in VueJS and using Axios to make the call
<script>
import { mapActions } from 'vuex';
import { required, username, minLength } from 'vuelidate/lib/validators';
export default {
data() {
return {
form: {
username: '',
password: ''
},
e1: true,
response: ''
}
},
validations: {
form: {
username: {
required
},
password: {
required
}
}
},
methods: {
...mapActions({
setToken: 'setToken',
setUser: 'setUser'
}),
login() {
this.response = '';
let req = {
"username": this.form.username,
"password": this.form.password
};
this.$http.post('/api/v1/login/', req)
.then(response => {
if (response.status === 200) {
this.setToken(response.data.token);
this.setUser(response.data.user);
this.$router.push('/dashboard');
} else {
this.response = response.data.error.message;
}
}, error => {
console.log(error);
this.response = 'Unable to connect to server.';
});
}
}
}
</script>
So when I debugged via Chrome's tools (Network), I noticed that the OPTIONS request goes through as shown below:
Here is a picture of the POST error:
Here is another class which handles the OPTIONS request (JWTLoginFilter as referenced in the WebSecurityConfig):
public class JWTLoginFilter extends AbstractAuthenticationProcessingFilter {
public JWTLoginFilter(String url, AuthenticationManager authManager) {
super(new AntPathRequestMatcher(url));
setAuthenticationManager(authManager);
}
#Override
public Authentication attemptAuthentication(
HttpServletRequest req, HttpServletResponse res)
throws AuthenticationException, IOException, ServletException {
AccountCredentials creds = new ObjectMapper()
.readValue(req.getInputStream(), AccountCredentials.class);
if (CorsUtils.isPreFlightRequest(req)) {
res.setStatus(HttpServletResponse.SC_OK);
return null;
}
return getAuthenticationManager().authenticate(
new UsernamePasswordAuthenticationToken(
creds.getUsername(),
creds.getPassword(),
Collections.emptyList()
)
);
}
#Override
protected void successfulAuthentication(
HttpServletRequest req,
HttpServletResponse res, FilterChain chain,
Authentication auth) throws IOException, ServletException {
TokenAuthenticationService
.addAuthentication(res, auth.getName());
}
}
When you configure Axios, you can simply specify the header once and for all:
import axios from "axios";
const CSRF_TOKEN = document.cookie.match(new RegExp(`XSRF-TOKEN=([^;]+)`))[1];
const instance = axios.create({
headers: { "X-XSRF-TOKEN": CSRF_TOKEN }
});
export const AXIOS = instance;
Then (here I assume you use SpringBoot 2.0.0, while it should work also in SpringBoot 1.4.x onward) in your Spring Boot application you should add the following security configs.
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
// CSRF Token
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
// you can chain other configs here
}
}
In this way, Spring will return the token as a cookie in the response (I assume you do a GET first) and you will read it in the AXIOS configuration file.
You should not disable CSRF as per Spring Security documentation except, few special cases. This code will put the CSRF header to VUE. I used vue-resource.
//This token is from Thymeleaf JS generation.
var csrftoken = [[${_csrf.token}]];
console.log('csrf - ' + csrftoken) ;
Vue.http.headers.common['X-CSRF-TOKEN'] = csrftoken;
Hope this helps.
Axios will, by default, handle the X-XSRF-TOKEN correctly.
So the only action is to configure the server, like JeanValjean explained:
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
// CSRF Token
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
// you can chain other configs here
}
}
Axios will automatically send the correct token in the request headers, so there's no need to change the front-end.
I had the same kind of problem where a GET request was working, and yet a POST request was replied with status 403.
I found that for my case, it was because of the CSRF protection enabled by default.
A quick way to make sure of this case is to disable CSRF:
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// …
#Override
protected void configure(HttpSecurity http) throws Exception {
// …
http.csrf().disable();
// …
}
// …
}
More information on Spring-Security website.
Mind that disabling CSRF isn't always the correct answer as it is there for security purpose.
I try to add spring security to my webapp. I've found this tuto : spring boot security application. But I get an error 400 on my login page.
Here is the code of my security config :
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
#Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private UserDetailsService userDetailsService;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/bus/topologie", "/home").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/bus/login")
.failureUrl("/bus/login?error")
.usernameParameter("email")
.permitAll()
.and()
.logout()
.logoutUrl("/bus/logout")
.logoutSuccessUrl("/")
.permitAll();
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(new BCryptPasswordEncoder());
}
}
Here is the code of my controller, when using Java 7 optional instead of Java 8 Optional, the request dispatcher doesn't find it :
#Controller
public class LoginController {
private static final Logger LOGGER = LoggerFactory.getLogger(LoginController.class);
#RequestMapping(value = "/bus/login", method = RequestMethod.GET)
public ModelAndView getLoginPage(#RequestParam Optional<String> error) {
LOGGER.debug("Getting login page, error={}", error);
return new ModelAndView("login", "error", error);
}
}
And the code of the application.java :
#SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
#Bean
public EmbeddedServletContainerCustomizer containerCustomizer() {
return new EmbeddedServletContainerCustomizer() {
#Override
public void customize(ConfigurableEmbeddedServletContainer container) {
ErrorPage error400Page = new ErrorPage(HttpStatus.BAD_REQUEST, "/400.html");
ErrorPage error401Page = new ErrorPage(HttpStatus.UNAUTHORIZED, "/401.html");
ErrorPage error404Page = new ErrorPage(HttpStatus.NOT_FOUND, "/404.html");
ErrorPage error500Page = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/500.html");
container.addErrorPages(error401Page, error404Page, error500Page, error400Page);
}
};
}
}
The login.html page is almost empty.
Edit : I've found a good lead, but I don't know how to correct it. When I remove the attribute #RequestParam Optional<String> error, everything works fine. But I need to handle the error. I'm working with java 7, using guava optional instead of java 8 java.util.Optional. What is the good way to do this in java 7?
Probably it is because you are requesting a param that you are not getting all times... you have two options.
Ensure you always send that param.
Make it not required.
If you want to make it not required you can do something like this
#RequestParam((value = "error", required=false)String error)
Form based security is redirection to the login page if a user is not authenticated and tries to access a protected action. Instead of the redirect I want it to return HTTP code 403.
As far as I understand, I have to register some kind of entry point for this. Unfortunately I don't undertand how I can set this up for a java based configuration.
This is my security config:
#Configuration
#EnableWebMvcSecurity
pubfooc class FOOSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
//#formatter:off
protected void configure(HttpSecurity http) throws Exception {
http
.authenticationProvider(aS400AuthenticationProvider())
.formLogin()
.loginProcessingUrl("/authorized")
.passwordParameter("password")
.usernameParameter("cfooentId")
.successHandler(foorAuthenticationSuccessHandler())
.failureHandler(foorAuthenticationFailureHandler())
.and()
.csrf().disable()
.rememberMe()
.rememberMeServices(foorRememberMeServices())
.key(CookieService.FOO_SESSION_COOKIE_NAME)
.and()
.sessionManagement().sessionCreationPofoocy(SessionCreationPofoocy.STATELESS)
;
}
//#formatter:on
#Bean
pubfooc FOORememberMeServices foorRememberMeServices() {
return new FOORememberMeServices();
}
#Bean
pubfooc AS400AuthenticationProvider aS400AuthenticationProvider() {
return new AS400AuthenticationProvider();
}
#Bean
pubfooc CookieService cookieService() {
return new CookieService.Impl();
}
#Bean
pubfooc FOOAuthenticationSuccessHandler foorAuthenticationSuccessHandler() {
return new FOOAuthenticationSuccessHandler();
}
#Bean
pubfooc FOOAuthenticationFailureHandler foorAuthenticationFailureHandler() {
return new FOOAuthenticationFailureHandler();
}
}
You should try http.exceptionHandling().authenticationEntryPoint(new org.springframework.security.web.authentication.Http403ForbiddenEntryPoint()) (assuming you are using Spring Security 2.0 or higher).