I'm trying to create spring secured application
I implemented SecurityConfig like this
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.and()
.csrf()
.disable()
.formLogin().disable()
.httpBasic().disable()
.exceptionHandling()
.authenticationEntryPoint(unauthorizedHandler)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.anyRequest()
.authenticated();
// Add our custom JWT security filter
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
My JwtAuthenticationEntryPoint to handle exception's :
#Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationEntryPoint.class);
#Override
public void commence(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
AuthenticationException e) throws IOException, ServletException {
logger.error("Responding with unauthorized error. Message - {}", e.getMessage());
httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.getMessage());
}
}
Also I implemented UserDetail Service
#Service
public class CustomUserDetailsService implements UserDetailsService {
#Autowired
UserRepository userRepository;
#Override
#Transactional
public UserDetails loadUserByUsername(String usernameOrEmail)
throws UsernameNotFoundException {
User user = userRepository.findByUsernameOrEmail(usernameOrEmail, usernameOrEmail)
.orElseThrow(() ->
new UsernameNotFoundException
("User not found with username or email : " + usernameOrEmail)
);
return UserPrincipal.create(user);
}}
It was working fine before I added ControllerAdvice
#ControllerAdvice
public class ExceptionControllerAdvice {
/**** 500 Entity */
#ExceptionHandler(value = Exception.class)
#ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public final ErrorDetail handleAllExceptions(Exception ex) {
LOGGER.error("An unexpected error occurred", ex);
ErrorDetail problem = new ErrorDetail("Internal Error",
"An unexpected error has occurred");
problem.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
return problem;
}
}
Now spring is trying to handle Exception in controller advice. As a consequence, I can not handle the error correctly. if I comment out Controller advice logic then AuthenticationEntryPoint is processed authentication error.
How can make AuthenticationEntryPoint and #ControllerAdvice work together?
Related
I have implemented the AccessDeniedHandler interface and added the interface to the WebSecurityConfigurerAdapter, and I also have an ExceptionHandler of RuntimeException in my ControllerAdvice.
When I receive accessDeniedException from MethodSecurity, ExceptionHandler calls RuntimeException before running AccessDeniedHandler.
How to call AccessDeniedHandler without deleting ExceptionHandler?
this is my WebSecurityConfig:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**";
public static final String JWT_TOKEN_HEADER_PARAM = "X-Authorization";
#Value("${payment.gateway.callback-path}")
private String paymentCallbackPath;
#Autowired
private ThingspodAccessDeniedHandler accessDeniedHandler;
#Autowired
private RestAuthenticationFailureHandler authenticationFailureHandler;
#Autowired
private JwtAuthenticationProvider jwtAuthenticationProvider;
protected JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
List<String> pathsToSkip = new ArrayList<>(Collections.singletonList(paymentCallbackPath));
SkipPathRequestMatcher matcher = new SkipPathRequestMatcher(pathsToSkip, TOKEN_BASED_AUTH_ENTRY_POINT);
JwtAuthenticationFilter filter = new JwtAuthenticationFilter(matcher, JWT_TOKEN_HEADER_PARAM, authenticationFailureHandler);
filter.setAuthenticationManager(authenticationManager());
return filter;
}
#Override
protected void configure(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(jwtAuthenticationProvider);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.headers().cacheControl().and().frameOptions().disable()
.and()
.cors()
.and()
.csrf().disable()
.exceptionHandling()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers(paymentCallbackPath).permitAll()
.and()
.authorizeRequests()
.antMatchers(TOKEN_BASED_AUTH_ENTRY_POINT).authenticated()
.and()
.authorizeRequests()
.and()
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.exceptionHandling().accessDeniedHandler(accessDeniedHandler);
}
}
and this is my AccessDeniedHander and ExceptionHandler in ControllerAdvice:
#Component
public class ThingspodAccessDeniedHandler implements AccessDeniedHandler {
private final ObjectMapper mapper;
public ThingspodAccessDeniedHandler(ObjectMapper mapper) {
this.mapper = mapper;
}
#Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
Object res = buildResponse(accessDeniedException, "You don't have permission to perform this operation!", HttpStatus.FORBIDDEN, ThingspodErrorCode.PERMISSION_DENIED, request);
response.setStatus(HttpStatus.FORBIDDEN.value());
mapper.writeValue(response.getWriter(), res);
}
}
#ExceptionHandler(RuntimeException.class)
public ResponseEntity<Object> handleAllUncaughtRuntimeException(
RuntimeException ex, WebRequest request){
return buildApiErrorResponse(ex, HttpStatus.INTERNAL_SERVER_ERROR, ThingspodErrorCode.GENERAL, request);
}
This is an unfortunate limitation of the way Spring Security handles its own exceptions. You can read more about it at the bug report (which was rejected) at Spring Security: https://github.com/spring-projects/spring-security/issues/6908#issuecomment-533269673
The only thing to do in this case is to spot Spring Security related exceptions in your ExceptionHandler and rethrow them.
When selecting a error handler, spring will look for the most specific one.
Since you have exception handler for the more general exception (RuntimeException) and another one for AccessDeniedException.
In that case first exception handler (for handling RuntimeException) won't be called when AccessDeniedException occurs.
So you could keep exception handler for handling RuntimeExceptions, but you will need to add exception handler for handling AccessDeniedException and move logic from ThingspodAccessDeniedHandler into that handler method, like this:
#ExceptionHandler(UnknownHostException.class)
public void handleUnknownAccessDeniedException(UnknownHostException e, WebRequest request) {
Object res = buildResponse(accessDeniedException, "You don't have permission to perform this operation!", HttpStatus.FORBIDDEN, ThingspodErrorCode.PERMISSION_DENIED, request);
response.setStatus(HttpStatus.FORBIDDEN.value());
mapper.writeValue(response.getWriter(), res);
}
#ExceptionHandler(RuntimeException.class)
public ResponseEntity<Object> handleAllUncaughtRuntimeException(
RuntimeException ex, WebRequest request){
return buildApiErrorResponse(ex, HttpStatus.INTERNAL_SERVER_ERROR, ThingspodErrorCode.GENERAL, request);
}
Similar to this question but it got no answers: Spring Security: Handle InvalidBearerTokenException in #ExceptionHandler
I have similar code and I'm trying to catch org.springframework.security.oauth2.server.resource.InvalidBearerTokenException when a user has supplied invalid/expired/bad JWT format.
#Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
#Autowired
#Qualifier("handlerExceptionResolver")
private HandlerExceptionResolver resolver;
#Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException e) throws IOException, ServletException {
resolver.resolveException(request, response, null, e);
}
}
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
#Autowired
private CustomAuthenticationEntryPoint authenticationEntryPoint;
#Autowired
private CustomAccessDeniedHandler accessDeniedHandler;
#Override
protected void configure(HttpSecurity http) throws Exception
{
// other config here
http.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.oauth2ResourceServer().jwt();
http.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler);
}
}
I've also implemented the #ExceptionHandler of AuthenticationException for custom response.
#ExceptionHandler({AuthenticationException.class})
protected ResponseEntity<Object> handleAuthException(AuthenticationException ex, WebRequest req)
{
CustomResponse response = ...
return new ResponseEntity<>(response, ...);
}
InvalidBearerTokenException is a subclass of AuthenticationException.
Any idea why this AuthenticationEntryPoint code is not catching it? I've also tried adding logging inside the commence method but it's not being called when InvalidBearerTokenException is thrown, but other AuthenticationException does.
You have to specify this AuthenticationEntryPoint inside the OAuth2ResourceServerConfigurer, like so:
#Override
protected void configure(HttpSecurity http) throws Exception
{
// other config here
http.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.oauth2ResourceServer().jwt().and()
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler);
}
When you set it, the Configurer will change the AuthenticationEntryPoint that is used inside the BearerTokenAuthenticationFilter, see here.
I am getting started with Spring Web Security for my application and I am trying to implement stateless JWT authentication. Curretly, the configure method in the Web Security config is the following
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.csrf().disable()
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/login", "/register", "/authenticate/{uuid}").permitAll()
.anyRequest().authenticated()
;
httpSecurity.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}
My jwtAuthenticationEntryPoint is the following:
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
log.warn("Responding with unauthorized error. Message - {}", authException.getMessage());
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Sorry, You're not authorized to access this resource.");
}
}
The authentication works correctly besides for the "/authenticate/{uuid}" endpoint. The request is allowed (Status 200 and correct return of the function) but I keep getting the warning from the jwtAuthenticationEntryPoint class ("Responding with unauthorized error") in the console.
Why is the EntryPoint getting triggered for that specific request and how can I resolve it?
EDIT:
AuthenticationController:
#RestController
#CrossOrigin
public class AuthenticationController {
#RequestMapping(value = "/authenticate/{uuid}", method = RequestMethod.GET)
public ResponseEntity<?> authenticate(#PathVariable String uuid){
return ResponseEntity.ok(uuid);
}
}
Pls use web.ignoring() to try as the below:
public void configure(WebSecurity web) throws Exception {
String [] notauthlist = new String[]{"/login", "/register","/authenticate/**"};
web.ignoring().antMatchers(notauthlist);
}
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.csrf().disable()
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests().anyRequest().authenticated()
;
httpSecurity.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}
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();
}
This question is actually related to this issue problem.
Based on the suggestion from #harsh-poddar, I added the filter accordingly.
However, after adding that it seems like I can't login even with valid credential.
Following is the related code:
SecurityConfig
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// #Bean
// public CustomAuthenticationEntryPoint customAuthenticationEntryPoint() {
// return new CustomAuthenticationEntryPoint();
// }
#Bean
public CustomExceptionTranslationFilter customExceptionTranslationFilter() {
return new CustomExceptionTranslationFilter(new CustomAuthenticationEntryPoint());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
//Note : Able to login without this filter, but after adding this, valid credential also fails
.addFilterAfter(customExceptionTranslationFilter(), ExceptionTranslationFilter.class)
// .exceptionHandling()
// .authenticationEntryPoint(new customAuthenticationEntryPoint())
// .and()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.requestCache()
.requestCache(new NullRequestCache())
.and()
.httpBasic()
.and()
.csrf().disable();
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(new CustomAuthenticationProvider());
}
}
CustomAuthenticationProvider
#Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
public CustomAuthenticationProvider() {
super();
}
#Override
public Authentication authenticate(final Authentication authentication) throws AuthenticationException {
final String name = authentication.getName();
final String password = authentication.getCredentials().toString();
if (name.equals("admin") && password.equals("password")) {
final List<GrantedAuthority> grantedAuths = new ArrayList<>();
grantedAuths.add(new SimpleGrantedAuthority("ROLE_USER"));
final UserDetails principal = new User(name, password, grantedAuths);
final Authentication auth = new UsernamePasswordAuthenticationToken(principal, password, grantedAuths);
return auth;
} else {
throw new BadCredentialsException("NOT_AUTHORIZED");
}
}
#Override
public boolean supports(final Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
CustomExceptionTranslationFilter
#Component
public class CustomExceptionTranslationFilter extends ExceptionTranslationFilter {
public CustomExceptionTranslationFilter(AuthenticationEntryPoint authenticationEntryPoint) {
super(authenticationEntryPoint);
}
}
CustomAuthenticationEntryPoint
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized.");
}
}
p/s : sorry for the basic question, I'm really new in spring & spring security.
The intended design for AuthenticationEntryPoint is to start/initiate an authentication. However, your implementation CustomAuthenticationEntryPoint does not do this. Instead, it simply sends back an unauthorized response. Please see javadoc for AuthenticationEntryPoint for more details on implementation specifics.
Based on your configuration you are using HTTP Basic for authentication:
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.httpBasic();
}
This specific configuration will automatically configure BasicAuthenticationEntryPoint which is an implementation of AuthenticationEntryPoint. The BasicAuthenticationEntryPoint will challenge the user with a http response header of WWW-Authenticate: Basic realm="User Realm" to authenticate, as per server protocol.
However, the fact that you are configuring your own CustomAuthenticationEntryPoint it will ultimately override the BasicAuthenticationEntryPoint which is not what you want to do.
The other post recommended this configuration which again is not what you want to do.
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.httpBasic()
.and()
.exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint());
}
If your main goal is to provide a custom response to the user when authentication fails than I would propose a form login configuration with a configured AuthenticationFailureHandler. Here is the configuration:
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin().failureHandler(new DefaultAuthenticationFailureHandler())
.and()
.csrf().disable(); // NOTE: I would recommend enabling CSRF
Your implementation of DefaultAuthenticationFailureHandler would be:
public class DefaultAuthenticationFailureHandler implements AuthenticationFailureHandler {
#Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
// Set status only OR do whatever you want to the response
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
}
}
The AuthenticationFailureHandler is specifically designed to handle a failed authentication attempt.