Im currently having a separate AngularJS on a Apache HTTP Server and a Spring boot Backend on a Tomcat 8 . The backend serves as a Rest API .I want to secure my application. I got many advices to use csrf token that provides spring security and consume it with angualrjs.But i don't really know how to use Angular JS to authenticate a user via a form and fetch a secure resource to render in the UI.
this is my main application class:`
package org.test;
import java.io.IOException;
import java.security.Principal;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.CsrfTokenRepository;
import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.WebUtils;
#SpringBootApplication
#RestController
public class TaliopjarApplication {
#RequestMapping("/user")
public Principal user(Principal user) {
return user;
}
#RequestMapping("/resource")
public Map<String, Object> home() {
Map<String, Object> model = new HashMap<String, Object>();
model.put("id", UUID.randomUUID().toString());
model.put("content", "Hello World");
return model;
}
public static void main(String[] args) {
SpringApplication.run(TaliopjarApplication.class, args);
}
#Configuration
#Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
protected static class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
public void globalConfig(AuthenticationManagerBuilder auth,DataSource dataSource) throws Exception{
/*auth.inMemoryAuthentication().withUser("admin").password("123").roles("0");
auth.inMemoryAuthentication().withUser("talent1").password("123").roles("1");*/
auth.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery("select str_login as principal, str_password as credentials, true from t_user where str_login = ?")
.authoritiesByUsernameQuery("select str_login as principal, ln_type as role from t_user where str_login = ?")
.rolePrefix("ROLE_");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic().and().authorizeRequests()
.antMatchers("/index.html", "/home.html", "/login.html", "/",
"/data/**","/myApp.js","/assets/**","/skilltalent").permitAll().anyRequest()
.authenticated()
.and().formLogin().loginPage("/login")
.permitAll().defaultSuccessUrl("/index.html")
.and().csrf()
.csrfTokenRepository(csrfTokenRepository()).and()
.addFilterAfter(csrfHeaderFilter(), CsrfFilter.class);
}
private Filter csrfHeaderFilter() {
return new OncePerRequestFilter() {
#Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class
.getName());
if (csrf != null) {
Cookie cookie = WebUtils.getCookie(request, "XSRF-TOKEN");
String token = csrf.getToken();
if (cookie == null || token != null
&& !token.equals(cookie.getValue())) {
cookie = new Cookie("XSRF-TOKEN", token);
cookie.setPath("/");
response.addCookie(cookie);
}
}
filterChain.doFilter(request, response);
}
};
}
private CsrfTokenRepository csrfTokenRepository() {
HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
repository.setHeaderName("X-XSRF-TOKEN");
return repository;
}
}
}
this is my angular app
angular.module('hello', [ 'ngRoute' ])
.config(function($routeProvider, $httpProvider) {
$routeProvider.when('/', {
templateUrl : 'index.html',
controller : 'home',
controllerAs: 'controller'
}).when('/login', {
templateUrl : 'login.html',
controller : 'navigation',
controllerAs: 'controller'
}).otherwise('/');
$httpProvider.defaults.headers.common['X-Requested-With'] ='XMLHttpRequest';
})
.controller('navigation',
function($rootScope, $http, $location, $route) {
var self = this;
self.tab = function(route) {
return $route.current && route === $route.current.controller;
};
var authenticate = function(credentials, callback) {
var headers = credentials ? {
authorization : "Basic "
+ btoa(credentials.username + ":"
+ credentials.password)
} : {};
$http.get('user', {
headers : headers
}).then(function(response) {
if (response.data.name) {
$rootScope.authenticated = true;
} else {
$rootScope.authenticated = false;
}
callback && callback($rootScope.authenticated);
}, function() {
$rootScope.authenticated = false;
callback && callback(false);
});
}
authenticate();
self.credentials = {};
self.login = function() {
alert("llllll");
authenticate(self.credentials, function(authenticated) {
if (authenticated) {
console.log("Login succeeded")
$location.path("/");
self.error = false;
$rootScope.authenticated = true;
} else {
console.log("Login failed")
$location.path("/login");
self.error = true;
$rootScope.authenticated = false;
}
})
};
self.logout = function() {
$http.post('logout', {}).finally(function() {
$rootScope.authenticated = false;
$location.path("/");
});
}
}).controller('home', function($http) {
var self = this;
$http.get('/resource/').then(function(response) {
self.greeting = response.data;
})
});
All works fine when i use just one server (tomcat) but i don't how to do it with appache for the frontend and tomcat for the backend.
Any help, advice and experience is welcome.
Related
So I am new to Spring Security and I tried to build a Spring Boot app with REST services and also implement Spring Security with JWT. I have followed a security tutorial for the most part and I got the app running but when I try to call an authentication(/auth/**) endpoint I just get the 401 Unauthorized error. Since spring has updated to v 3.0.0 and now WebSecurityConfigurerAdapter is deprecated I have changed the configurations accordingly.
This is my configuration file
import com.example.myshroombackend.security.JwtAuthenticationEntryPoint;
import com.example.myshroombackend.security.JwtAuthenticationFilter;
import com.example.myshroombackend.service.AuthServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
#Configuration
#EnableWebSecurity
public class SecurityConfig{
#Autowired
AuthServiceImpl authService;
#Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
#Bean JwtAuthenticationFilter jwtAuthenticationFilter(){
return new JwtAuthenticationFilter();
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(authService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
#Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfiguration) throws Exception {
return authConfiguration.getAuthenticationManager();
}
#Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().requestMatchers("/js/**", "/images/**");
}
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf()
.and()
.cors()
.disable()
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeHttpRequests()
.requestMatchers("/auth/**").permitAll() //every /auth/login or /auth/register are permitted
.anyRequest().authenticated(); //all other requests must be authenticated ==>
// you first have to authenticate yourself to perform the other requests
http.authenticationProvider(authenticationProvider());
http.addFilterBefore(jwtAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
my custom filter
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
public class JwtAuthenticationFilter extends OncePerRequestFilter {
#Autowired
AuthServiceImpl authService;
#Autowired
private JwtTokenProvider jwtTokenProvider;
#Override
protected void doFilterInternal(#NotNull HttpServletRequest request, #NotNull HttpServletResponse response,#NotNull FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = parseJwt(request);
if (jwt != null && jwtTokenProvider.validateToken(jwt)) {
String username = jwtTokenProvider.getUserNameFromToken(jwt);
UserDetails userDetails = authService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails,
null,
userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
logger.error("Cannot set user authentication: {}", e);
}
filterChain.doFilter(request, response);
}
private String parseJwt(HttpServletRequest request) {
String jwt = jwtTokenProvider.getJwtFromCookies(request);
return jwt;
}
}
my token service
import com.example.myshroombackend.exception.JwtAuthenticationException;
import com.example.myshroombackend.service.AuthServiceImpl;
import com.example.myshroombackend.service.UserDetailsImpl;
import io.jsonwebtoken.*;
import jakarta.servlet.http.Cookie;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseCookie;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.util.WebUtils;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.Optional;
#Component
#Slf4j
public class JwtTokenService {
#Value("${application.secret")
private String jwtSecret;
#Value("${application.jwtCookieName}")
private String jwtCookie;
private AuthServiceImpl authService;
private static final Instant EXPIRATIONTIME = Instant.now().plus(20, ChronoUnit.HOURS);
public String generateToken(String userName) {
Instant now = Instant.now();
return Jwts.builder()
.setSubject(userName)
.setIssuedAt(Date.from(now))
.setExpiration(Date.from(EXPIRATIONTIME))
.signWith(SignatureAlgorithm.HS512, jwtSecret.getBytes(StandardCharsets.UTF_8))
.compact();
}
public boolean validateToken(String token) {
if ((token != null) && (!"".equals(token))) {
Jws<Claims> claimsJws;
try{
claimsJws = Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token);
if (claimsJws != null) {
final String userId = Optional
.ofNullable(claimsJws.getBody().get(new String("id")))
.map(Object::toString)//
.orElseThrow(
() -> new AuthenticationCredentialsNotFoundException("No username given in jwt"));
}
return true;
} catch (ExpiredJwtException e) {
throw new RuntimeException(e);
} catch (UnsupportedJwtException e) {
throw new RuntimeException(e);
} catch (MalformedJwtException e) {
throw new RuntimeException(e);
} catch (SignatureException e) {
throw new RuntimeException(e);
} catch (IllegalArgumentException e) {
throw new RuntimeException(e);
} catch (AuthenticationCredentialsNotFoundException e) {
throw new RuntimeException(e);
}
}
return false;
}
public String generateToken(Authentication authentication) {
User user = (User) authentication.getPrincipal();
return generateToken(user.getUsername());
}
public String getJwtFromCookies(HttpServletRequest request) {
Cookie cookie = WebUtils.getCookie(request, jwtCookie);
if (cookie != null) {
return cookie.getValue();
} else {
return null;
}
}
public String generateTokenFromUsername(String username) {
return Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date((new Date()).getTime() + 10000000))
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
public ResponseCookie generateJwtCookie(UserDetailsImpl userPrincipal) {
String jwt = generateTokenFromUsername(userPrincipal.getUsername());
ResponseCookie cookie = ResponseCookie.from(jwtCookie, jwt).path("/auth").maxAge(24 * 60 * 60).httpOnly(true).build();
return cookie;
}
public String getUserNameFromToken(final String token) throws JwtAuthenticationException {
String userName = null;
try {
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(jwtSecret.getBytes(StandardCharsets.UTF_8)).parseClaimsJws(token);
if (claimsJws != null) {
userName = claimsJws.getBody().getSubject();
}
} catch (final SignatureException | MalformedJwtException | UnsupportedJwtException ex) {
log.error("Unsupported jwt token {} with exception {}",
token,
ex.getMessage());
throw new JwtAuthenticationException(ex);
} catch (final ExpiredJwtException ex) {
log.error("Expired jwt token {}",
ex.getMessage());
throw new JwtAuthenticationException(ex);
} catch (final AuthenticationCredentialsNotFoundException ex) {
log.error("An error occured while trying to create authentication based on jwt token, missing credentials {}",
ex.getMessage());
throw new JwtAuthenticationException(ex);
} catch (final Exception ex) {
log.error("Unexpected exception occured while parsing jwt {} exception: {}",
token,
ex.getMessage());
throw new JwtAuthenticationException(ex);
}
return userName;
}
}
also my RestController
import com.example.myshroombackend.dto.LoginRequestDto;
import com.example.myshroombackend.entity.Rank;
import com.example.myshroombackend.entity.UserEntity;
import com.example.myshroombackend.repository.UserRepository;
import com.example.myshroombackend.security.JwtTokenService;
import com.example.myshroombackend.service.UserDetailsImpl;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;
import java.util.Optional;
#RestController
#RequestMapping("/auth")
#CrossOrigin
public class AuthController {
private final UserRepository userRepository;
private AuthenticationManager authenticationManager;
private JwtTokenService provider;
private PasswordEncoder encoder;
public AuthController(UserRepository userRepository, AuthenticationManager authenticationManager, JwtTokenService provider) {
this.userRepository = userRepository;
this.authenticationManager = authenticationManager;
this.provider = provider;
}
#PostMapping("/login")
public ResponseEntity<LoginRequestDto> loginUser(#RequestBody LoginRequestDto dto) {
Authentication authentication = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(dto.getUserName(), dto.getPassword()));
UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
SecurityContextHolder.getContext().setAuthentication(authentication);
ResponseCookie jwtCookie = provider.generateJwtCookie(userDetails);
return ResponseEntity.ok().header(HttpHeaders.SET_COOKIE, jwtCookie.toString())
.body(new LoginRequestDto(
userDetails.getUsername()));
}
#PostMapping("/register")
public ResponseEntity<?> registerUser( #RequestBody LoginRequestDto dto) {
Optional<UserEntity> optionalUserEntity = userRepository.findByUserName(dto.getUserName());
if (optionalUserEntity.isPresent()) {
return ResponseEntity.badRequest().body("User already in database");
}
// Create new user's account
UserEntity userEntity = new UserEntity();
userEntity.setUserName(dto.getUserName());
userEntity.setPassword(encoder.encode(dto.getUserName()));
userEntity.setRank(Rank.BEGINNER);
userRepository.save(userEntity);
return ResponseEntity.ok("User registered successfully!");
}
}
I use requestMatchers() instead of antMatchers() that is no longer in this security version and authorizeHttpRequests() instead of authorizeRequests(). I don't know if the problem is from there or it's something else
When authorizeHttpRequests is used instead of authorizeRequests, then AuthorizationFilter is used instead of FilterSecurityInterceptor.
And as I understand, prior to Spring Security 6, AuthorizationFilter and FilterSecurityInterceptor were performing authorization checks on the first request only, but since Spring Security 6, it seems to be applied to every request (i.e. on all dispatcher types) at least for AuthorizationFilter (FilterSecurityInterceptor is now deprecated).
So my guess is that it actually works while your request is non-dispatched but then fail as soon as your request is forwarded to another resource.
Maybe try to explicitly indicate that you do not want to apply the authorization rules to all dispatcher types, either by using shouldFilterAllDispatcherTypes or dispatcherTypeMatchers.
For example to grant all access on requests with dispatcher type ASYNC or FORWARD :
http
...
.authorizeHttpRequests()
.dispatcherTypeMatchers(DispatcherType.ASYNC, DispatcherType.FORWARD).permitAll()
...
Of course you can also customize it to require a specific role (hasRole) instead of permitAll if needed.
See the documentation for more information.
i have so much trouble to identify why Spring security is not working on my project.
There is my code :
import com.corp.myproject.config.security.filters.JwtAuthorizationFilter;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
#Configuration
#EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(new JwtAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class);
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.formLogin().loginPage("http://localhost:8081/login/openidProvider");
http.authorizeRequests().anyRequest().authenticated();
http.csrf().disable();
}
}
But I can access to my backend without any authentication.
The expected result is :
If a user don't have a authorization jwt, Spring security redirects him to the custom login url to login in with Openid
When the user logs in, he is redirected to my backend with a jwt
Every requests to my backend will be filtred by my JwtAuthorizationFilter
EDIT : Adding the JwtAuthorizationFilter's code :
import com.corp.myproject.config.security.JwtTokenUtil;
import com.corp.myproject.config.security.UserFromToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
public class JwtAuthorizationFilter extends OncePerRequestFilter {
JwtTokenUtil jwtTokenUtil;
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String authorizationToken = request.getHeader("Authorization");
if (authorizationToken != null && authorizationToken.startsWith("Bearer ")) {
String jwt = authorizationToken.substring(7); // Ignore 'Bearer '
String error = "";
if (!jwtTokenUtil.isTokenIntegritySafe(jwt)) {
error = "Token jwt is not valid";
}
if (!jwtTokenUtil.validateToken(jwt)) {
error = "Token jwt is expired";
}
UserFromToken userFromToken = jwtTokenUtil.getUserFromToken(jwt);
Collection<GrantedAuthority> authorities = new ArrayList<>();
for (String r : userFromToken.getRoles()) {
authorities.add(new SimpleGrantedAuthority(r));
}
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userFromToken.getName(), null, authorities);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
if (error == "") {
filterChain.doFilter(request, response);
} else {
response.setHeader("error-message", error);
response.sendError(HttpServletResponse.SC_FORBIDDEN);
}
} else {
response.setHeader("error-message", "User not authorized");
response.sendError(HttpServletResponse.SC_FORBIDDEN);
}
}
}
I have edited the question to make more sense. The original was:
How can I fix Angular and Spring Boot configuration with Spring JDBC authentication so that I am able to logout even with CSRF protection enabled?
With CSRF disabled for /logout:
I am able to login (receiving CSRF and JSESSIONID cookies) and logout (200 OK is received) using Postman.
I am able to login (receiving CSRF and JSESSIONID cookies) and logout (200 OK is received) using Firefox and the Angular frontend.
With CSRF enabled for /logout:
I am able to login (receiving CSRF and JSESSIONID cookies) and logout (200 OK is received) using Postman.
I am able to login using Firefox and the Angular frontend.
When trying to log out, however...
There is first a preflight request that succeeds:
Then, I see a request to /logout:
I debugged the backend and it seems Spring is unable to find a matching CSRF token in its TokenRepository. So I end up with a MissingCsrfTokenException and a 403 Forbidden. How can I fix that?
Backend:
SecurityConfiguration:
package org.adventure.configuration;
import org.adventure.security.RESTAuthenticationEntryPoint;
import org.adventure.security.RESTAuthenticationFailureHandler;
import org.adventure.security.RESTAuthenticationSuccessHandler;
import org.adventure.security.RESTLogoutSuccessHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configurers.provisioning.JdbcUserDetailsManagerConfigurer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import javax.sql.DataSource;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
#Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private RESTAuthenticationEntryPoint authenticationEntryPoint;
#Autowired
private RESTAuthenticationSuccessHandler authenticationSuccessHandler;
#Autowired
private RESTAuthenticationFailureHandler authenticationFailureHandler;
#Autowired
private RESTLogoutSuccessHandler restLogoutSuccessHandler;
#Autowired
private DataSource dataSource;
#Autowired
private AccountsProperties accountsProperties;
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeRequests().antMatchers("/h2-console/**")
.permitAll();
httpSecurity.authorizeRequests().antMatchers("/secure/**").authenticated();
httpSecurity.cors().configurationSource(corsConfigurationSource());
httpSecurity.csrf()
.ignoringAntMatchers("/h2-console/**")
.ignoringAntMatchers("/login")
//.ignoringAntMatchers("/logout")
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
httpSecurity.headers()
.frameOptions()
.sameOrigin();
httpSecurity.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
httpSecurity.formLogin().successHandler(authenticationSuccessHandler);
httpSecurity.formLogin().failureHandler(authenticationFailureHandler);
httpSecurity.logout().logoutSuccessHandler(restLogoutSuccessHandler);
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
JdbcUserDetailsManagerConfigurer jdbcUserDetailsManagerConfigurer = auth.jdbcAuthentication()
.dataSource(dataSource)
.withDefaultSchema();
if (Objects.nonNull(accountsProperties)) {
FirstUser firstUser = accountsProperties.getFirstUser();
if (Objects.nonNull(firstUser)) {
String name = firstUser.getName();
String password = firstUser.getPassword();
if (Objects.nonNull(name) && Objects.nonNull(password) &&
!("".equals(name) || "".equals(password))) {
jdbcUserDetailsManagerConfigurer.withUser(User.withUsername(name)
.password(passwordEncoder().encode(password))
.roles("USER"));
}
}
FirstAdmin firstAdmin = accountsProperties.getFirstAdmin();
if (Objects.nonNull(firstAdmin)) {
String name = firstAdmin.getName();
String password = firstAdmin.getPassword();
if (Objects.nonNull(name) && Objects.nonNull(password) &&
!("".equals(name) || "".equals(password))) {
jdbcUserDetailsManagerConfigurer.withUser(User.withUsername(name)
.password(passwordEncoder().encode(password))
.roles("ADMIN"));
}
}
}
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // Strength increased as per OWASP Password Storage Cheat Sheet
}
#Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://localhost:4200"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST"));
configuration.setAllowedHeaders(List.of("X-XSRF-TOKEN", "Content-Type"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
source.registerCorsConfiguration("/secure/classifieds", configuration);
source.registerCorsConfiguration("/login", configuration);
source.registerCorsConfiguration("/logout", configuration);
return source;
}
}
RESTAuthenticationEntryPoint:
package org.adventure.security;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
#Component
public class RESTAuthenticationEntryPoint implements AuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
}
RESTAuthenticationFailureHandler
package org.adventure.security;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
#Component
public class RESTAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
#Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
super.onAuthenticationFailure(request, response, exception);
}
}
RESTAuthenticationSuccessHandler
package org.adventure.security;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
#Component
public class RESTAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) {
clearAuthenticationAttributes(request);
}
}
RESTLogoutSuccessHandler
package org.adventure.security;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
#Component
public class RESTLogoutSuccessHandler implements LogoutSuccessHandler {
#Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) {
response.setStatus(HttpServletResponse.SC_OK);
}
}
Frontend:
To /login and /logout I make POST requests with withCredentials: true, and have a HttpInterceptor configured:
import { Injectable } from '#angular/core';
import { HttpInterceptor, HttpXsrfTokenExtractor, HttpRequest, HttpHandler, HttpEvent } from '#angular/common/http';
import { Observable } from 'rxjs';
#Injectable()
export class XsrfInterceptor implements HttpInterceptor {
constructor(private tokenExtractor: HttpXsrfTokenExtractor) {
}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
let requestToForward = req;
let token = this.tokenExtractor.getToken() as string;
if (token !== null) {
requestToForward = req.clone({ setHeaders: { "X-XSRF-TOKEN": token } });
}
return next.handle(requestToForward);
}
}
LogoutService:
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
#Injectable({
providedIn: 'root'
})
export class LogoutService {
private url = 'http://localhost:8080/logout';
constructor(private http: HttpClient) { }
public logout(): Observable<any> {
return this.http.post(
this.url, { withCredentials: true }).pipe(
map(response => {
console.log(response)
})
);
}
}
In HttpClient, the POST method has a little bit different signature than a GET: https://github.com/angular/angular/blob/master/packages/http/src/http.ts
The second parameter is any request body we want to send, not the options, which are the third parameter. So withCredentials: true was simply never set correctly on the actual request.
Changing the call to:
this.http.post(
this.url, null, { withCredentials: true })
fixed the problem.
I'm trying to work out a simple web app with angular as the front end and Spring Boot as the back end. My pre-flight request succeeds and I receive a valid token, however when trying to make a subsequent request to a route I get a 404. When I try to curl the url I get a 500 with the message "Missing or invalid authorization header" which seems to me to be saying that the route does indeed exist and is listening, but something else is wrong.
First of all the Typescript. Here is my login.component.ts
import { Component } from "#angular/core";
import { Observable } from "rxjs/index";
import { LoginService } from "../services/login.service";
#Component({
selector: "login",
templateUrl: "./login.component.html"
})
export class Login {
private model = {"username": "", "password": ""};
private currentUserName;
constructor (private loginService: LoginService) {
this.currentUserName = localStorage.getItem("currentUserName");
}
public onSubmit() {
console.log("submitting");
this.loginService.sendCredential(this.model).subscribe(
data => {
console.log(data);
localStorage.setItem("token", data);
console.log("Setting token");
this.loginService.sendToken(localStorage.getItem("token")).subscribe(
data => {
this.currentUserName = this.model.username;
localStorage.setItem("currentUserName", this.model.username);
this.model.username = "";
this.model.password = "";
},
error => console.log(error)
);
},
error => {
console.log("oh no");
console.log(error)
}
);
}
}
And then my LoginService
import { Injectable } from "#angular/core";
import { HttpClient } from '#angular/common/http';
import { HttpHeaders } from '#angular/common/http';
import { Observable } from "rxjs/index";
#Injectable()
export class LoginService {
token: string;
constructor (private http: HttpClient) {}
public sendCredential(model): Observable<String> {
const tokenUrlPreFlight = "http://localhost:8080/users/login/";
const httpOptions: {} = {
headers: new HttpHeaders({
'ContentType': 'application/json'
})
};
return this.http.post<String>(tokenUrlPreFlight, model, httpOptions);
}
public sendToken(token) {
const tokenUrlPostFlight = "http://localhost:8080/rest/users/";
console.log("Bearer " + token);
const httpOptions: {} = {
headers: new HttpHeaders({
'Authorization': 'Bearer ' + token
})
};
return this.http.get(tokenUrlPostFlight, httpOptions);
}
public logout(): void {
localStorage.setItem("token", "");
localStorage.setItem("currentUserName", "");
alert("You just logged out");
}
public checkLogin(): boolean {
if(localStorage.getItem("currentUserName") != null && localStorage.getItem("currentUserName") != "" && localStorage.getItem("token") != null && localStorage.getItem("token") != "") {
console.log(localStorage.getItem("currentUserName"));
console.log(localStorage.getItem("token"));
return true;
}
return false;
}
}
And now for the java. First here is my entry point:
import com.acb.app.configuration.JwtFilter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
#SpringBootApplication
public class App {
#Bean
public FilterRegistrationBean jwtFilter() {
final FilterRegistrationBean<JwtFilter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new JwtFilter());
filterRegistrationBean.addUrlPatterns("/rest/*");
return filterRegistrationBean;
}
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
My UserController.java
import com.acb.app.model.User;
import io.jsonwebtoken.*;
import com.acb.maki.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.ServletException;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Optional;
#RestController
#RequestMapping("/users")
public class UserController {
#Autowired
private UserService userService;
#GetMapping("")
public List<User> userIndex() {
return userService.getAllUsers();
}
#GetMapping("{username}")
public Optional<User> getUserByUsername(#RequestBody String username) {
return userService.findByUsername(username);
}
#PostMapping("login")
public String login(#RequestBody Map<String, String> json) throws ServletException {
if(json.get("username") == null || json.get("password") == null) {
throw new ServletException("Please fill in username and password");
}
final String username = json.get("username");
final String password = json.get("password");
Optional<User> optionalUser = userService.findByUsername(username);
if(!optionalUser.isPresent()) {
throw new ServletException("Username not found");
}
User user = optionalUser.get();
if(!password.equals(user.getPassword())) {
throw new ServletException("Invalid login. Please check username and password");
}
final String response = Jwts.builder().setSubject(username).claim("roles", "user").setIssuedAt(new Date()).signWith(SignatureAlgorithm.HS256, "secretKey").compact();
final String jsonResponse = String.format("\"%s\"", response);
System.out.println(jsonResponse);
return jsonResponse;
}
#PostMapping(value="/register")
public User register(#RequestBody User user) {
return userService.save(user);
}
}
And last but not least my JwtFilter.java
import io.jsonwebtoken.*;
import org.springframework.web.filter.GenericFilterBean;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class JwtFilter extends GenericFilterBean {
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
final HttpServletRequest request = (HttpServletRequest) servletRequest;
final HttpServletResponse response = (HttpServletResponse) servletResponse;
final String authHeader = request.getHeader("Authorization");
if ("OPTIONS".equals(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
filterChain.doFilter(servletRequest, servletResponse);
} else {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new ServletException("Missing or invalid authorization header");
}
final String token = authHeader.substring(7);
try {
final JwtParser jwtParser = Jwts.parser();
final Jws<Claims> claimsJws = jwtParser.setSigningKey("secretKey").parseClaimsJws(token);
final Claims claims = claimsJws.getBody();
request.setAttribute("claims", claims);
} catch (final SignatureException e) {
throw new ServletException("Invalid token.");
}
}
filterChain.doFilter(servletRequest, servletResponse);
}
}
I am very aware of the bad practices here with comparing plain text passwords, etc. I'm just trying to get this to work for the time being. The token is correctly generated and returned. The token is also set correctly to local storage, but upon making the get request to the /rest/users route a 404 is returned. There are no errors on the java side.
So as I expected, I am indeed an idiot. As the user mentioned above my service maps to /users/ and what I needed is the protected version /rest/users. So next to my UserController I created UserResource.java which looks like so:
import com.acb.app.model.User;
import com.acb.app.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
#RestController
#RequestMapping("/rest")
public class UserResource {
#Autowired
private UserService userService;
#RequestMapping("/users")
public List<User> findAllUsers() {
return userService.getAllUsers();
}
}
And this makes use of the JwtFilter and allows me to protect my routes. Hopefully this will be helpful to someone else.
When I launch my application or access using the base URL http://localhost:8180/MyProject/ it is by passing the login page and showing up the actual home page and I see the user as Anonymous though it should first load the login page and then after validating it should display the home page. Suggestions will be appreciated!
****SecurityConfig****
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider;
#Configuration
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
protected void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(activeDirectoryLdapAuthenticationProvider());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/", "/home").permitAll()
.antMatchers("/admin/**").access("hasRole('ADMIN')")
.and().formLogin().loginPage("/login")
.usernameParameter("ssoId").passwordParameter("password")
.and().csrf()
.and().exceptionHandling().accessDeniedPage("/Access_Denied");
}
#Bean
public AuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
ActiveDirectoryLdapAuthenticationProvider authenticationProvider =
new ActiveDirectoryLdapAuthenticationProvider("", "");
authenticationProvider.setConvertSubErrorCodesToExceptions(true);
authenticationProvider.setUseAuthenticationRequestCredentials(true);
return authenticationProvider;
}
}
MyController
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
#Controller
public class MyController{
#RequestMapping(value="/processAccount", method = RequestMethod.POST)
public #ResponseBody String processRequestAccount(ModelMap model,#RequestParam("accountNumber") String accountNumber,#RequestParam("companyNumber") String companyNumber) {
//String accountNumber = request.getParameter("accountNumber");
String responseMessage = null;
//String companyNumber = request.getParameter("companyNumber");
System.out.println(companyNumber);
MyServiceImpl accService = new MyServiceImpl();
boolean responseFlag = accService.verifyAndProcessAccount(accountNumber, companyNumber);
System.out.println(responseFlag);
if(responseFlag)
{
//model.addAttribute("message", "The account number"+" "+accountNumber+" "+"is processed successfully!");
responseMessage = "The account number"+" "+accountNumber+" "+"is processed successfully!";
}
else
{
responseMessage = "Account number"+" "+accountNumber+" "+"cannot not be found, email has sent to the support team";
}
return responseMessage;
}
#RequestMapping(value = { "/", "/home" }, method = RequestMethod.GET)
public String homePage(ModelMap model) {
model.addAttribute("user", getPrincipal());
model.addAttribute("companyNumber","1");
return "accountsearch";
}
#RequestMapping(value = "/admin", method = RequestMethod.GET)
public String adminPage(ModelMap model) {
model.addAttribute("user", getPrincipal());
return "admin";
}
#RequestMapping(value = "/db", method = RequestMethod.GET)
public String dbaPage(ModelMap model) {
model.addAttribute("user", getPrincipal());
return "dba";
}
#RequestMapping(value = "/Access_Denied", method = RequestMethod.GET)
public String accessDeniedPage(ModelMap model) {
model.addAttribute("user", getPrincipal());
return "accessDenied";
}
#RequestMapping(value = "/login", method = RequestMethod.GET)
public String loginPage() {
return "login";
}
#RequestMapping(value="/logout", method = RequestMethod.GET)
public String logoutPage (HttpServletRequest request, HttpServletResponse response) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null){
new SecurityContextLogoutHandler().logout(request, response, auth);
}
return "redirect:/login?logout";
}
private String getPrincipal(){
String userName = null;
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
userName = ((UserDetails)principal).getUsername();
} else {
userName = principal.toString();
}
return userName;
}
}
you need to change Spring permission expression .permitAll() to .isAuthenticated() if you want authentication first.
try this
http.authorizeRequests()
.antMatchers("/login").isAnonymous() // anonymous user access this page
.antMatchers("/", "/home").isAuthenticated() // now home will be check authentication first
.antMatchers("/admin/**").access("hasRole('ADMIN')")
.and().formLogin().loginPage("/login")
.usernameParameter("ssoId").passwordParameter("password")
.and().csrf()
.and().exceptionHandling().accessDeniedPage("/Access_Denied");