Spring Security - How to handle a RuntimeException in a custom AuthenticationFilter? - spring

Using Spring Security, I have created a custom UsernamePasswordAuthenticationFilter. In this filter's attemptAuthentication method, I would like to retrieve the body of the HttpServletRequest, since credentials should be passed inside the body instead of request parameters.
I think I have found a good way to achieve this, but I am unsure about how to handle the IOException that could now occur inside this method. I have to catch the IOException inside this method, since the original method, which I override, does not throw an IOException.
This is my implementation:
#RequiredArgsConstructor
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
try {
UserDTO user = new ObjectMapper().readValue(request.getInputStream(), UserDTO.class);
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());
return authenticationManager.authenticate(authenticationToken);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
My IDE suggests to throw a custom exception instead of a RuntimeException. But since this filter is part of the Spring Security filter chain, I am unsure about what should happen in case of an IOException.

Related

How to track all login logs about the jwt in spring security

Recently, I started to make a platform and chose spring security as the back end and angular as the front end. And I want to track all login logs, such as failed login, successful login, username does not exist, incorrect password, etc.
I try to use spring aop to track all login logs, but I only get the logs when the login is successful.
These are the jwt filter and the spring aop code.
public class JwtUsernameAndPasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
public JwtUsernameAndPasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
/* get username and password in user request by jwt */
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
UsernameAndPasswordAuthenticationRequest authenticationRequest = new ObjectMapper()
.readValue(request.getInputStream(), UsernameAndPasswordAuthenticationRequest.class);
Authentication authentication = new UsernamePasswordAuthenticationToken(
authenticationRequest.getUsername(),
authenticationRequest.getPassword()
);
Authentication authenticate = authenticationManager.authenticate(authentication);
return authenticate;
} catch (IOException e) {
throw new RuntimeException();
}
}
/* create jwt token when user pass the attemptAuthentication */
#Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain, Authentication authResult) throws IOException, ServletException {
String key = "securesecuresecuresecuresecuresecuresecuresecuresecuresecuresecure";
String token = Jwts.builder()
.setSubject(authResult.getName())
.claim("authorities", authResult.getAuthorities())
.setIssuedAt(new Date())
.setExpiration(java.sql.Date.valueOf(LocalDate.now().plusWeeks(2)))
.signWith(Keys.hmacShaKeyFor(key.getBytes()))
.compact();
response.addHeader("Authorization", "Bearer " + token);
}
}
#Aspect
#Component
public class LoginLogAOP {
private static final Logger logger = LoggerFactory.getLogger(LoginLogAOP.class);
#AfterReturning(pointcut="execution(* org.springframework.security.authentication.AuthenticationManager.authenticate(..))"
,returning="result")
public void afteReturn(JoinPoint joinPoint,Object result) throws Throwable {
logger.info("proceed: {}", joinPoint.getArgs()[0]);
logger.info("result: {}", ((Authentication) result));
logger.info("user: " + ((Authentication) result).getName());
}
}
Has anyone tracked login logs through Spring Security jwt? Thank you very much for your help!
Your advice type #AfterReturning does exactly what the name implies: It kicks in after the method returned normally, i.e. without exception. There is another advice type #AfterThrowing, if you want to intercept a method which exits by throwing an exception. Then there is the general #After advice type which kicks in for both. It is like the supertype for the first two subtypes.
And then of course if you want to do more than just react to method results and log something, but need to actually modify method parameters or method results, maybe handle exceptions (which you cannot do in an "after" advice), you can use the more versatile, but also more complex #Around advice.
Actually, all of what I said is nicely documented in the Spring manual.
Update: I forgot to mention why #AfterReturning does not capture the cases you are missing in your log: Because according to the documentation for AuthenticationManager.authenticate(..), in case of disabled or locked accounts or wrong credentials the method must throw certain exceptions and not exit normally.

Throw custom exception from AuthenticationProvider to client(spring security)

I develop a spring boot REST service. I use #ControllerAdvice for exception catching. Also, I have a custom AuthenticationProvider and check a license in it.
#Component
public class MyAuthenticationProvider implements AuthenticationProvider {
private final LicenseService licenseService;
public MyAuthenticationProvider(LicenseService licenseService) {
this.licenseService = licenseService;
}
#Override
public boolean supports(Class<?> authentication) {
//...some code
}
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
try {
//...some code
licenseService.checkLicense(userDetails.getSomeId(), LicenseCode.FOO);
//..some code
} catch (LicenseException error) {
throw new AccessDeniedException(error.getMessage());
}
}
}
My licenseService throws LicenseException if the license does not exist or incorrect. I catch it and wrap to AccessDeniedException
at first, I wanted to catch LicenseException in #ControllerAdvice but quickly understood that it could be wrong. #ControllerAdvice catches exceptions in the controller layer.
That is why I wrap my exception to AccessDeniedException. But I want another logic: I want to throw a custom exception to the frontend. The frontend must understand this exception and show a special dialog to the client(License is required... bla-bla-bla). But I don't know how to do it on this step(AuthenticationProvider)
Your Controller catching the exception from your AuthenticationProvider would send a ResponseEntity with HttpStatus.FORBIDDEN to your client. You can wrap your exception or an errormessage in the body of the HTTP response.

How can I read the user credentials from a JSON login attempt using default classes?

I am following a tutorial on Implementing JWT Authentication on Spring Boot APIs and in the key part of the author's JWTAuthenticationFilter class (which is just what it sounds like), he retrieves the username and password from the JSON body of a POST request to a login endpoint.
So, using curl or Postman or something, you'd POST to /login with the body of your request containing something like {"username":"joe", "password":"pass"}.
The author's authentication filter maps this JSON into an ApplicationUser instance with this method:
#Override
public Authentication attemptAuthentication(HttpServletRequest req,
HttpServletResponse res) throws AuthenticationException {
try {
ApplicationUser creds = new ObjectMapper()
.readValue(req.getInputStream(), ApplicationUser.class);
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
creds.getUsername(),
creds.getPassword(),
new ArrayList<>())
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
Good enough, but to do this he's created a custom class ApplicationUser as well as a customer UserDetailsService, a UserController, and an ApplicationUserRepository all to work with his own idiosyncratic in-memory database backend. In my real Spring Boot app, I'm using the default database schema and letting Spring Boot authenticate users via JDBC. It works great with form authentication. This is the bit in my WebSecurityConfigurerAdapter that does it:
#Autowired
private DataSource dataSource;
#Override
public void configure(AuthenticationManagerBuilder builder) throws Exception {
builder .jdbcAuthentication()
.dataSource(dataSource);
}
So my question is, how does the attemptAuthentication method need to change from the tutorial's code? I took a guess that the default implementation might be User.class and tried to plug it into the expression that imports "creds", like this:
User creds = new ObjectMapper().readValue(request.getInputStream(), User.class);
That didn't work. I get this exception:
webapp_1 | 2019-11-22 15:54:43.234 WARN 1 --- [nio-8080-exec-2] n.j.w.g.config.JWTAuthenticationFilter : inputstream: org.apache.catalina.connector.CoyoteInputStream#1477c9d5
webapp_1 | 2019-11-22 15:54:43.298 ERROR 1 --- [nio-8080-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception
webapp_1 |
webapp_1 | java.lang.RuntimeException: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `org.springframework.security.core.userdetails.User` (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
I don't want to just dive into subclassing User because I actually don't even know if it's the right class, or if I even need it here. Can I just extract username and password from my JSON POST in some other way, maybe bypassing the use of ObjectMapper()? Or should I create a private internal class here? To sum up, the question is: How can I best adapt this "attemptAuthentication" method while using the default JDBC-based implementation of users in Spring Boot?
I found a solution that works, although I still think that there's got to be a more elegant way of doing it. What I do is define a static inner class LoginAttempt and use ObjectMapper to read the JSON as an instance of it.
I found that without the static modifier, Jackson cannot deserialize an inner class (see this question) but with "static" it works.
The relevant code from JWTAuthenticationFilter is:
static class LoginAttempt {
private String username;
private String password;
public LoginAttempt() {}
public String getUsername() { return username; }
public String getPassword() { return password; }
public void setUsername(String username) { this.username = username; }
public void setPassword(String password) { this.password = password; }
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
LoginAttempt creds = new ObjectMapper().readValue(request.getInputStream(), LoginAttempt.class);
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
creds.getUsername(),
creds.getPassword(),
new ArrayList<>()
)
);
} catch( IOException e ) {
throw new RuntimeException(e);
}
}

AuthenticationFailureHandler HttpServletResponse.sendError url

I have developed single page web application using Spring Boot and Spring MVC. I am using Spring Security and JWT to authenticate users. I have written a custom AuthenticationFailureHandler which works but I want to know how I can control the url that a user gets redirect to when an exception is thrown. My AuthenticationFailureHandler looks like this:
public class JwtAuthenticationFailureHandler implements AuthenticationFailureHandler {
#Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.sendError(HttpStatus.UNAUTHORIZED.value(), exception.getMessage());
}
}
When the JWT expires the application throws an AccountExpiredException, the AuthenticationFailureHandler.onAuthenticationFailure method gets executed and the user gets redirected to the login page:
http://localhost:8080/login?sessionExpired=true
This is all good, but I have no idea how the sessionExpired=true query string is generated and I want to have some control over it. In the past I have used ExceptionMappingAuthenticationFailureHandlers like this:
Map<String, String> mappings = new HashMap<>();
mappings.put(BadCredentialsException.class.getCanonicalName(), BAD_CREDENTIALS_EXCEPTION_URL);
mappings.put(AccountExpiredException.class.getCanonicalName(), ACCOUNT_EXPIRED_EXCEPTION_URL);
mappings.put(CredentialsExpiredException.class.getCanonicalName(), CREDENTIALS_EXPIRED_EXCEPTION_URL);
mappings.put(DisabledException.class.getCanonicalName(), ACCOUNT_INACTIVE_EXCEPTION_URL);
mappings.put(LockedException.class.getCanonicalName(), ACCOUNT_LOCKED_EXCEPTION_URL);
mappings.put(ValidationException.class.getCanonicalName(), VALIDATION_EXCEPTION_URL);
ExceptionMappingAuthenticationFailureHandler exceptionMappingAuthenticationFailureHandler = new ExceptionMappingAuthenticationFailureHandler();
exceptionMappingAuthenticationFailureHandler.setExceptionMappings(mappings);
So based on the various exceptions above I would like to be able to redirect to the following URLs:
http://localhost:8080/login?error
http://localhost:8080/login?accountexpired
http://localhost:8080/login?credentialsexpired
http://localhost:8080/login?accountlocked
http://localhost:8080/login?accountinactive
http://localhost:8080/login?validationerror
I'm not sure who to do this with response.sendError and I don't know how the sessionExpired=true query string is being generated. I have tried throwing different exceptions but the url never changes.
I have a couple of questions. Is it possible to control the URL when using HttpServletResponse.sendError and if not is it possible ot set the HttpStatus code when using ExceptionMappingAuthenticationFailureHandler.sendRedirect?
Why don't you try to use the response.sendRedirect:
#Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
final HttpSession session = request.getSession(false);
if (session != null) {
request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);
}
//here the logic to get the error type of the exception
String errorMessage = ????
redirectStrategy.sendRedirect(request, response,
"http://localhost:8080/login?" + errorMessage);
}

dynamically add param to userAuthorizationUri in oauth2

Sometimes user's refresh token in local DB becomes stale. To replenish I'm trying to add prompt=consent param while making the oauth2 call. I was trying to #Autowire AuthorizationCodeAccessTokenProvider in my config class and in the afterPropertiesSet I was doing a setTokenRequestEnhancer and then realized that this bean is not even initialized via spring container when i looked the following code in OAuth2RestTemplate
private AccessTokenProvider accessTokenProvider = new AccessTokenProviderChain(Arrays.<AccessTokenProvider> asList(
new AuthorizationCodeAccessTokenProvider(), new ImplicitAccessTokenProvider(),
new ResourceOwnerPasswordAccessTokenProvider(), new ClientCredentialsAccessTokenProvider()));
Searched if any spring code is calling org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider.setAuthorizationRequestEnhancer(RequestEnhancer) to learn how to access it, but no one is calling it.
Question: How to dynamically add a param to userAuthorizationUri while making oauth2 call?
Unfortunately, I haven't found an elegant solution neither. I have noticed, however, that redirect is triggered by UserRedirectRequiredException.
I was able to dynamically add request params by registering custom filter that modifies this exception on the fly.
#Component
#Order(-102)
public class EnhanceUserRedirectFilter implements Filter {
#Override
public void init(final FilterConfig filterConfig) throws ServletException {
}
#Override
public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException {
try {
chain.doFilter(request, response);
} catch (final UserRedirectRequiredException ex) {
ex.getRequestParams().put("prompt", "consent");
throw ex;
}
}
#Override
public void destroy() {
}
}
Please note, such servlet filter has to have higher precedence than Spring Security. In my case, -102 is higher precedence than Spring Security default of -100.

Resources