Spring-security - invalid login always return 403 instead of appropriate errors - spring

I am trying to input some more "accurate" error handling for invalid logins.
The three main objectives: invalid password, account disabled, invalid email.
The current calling hierarchy is the following:
Attempted login requests
#Override // THIS OVERRIDES THE DEFAULT SPRING SECURITY IMPLEMENTATION
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
String email = request.getParameter("email");
String password = request.getParameter("password");
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(email, password);
return authManager.authenticate(authToken);
}
This calls another override method where I tried to insert error handling because it has access to the userRepo and object. The issue here is if the AccountLockedException or fails on email finding or password verification, it will always reutrn a 403 and no indication of the thrown exception.
#SneakyThrows
#Override // THIS OVERWRITES THE DEFAULT SPRING SECURITY ONE
public UserDetails loadUserByUsername(String email){
User user = findUserByEmail(email);
if ( user != null){
if (user.isEnabled()){
Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
user.getRoles().forEach(role -> { authorities.add(new SimpleGrantedAuthority(role.getName()));});
sucessfulLogin(user);
return new org.springframework.security.core.userdetails.User(user.getEmail(), user.getPassword(), authorities);
}
else { throw new AccountLockedException("Account disabled"); }
}
}
However, what I have found this previous method on throwing will call this additional override method (in the same class as the attempted authentication)
#Override // DO SOMETHING WITH THIS TO PREVENT BRUTE FORCE ATTACKS WITH LIMITED NUMBER OF ATTEMPTS IN A TIME-FRAME
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
System.out.println("aaa");
super.unsuccessfulAuthentication(request, response, failed);
}
Though, at this point it will display the following:
this option gets shown when the password is incorrect.
this option gets shown when the account is disabeld.
this option gets shown when the email is incorrect.
My question is. Firstly how do I appropriately distinguish between these errors and secondly send appropriate http responses based on these errors?

if (failed != null) {
if (failed.getMessage() == "AccountLockedException") {
response.setStatus(403);
} // if account is disabled
else if (failed.getMessage() == "EntityNotFoundException") {
response.setStatus(400);
} // if email incorrect
else if (failed.getMessage() == "Bad credentials") {
response.setStatus(400);
} // if password incorrect
else {
System.out.println("some kind of other authentication error");
response.setStatus(418); // some random error incase this ever happens
}

Related

Spring security 403 with disabled csrf

Using spring security, I've looked at similar questions but they say to try disable cors & csrf.
I am using it on the browser so I will need csrf. But just testing briefly doesn't change the outcome.
On login I get an access token and refresh token.
Using this token gives me a 403 forbidden response code.
My configuration is the following:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.authorizeRequests().antMatchers("/login").permitAll();
http.authorizeRequests().antMatchers(GET, "/**").hasAnyAuthority("STUDENT");
http.authorizeRequests().anyRequest().authenticated();
http.addFilter(new CustomAuthenticationFilter(authenticationManagerBean()));
http.addFilterBefore(new CustomAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class);
}
I think maybe its something to do with this filter but if I change forbidden.value to something else the result is still forbidden.value
public class CustomAuthorizationFilter extends OncePerRequestFilter { // INTERCEPTS EVERY REQUEST
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if(request.getServletPath().equals("/login")){ filterChain.doFilter(request,response); } // DO NOTHING IF LOGGING IN
else{
String authorizationHeader = request.getHeader(AUTHORIZATION);
if(authorizationHeader != null && authorizationHeader.startsWith("Bearer ")){
try {
String token = authorizationHeader.substring("Bearer ".length()); // TAKES TOKEN STRING AND REMOVES BEARER
// THIS NEEDS MAKING SECURE AND ENCRYPTED vvvvvvv
Algorithm algorithm = Algorithm.HMAC256("secret".getBytes()); // <<<<<<<<<<<<<<<<<<<<<<<
JWTVerifier verifier = JWT.require(algorithm).build(); // USING AUTH0
DecodedJWT decodedJWT = verifier.verify(token);
String email = decodedJWT.getSubject(); // GETS EMAIL
String[] roles = decodedJWT.getClaim("roles").asArray(String.class); // GETS ROLES
Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
stream(roles).forEach(role -> { authorities.add(new SimpleGrantedAuthority(role)); }); // CONVERTS ALL USERS ROLE INTO AN AUTHORITY
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(email, null); // PASSWORD IS NULL AT THIS POINT
SecurityContextHolder.getContext().setAuthentication(authToken); // INSERTS TOKEN INTO CONTEXT // THIS SHOWS AUTHENTICATED FALSE, DETIALS FALSE AND GRANTED AUTHORITIES EMPTY
filterChain.doFilter(request, response); // GETS TO THIS LINE HERE
}
catch (Exception e){
response.setHeader("error" , e.getMessage() );
response.setStatus(FORBIDDEN.value());
Map<String, String> error = new HashMap<>();
error.put("error_message", e.getMessage());
response.setContentType(APPLICATION_JSON_VALUE);
new ObjectMapper().writeValue(response.getOutputStream(), error); // THEN SKIPS RIGHT TO THIS LINE HERE EVEN IF BREAKPOINTING BEFORE
}
}
else{ filterChain.doFilter(request, response); }
}
}
}
debugging shows it hits filterChain.doFilter(request, response) then jumps straight to the exception catch objectMapper line
The user submitting is also of the Student role.
this line
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(email, null);
is missing authorities:
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(email, null, authorities);
Hope that my answer can help,
you can drop a breakpoint to the line change the response status, and then check who and why it returns 403, it can finally help you get the solution
Drop a breakpoint on the line set the 403 status, to see how this happen from the stackframes.
Guess that it returns 403 without much other information, but it must need to set the status to the response, right? So drop a breakpoint to the setStatus method, I don't know where it should locate, in tomcat lib, spring lib, or servlet lib. Check the HttpResponse, they're several implementation, set the breakpoints for those setStatus/setCode methods. (Next you can see it acutally happens at HttpResponseWrapper::setStatus)
Analyze the stackframes to see what's going on there
please check https://stackoverflow.com/a/73577697/4033979

How can I refresh tokens in Spring security

This line:
Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
Throws an error like this when my jwt token expires:
JWT expired at 2020-05-13T07:50:39Z. Current time:
2020-05-16T21:29:41Z.
More specifically, it is this function that throws the "ExpiredJwtException" exception :
How do I go about handling these exceptions? Should I catch them and send back to the client an error message and force them to re-login?
How can I implement a refresh tokens feature? I'm using Spring and mysql in the backend and vuejs in the front end.
I generate the initial token like this:
#Override
public JSONObject login(AuthenticationRequest authreq) {
JSONObject json = new JSONObject();
try {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(authreq.getUsername(), authreq.getPassword()));
UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
List<String> roles = userDetails.getAuthorities().stream().map(item -> item.getAuthority())
.collect(Collectors.toList());
if (userDetails != null) {
final String jwt = jwtTokenUtil.generateToken(userDetails);
JwtResponse jwtres = new JwtResponse(jwt, userDetails.getId(), userDetails.getUsername(),
userDetails.getEmail(), roles, jwtTokenUtil.extractExpiration(jwt).toString());
return json.put("jwtresponse", jwtres);
}
} catch (BadCredentialsException ex) {
json.put("status", "badcredentials");
} catch (LockedException ex) {
json.put("status", "LockedException");
} catch (DisabledException ex) {
json.put("status", "DisabledException");
}
return json;
}
And then in the JwtUtil class:
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, userDetails.getUsername());
}
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + EXPIRESIN))
.signWith(SignatureAlgorithm.HS256, SECRET_KEY).compact();
}
For more info, here is my doFilterInternal function that filters every request:
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException, ExpiredJwtException, MalformedJwtException {
try {
final String authorizationHeader = request.getHeader("Authorization");
String username = null;
String jwt = null;
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
jwt = authorizationHeader.substring(7);
username = jwtUtil.extractUsername(jwt);
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userService.loadUserByUsername(username);
boolean correct = jwtUtil.validateToken(jwt, userDetails);
if (correct) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken
.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
chain.doFilter(request, response);
} catch (ExpiredJwtException ex) {
resolver.resolveException(request, response, null, ex);
}
}
There are 2 main approaches to deal with such situations:
Manage access and refresh tokens
In this case, the flow is the following one:
User logins into the application (including username and password)
Your backend application returns any required credentials information and:
2.1 Access JWT token with an expired time usually "low" (15, 30 minutes, etc).
2.2 Refresh JWT token with an expired time greater than access one.
From now, your frontend application will use access token in the Authorization header for every request.
When backend returns 401, the frontend application will try to use refresh token (using an specific endpoint) to get new credentials, without forcing the user to login again.
Refresh token flow
(This is only an example, usually only the refresh token is sent)
If there is no problem, then the user will be able to continue using the application. If backend returns a new 401 => frontend should redirect to login page.
Manage only one Jwt token
In this case, the flow is similar to the previous one and you can create your own endpoint to deal with such situations: /auth/token/extend (for example), including the expired Jwt as parameter of the request.
Now it's up to you manage:
How much time an expired Jwt token will be "valid" to extend it?
The new endpoint will have a similar behaviour of refresh one in the previous section, I mean, will return a new Jwt token or 401 so, from the point of view of frontend the flow will be the same.
One important thing, independently of the approach you want to follow, the "new endpoint" should be excluded from the required Spring authenticated endpoints, because you will manage the security by yourself:
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
..
#Override
protected void configure(HttpSecurity http) throws Exception {
http.
..
.authorizeRequests()
// List of services do not require authentication
.antMatchers(Rest Operator, "MyEndpointToRefreshOrExtendToken").permitAll()
// Any other request must be authenticated
.anyRequest().authenticated()
..
}
}
You can call the API for getting the refresh token as below
POST https://yourdomain.com/oauth/token
Header
"Authorization": "Basic [base64encode(clientId:clientSecret)]"
Parameters
"grant_type": "refresh_token"
"refresh_token": "[yourRefreshToken]"
Please be noticed that, the
base64encode is the method to encrypt the client authorization. You can use online at https://www.base64encode.org/
the refresh_token is the String value of the grant_type
yourRefreshToken is the refresh token received with JWT access token
The result can be seen as
{
"token_type":"bearer",
"access_token":"eyJ0eXAiOiJK.iLCJpYXQiO.Dww7TC9xu_2s",
"expires_in":20,
"refresh_token":"7fd15938c823cf58e78019bea2af142f9449696a"
}
Good luck.

Spring Boot: How to tell client that account password has expired?

I have a simple login form and a login() method in a controller :
#PostMapping("/login")
public ResponseEntity<UserVO> login(#RequestBody UserVO userVO) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
userVO.getUsername(),
userVO.getPassword()
)
);
SecurityContextHolder.getContext().setAuthentication( authentication );
// ...
return ResponseEntity.ok( loggedInUser );
}
I have a users table with columns :
`username`
`password`
`password_expiration`
`account_expiration`
`account_locked`
`account_disabled`
When I set the value of column password_expiration to expire a user's password, Spring Boot's authenticationManager.authenticate() method throws an AccountExpiredException exception at next login attempt :
package org.springframework.security.authentication;
public class AccountExpiredException extends AccountStatusException {
public AccountExpiredException(String msg) {
super(msg);
}
public AccountExpiredException(String msg, Throwable t) {
super(msg, t);
}
}
and JSON response is :
{
"timestamp":"2018-07-26T22:53:05.392+0000",
"status":401,
"error":"Unauthorized",
"message":"Unauthorized",
"path":"/login"
}
I get the same JSON response (401 error code) whenever the password is wrong or one of the methods of UserVO (which in turn implements UserDetails) return false :
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
So far so good.
When the user logs in and his/her password is expired I want to redirect the UI to a mandatory password change page. But how ? I always get the same JSON respons.
1) Since the JSON output that is returned is always a HTTP 401 error, how can I get a more fine grained response ? (How to tell client code that the password has expired?)
2) Is it generally considered good or bad practice to inform the user that his/her account is locked / expired / disabled ? (good user experience vs giving away info about account state to hackers)
Maybe not the best solution, but I solved it by setting the "message" field :
#Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {
#Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
String message = "Unauthorized";
if ( authException instanceof BadCredentialsException )
message = AUTH_CREDENTIALS_BAD;
else if ( authException instanceof CredentialsExpiredException )
message = AUTH_CREDENTIALS_EXPIRED;
else if ( authException instanceof LockedException )
message = AUTH_ACCOUNT_LOCKED;
else if ( authException instanceof DisabledException )
message = AUTH_ACCOUNT_DISABLED;
else if ( authException instanceof AccountExpiredException )
message = AUTH_ACCOUNT_EXPIRED;
response.sendError( HttpServletResponse.SC_UNAUTHORIZED, message );
}
}
Now I get this as response :
{
"timestamp":"2018-07-27T12:54:53.097+0000",
"status":401,
"error":"Unauthorized",
"message":"credentials_expired",
"path":"/login"
}

Error Response while getting jwt access token for google user with Google Credential Object

I am trying to get the jwt access tokens for each user of my gsuite domain using the GoogleCredential and JacksonFactory libraries.
Code sample -
GoogleCredential credential = new GoogleCredential.Builder().setTransport(httpTransport)
.setJsonFactory(JSON_FACTORY)
.setServiceAccountId(clientEmail)
.setServiceAccountScopes(scopes)
.setServiceAccountPrivateKeyFromP12File(privateKeyFile)
.setServiceAccountUser(userEmail)
.build();
credential.refreshToken();
String accessToken = credential.getAccessToken();
All fields - clientEmail, scopes, key and userEmail are neither null nor empty
For a few number of users I am not able to get the access token and am getting this error
com.google.api.client.repackaged.com.google.common.base.Preconditions.checkNotNull(Preconditions.java:191) com.google.api.client.util.Preconditions.checkNotNull(Preconditions.java:127) com.google.api.client.json.jackson2.JacksonFactory.createJsonParser(JacksonFactory.java:96) com.google.api.client.json.JsonObjectParser.parseAndClose(JsonObjectParser.java:85) com.google.api.client.json.JsonObjectParser.parseAndClose(JsonObjectParser.java:81) com.google.api.client.auth.oauth2.TokenResponseException.from(TokenResponseException.java:88) com.google.api.client.auth.oauth2.TokenRequest.executeUnparsed(TokenRequest.java:287) com.google.api.client.auth.oauth2.TokenRequest.execute(TokenRequest.java:307) com.google.api.client.googleapis.auth.oauth2.GoogleCredential.executeRefreshToken(GoogleCredential.java:269) com.google.api.client.auth.oauth2.Credential.refreshToken(Credential.java:489) com.hubble.hubbleEngine.policyTypes.OAuth.getJWTAccessToken(OAuth.java:815)
This is happening only the first time, I am trying to get the access tokens. When I try to get the access tokens again for all users, I am able to get the access tokens for the users which were throwing the error the first time.
I debugged a bit and saw that the error gets generated from the following function present in com.google.api.client.auth.oauth2.TokenRequest
public final HttpResponse executeUnparsed() throws IOException {
// must set clientAuthentication as last execute interceptor in case it needs to sign request
HttpRequestFactory requestFactory =
transport.createRequestFactory(new HttpRequestInitializer() {
public void initialize(HttpRequest request) throws IOException {
if (requestInitializer != null) {
requestInitializer.initialize(request);
}
final HttpExecuteInterceptor interceptor = request.getInterceptor();
request.setInterceptor(new HttpExecuteInterceptor() {
public void intercept(HttpRequest request) throws IOException {
if (interceptor != null) {
interceptor.intercept(request);
}
if (clientAuthentication != null) {
clientAuthentication.intercept(request);
}
}
});
}
});
// make request
HttpRequest request =
requestFactory.buildPostRequest(tokenServerUrl, new UrlEncodedContent(this));
request.setParser(new JsonObjectParser(jsonFactory));
request.setThrowExceptionOnExecuteError(false);
HttpResponse response = request.execute();
if (response.isSuccessStatusCode()) {
return response;
}
throw TokenResponseException.from(jsonFactory, response);
}
The request.execute() hits the "https://accounts.google.com/o/oauth2/token" to get the token but it is throwing some error response. Due to this it throws the TokenResponseException mentioned at last. Here, the response.getContent() is null due to which the whole null exception is occuring.
Is there a way to know, which kind of error response is thrown by the call. (>300 or <200)? Or why such a case is happening ?

Session Tracking Login in spring mvc

I'm new using spring mvc in general. I'm generating login page and my problem is that it always redirects me to the notLoggedIn prompt after I've tried to log in.
The controller:
#RequestMapping(value="/login", method= RequestMethod.POST) //login
public String logIn(HttpServletRequest request, HttpServletResponse response, ModelMap map) {
HttpSession session= request.getSession();
request.getSession().setAttribute("isLoggedIn", "true");
String uname=request.getParameter("userid");
String pword=request.getParameter("password");
boolean exists=logInService.checkLogIn(uname, pword);
if(exists){
session.setAttribute("userid", uname);
return "Users"; //return to next success login jsp
} else {
return "Interface2"; //return to Invalid username and password jsp
}
}
The interceptor:
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
HttpSession session= request.getSession();
if(session.getAttribute("userid")!=null && session.getAttribute("isLoggedIn")!=null ){
System.out.println("Logged In");
}
else{
response.sendRedirect(request.getContextPath()+"/modulename/notLoggedIn");
System.out.println("Not logged in");
return false;
}
return true;
}
Your interceptor blocks every http request and does some check but it should actually allow and not check for login http request. Following changes are just to get the use case work. Refer note at the bottom for suggestions.
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
HttpSession session= request.getSession();
if(session.getAttribute("userid")!=null && session.getAttribute("isLoggedIn")!=null ){
//user has already logged in . so therefore can access any resource
System.out.println("Logged In");
return true;
}
//if code reaches here means that user is not logged in
//allow login http request. modify checks accordingly. like you can put strict equals.
if (request.getRequestURI().endsWith("/login")){
//user is not logged in but is trying to login. so allow only login requests
return true;
}
else{
//user is not logged in and is trying to access a resource. so redirect him to login page
response.sendRedirect(request.getContextPath()+"/modulename/notLoggedIn");
System.out.println("Not logged in");
return false;
}
}
Note: You can reorder your login http request check to avoid login request for already logged in user.

Resources