Spring security - Custom ExceptionTranslationFilter - java

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.

Related

Spring Boot - Spring Security - InvalidBearerTokenException Error Handling

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.

How to update authentication in auditorAware

#Configuration
#EnableJpaAuditing(auditorAwareRef = "auditorProvider")
public class JpaAuditingConfiguration {
#Bean
#Scope(value= ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public AuditorAware<String> auditorProvider() {
Authentication authentication
= SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
return () -> Optional.<String>empty();
}
return () -> Optional.of(authentication.getName());
}
public static void main(String[] args) {
SpringApplication.run(JpaAuditingConfiguration.class, args);
}
}
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private UserDetailServiceImpl userDetailsService;
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth)
throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(new BCryptPasswordEncoder());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().cors().and()
.logout()
.invalidateHttpSession(true)
.clearAuthentication(true)
.logoutSuccessUrl("/logout/success")
.and().authorizeRequests()
.antMatchers(HttpMethod.POST, "/login").permitAll()
.antMatchers(HttpMethod.GET, "/logout/**").permitAll()
.anyRequest().authenticated().and()
// Filter for the api/login requests
.addFilterBefore(new LoginFilter("/login",
authenticationManager()),
UsernamePasswordAuthenticationFilter.class)
// Filter for other requests to check JWT in header
.addFilterBefore(new AuthenticationFilter(),
UsernamePasswordAuthenticationFilter.class);
}
public class AuthenticationFilter extends GenericFilterBean {
#Override
public void doFilter(ServletRequest request,
ServletResponse response,
FilterChain filterChain)
throws IOException, ServletException {
Authentication authentication
= AuthenticationService
.getAuthentication((HttpServletRequest)request);
SecurityContext securityContext
= SecurityContextHolder.getContext();
securityContext.setAuthentication(authentication);
filterChain.doFilter(request, response);
}
}
auditorProvider seems to be called only once when application starts. authentication is called before it is set in doFilter. authentication object is always null and doesn't update when it's set later in doFilter. When I update a row in db, createdBy and lastModifiedBy are always null.
My doFilter seems to be called when an HTTP request is processed.
I followed a tutorial to learn spring security, but after troubleshooting for a few hours and followed through many similar tutorial, still can't figure out how to properly set the authentication in sequence so when I update a row, spring will update lastModifiedBy and createdBy automatically.
Your issue is that you are creating an anonymous AuditorAware but you are evaluating the SecurityContextHolder.getContext().getAuthentication() outside the body so whatever at that creation time will be kept inside the body.
#Bean
#Scope(value= ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public AuditorAware<String> auditorProvider() {
return () -> {
Authentication authentication
= SecurityContextHolder.getContext().getAuthentication();
return Optional.ofNullable(authentication)
.map(Authentication::getName);
};
}
You could say, you have SCOPE_PROTOTYPE but it is useless if it was autowired into a singleton instance that framework uses.

How can I avoid the duplicate code of mapping in Spring project?

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, "/*");

Angular 6 Basic Auth returns 401 from client

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

Redirect loop when x509 authentication fails

I have spring boot application with x509 authentication. My problem is that when authentication fails i get redirect loop instead of error screen.
When authentication fails i throw UsernameNotFoundException loadUserDetails from method in ArhivX509UserDetailsService.java
My code is as follows:
SecurityConfiguration.java
#Configuration
#Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
#EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
#Qualifier("x509UserDetailsService")
private AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> iX509UserDetailsService;
#Override
protected void configure(HttpSecurity pHttp) throws Exception {
//#formatter:off
pHttp
.authorizeRequests()
.antMatchers("/webjars/**").permitAll()
.antMatchers("/error").permitAll()
.antMatchers("/error401").permitAll()
.anyRequest().authenticated()
.and()
.x509()
.subjectPrincipalRegex("(.*)")
.authenticationUserDetailsService(iX509UserDetailsService)
.and()
.addFilterAfter(new X509ErrorCheckerFilter(), X509AuthenticationFilter.class)
.addFilterBefore(new LoggerMDCFilter(), X509ErrorCheckerFilter.class)
.exceptionHandling()
.authenticationEntryPoint(unauthorizedEntryPoint())
.accessDeniedPage(AppUrls.ERROR_401)
.and()
.requiresChannel()
.anyRequest()
.requiresSecure()
.and()
.sessionManagement()
.maximumSessions(1)
.and()
.and()
.logout()
.invalidateHttpSession(true)
.deleteCookies("SESSION", "JSESSIONID")
.logoutSuccessUrl("http://www.google.com")
.permitAll();
//#formatter:on
}
#Bean
public AuthenticationEntryPoint unauthorizedEntryPoint() {
return new AuthenticationEntryPoint() {
#Override
public void commence(HttpServletRequest pRequest, HttpServletResponse pResponse,
AuthenticationException pAuthException) throws IOException, ServletException {
pResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
};
}
}
ArhivX509UserDetailsService.java
#Service("x509UserDetailsService")
public class ArhivX509UserDetailsService
implements AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> {
#Autowired
private IUserProfileService iUserProfileService;
#Autowired
private ICheckCertificateService iCheckCertService;
#Override
public UserDetails loadUserDetails(PreAuthenticatedAuthenticationToken pToken) throws UsernameNotFoundException {
X509Certificate tCertificate = (X509Certificate) pToken.getCredentials();
String tSubjectDN = tCertificate.getSubjectDN().toString().toUpperCase();
ProfilKorisnika tProfilKorisnika = iUserProfileService.getUserProfile(tSubjectDN);
if (tProfilKorisnika == null) {
throw new UsernameNotFoundException("Pogreška kod prijave korisnika.");
}
return tProfilKorisnika;
}
}
X509ErrorCheckerFilter.java
public class X509ErrorCheckerFilter extends GenericFilterBean {
private static final String DEFAULT_REDIRECT_URL = "/error401";
private static final String[] UNAUTHENTICATED_URLS = { "/webjars/**", "/error", "/error401", "/login",
"/logout" };
#Override
public void doFilter(ServletRequest pRequest, ServletResponse pResponse, FilterChain pChain)
throws IOException, ServletException {
HttpServletRequest tHttpRequest = (HttpServletRequest) pRequest;
HttpServletResponse tHttpResponse = (HttpServletResponse) pResponse;
String tContextRoot = tHttpRequest.getSession().getServletContext().getContextPath();
String tUri = tHttpRequest.getRequestURI().replaceFirst(tContextRoot, "");
if (isUriSecured(tUri)) {
Authentication tAuthentication = SecurityContextHolder.getContext().getAuthentication();
AuthenticationException tException = (AuthenticationException) tHttpRequest
.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
if (tException != null || tAuthentication == null) {
RequestDispatcher tRd = tHttpRequest.getRequestDispatcher(DEFAULT_REDIRECT_URL);
tRd.forward(tHttpRequest, tHttpResponse);
return;
}
}
pChain.doFilter(pRequest, pResponse);
}
private boolean isUriSecured(String pRequestURI) {
boolean tResult = true;
for (String tUrl : UNAUTHENTICATED_URLS) {
if (pRequestURI.startsWith(tUrl)) {
tResult = false;
break;
}
}
return tResult;
}
}
If you need more details please ask.
Applying permitAll() to /error401 means that when the security filterchain is finished executing, the request will be processed normally, regardless of whether or not there is an Authentication in the current SecurityContext.
X509ErrorCheckerFilter is forwarding all unauthenticated requests to /error401 from within the filterchain, so permitAll() is never applied. Instead, the forwarded request goes through the filterchain again and fails authentication, causing the circular redirect.
To resolve this, you have several options. Here are a couple:
1. Disable Security for error endpoints
you can disable security for endpoints using web.ignoring() in your SecurityConfiguration class. This is the simplest option.
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/error*");
}
2. Implement the ErrorController interface
The security filterchain will not be called for request mappings included in a controller that implements ErrorController.
See Spring's own BasicErrorController for reference(source).
This second option is preferable as it removes any requirement for a redirect in your filter. Instead, it lets Spring Security do the heavy lifting for routing requests through the auth process. As long as there's no authenticated Authentication in the session's SecurityContext when the security filterchain has finished processing, spring security will return a 401 and return the error page specified by your ErrorController.

Categories

Resources