I want to return custom message if user during authentication process is locked or expired. I tried to implement this:
#Service
public class UserDetailsHandler implements UserDetailsService {
#Autowired
private UsersService usersService;
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
final Optional<Users> user = usersService.findByLogin(username);
if (!user.isPresent()) {
throw new UsernameNotFoundException("User '" + username + "' not found");
}
return user
.map(value -> {
return new User(
value.getLogin(),
value.getEncryptedPassword(),
value.getEnabled(),
hasAccountExpired(value.getExpiredAt()),
hasPasswordExpired(value.getPasswordChangedAt()),
hasAccountLocked(value.getLockedAt()),
Collections.singleton(new SimpleGrantedAuthority(value.getRole().getAuthority()))
);
}).orElseThrow(() -> new UsernameNotFoundException("User with username " + username + " not found"));
}
private boolean hasAccountExpired(LocalDateTime account_expired_at) {
return account_expired_at == null;
}
Full code: GitHub
The question is how to create handlers which return some custom message if the validation returns true value for statuses user locked or user expired?
The best option for you is:
Implement Spring UserDetails in your entity Users.
Check in loadUserByUsername if the user has been locked, etc using Spring AccountStatusUserDetailsChecker class.
Add into your EngineExceptionHandler the required methods to manage those exceptions: LockedException, AccountExpiredException, etc
You will see examples of above points in the following links:
Point 1
Point 2
Points 2-3
Well I briefly look at your codes and you implement a JwtTokenFilter that will some how calls the UserDetailsHandler .
In JwtTokenFilter , you already catch and handle EngineException which contain the HTTP status codes and a message. An HTTP response will be sent out which the status and the body message that are the same as what defined in the caught EngineException
It seems that you already configure everything for it to work , so just simply throw EngineException with the suitable HTTP status code and message from the UserDetailsHandler . Something like :
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Users user = usersService.findByLogin(username)
.map(value -> {
return new User(
value.getLogin(),
value.getEncryptedPassword(),
value.getEnabled(),
hasAccountExpired(value.getExpiredAt()),
hasPasswordExpired(value.getPasswordChangedAt()),
hasAccountLocked(value.getLockedAt()),
Collections.singleton(new SimpleGrantedAuthority(value.getRole().getAuthority()))
).orElseThrow(()-> throw new UsernameNotFoundException("User '" + username + "' not found"));
if (user.isAccountLock()){
throw new EngineException(HttpStatus.UNAUTHORIZED , "Custom message for account lock ......")
}
if(user.isAccountExpired()){
throw new EngineException(HttpStatus.UNAUTHORIZED , "Custom message for account expired... ......")
}
}
Spring Security uses the messages.properties which consist of default messages, we can add our custom message with the same. Add messages.properties and add a message as shown below.
AccountStatusUserDetailsChecker.expired=User account has expired
AccountStatusUserDetailsChecker.locked=User account is locked
AbstractUserDetailsAuthenticationProvider.expired=User account has expired
AbstractUserDetailsAuthenticationProvider.locked=User account is locked
You may find the default messages here
Its simple 2 steps approach. User expired means token expired
Step 1
Modify JWTTokenProvider Class to add a custom header to Http Servlet Request using setAttribute() method.
JwtTokenProvider.java
public boolean validateToken(String token,HttpServletRequest httpServletRequest){
try {
Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token);
return true;
}catch (SignatureException ex){
System.out.println("Invalid JWT Signature");
}catch (MalformedJwtException ex){
System.out.println("Invalid JWT token");
}catch (ExpiredJwtException ex){
System.out.println("Expired JWT token");
httpServletRequest.setAttribute("expired",ex.getMessage());
}catch (UnsupportedJwtException ex){
System.out.println("Unsupported JWT exception");
}catch (IllegalArgumentException ex){
System.out.println("Jwt claims string is empty");
}
return false;
}
Step 2
Modify commence method in JwtAuthenticationEntryPoint.class to check expired header in http servlet request header that we added in step 1.
JwtAuthenticationEntryPoint.java
#Override
public void commence(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
AuthenticationException e) throws IOException, ServletException {
final String expired = (String) httpServletRequest.getAttribute("expired");
System.out.println(expired);
if (expired!=null){
httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED,expired);
}else{
httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED,"Invalid Login details");
}
}
It is a best practice to return detailed error messages in any REST API. We used this to customize spring rest jwt token expired response to return more detailed error response. We can use this method not only for token expired but also for other jwt token exceptions like SignatureException, Malformed JwtException, UnsupportedJwtException and IllegalArgumentException.
Related
I want to implement JWT authentication with Spring Boot based on this tutorial.
Endpoint:
#PostMapping("/authorize")
public String login(#Valid #RequestBody AuthenticationDTO resetDTO) {
return userRestService.authorize(resetDTO.getName(), resetDTO.getPassword());
}
public String authorize(String username, String password) {
try {
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
Optional<Users> user = userService.findByLogin(username);
if(!user.isPresent()){
throw new EngineException(ErrorDetail.NOT_FOUND);
}
return jwtTokenProvider.createToken(username, Collections.singletonList(user.get().getRole()));
} catch (AuthenticationException e) {
throw new EngineException(ErrorDetail.NOT_FOUND);
}
}
Full code: Github
When I make a POST request to authenticate user:
{
"name": "admin",
"password": "qwerty"
}
I get this full log
I don't see into the log any SQL query ti get the credentials from the database. Looks like authenticationManager.authenticate is not making authentication.
Do you know what might cause this problem and how to fix this?
I am using apache shiro for ldap authentification:
Factory<SecurityManager> ldapFactory = new IniSecurityManagerFactory("classpath:active.ini");
SecurityManager sManager = ldapFactory.getInstance();
SecurityUtils.setSecurityManager(sManager);
Subject currentUser = SecurityUtils.getSubject();
if (!currentUser.isAuthenticated()) {
UsernamePasswordToken token = new UsernamePasswordToken("user", "password");
try {
currentUser.login(token);
} catch (UnknownAccountException ex) {
logger.info("Unknown user");
} catch (IncorrectCredentialsException ex) {
logger.info("Incorrect credentials");
} catch (LockedAccountException ex) {
logger.info("Account is Locked");
} catch (AuthenticationException ex) {
logger.info("Authentication Exception");
}
}
logger.info("User [" + currentUser.getPrincipal() +"] logged succesfully");
currentUser.logout();
Calling currentUser.getPrincipal(), I've got only email address from the logged user.
How to get more user information, for example lastname, firstname, etc?
getPrincipal() only returns the subject's identifier. Try to use getPrincipals() instead.
Object getPrincipal()
Returns this Subject's application-wide uniquely identifying
principal, or null if this Subject is anonymous.
PrincipalCollection getPrincipals()
Returns this Subject's principals
(identifying attributes) in the form of a PrincipalCollection or null
if this Subject is anonymous
Apache Shiro API (JavaDoc)
You could extend the LDAP Realm. and add more context to Principal/PrincipalCollection (or return a custom object or your Principal)
This is an old thread, but I find myself digging it up every once and a while and covers this topic (It mentions JSecurity which is the original project name for Shiro).
I've started to use JHipster weeks ago and everything went find since now. I want to have a LDAP authentification with at the same time the default authentification of JHipster.
I followed this https://jhipster.github.io/tips/016_tip_ldap_authentication.html and it doesn't work as planned.
Actually my configuration is connecting well to my LDAP server and i know by viewing logs that the login search into the LDAP server and compare the password.
The problem is the login fail with the error :
UT005023: Exception handling request to /api/authentication
org.springframework.security.core.userdetails.UsernameNotFoundException: User nseys was not found in the database
at com.mycompany.myapp.security.PersistentTokenRememberMeServices.lambda$onLoginSuccess$1(PersistentTokenRememberMeServices.java:116)
at java.util.Optional.orElseThrow(Optional.java:290)
at com.mycompany.myapp.security.PersistentTokenRememberMeServices.onLoginSuccess(PersistentTokenRememberMeServices.java:116)
at org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices.loginSuccess(AbstractRememberMeServices.java:294)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
...
The thing is I want JHipster to automatically create the user in database when it doesn't exist in there with a mapping of parameters (but only when it's a LDAP user) and just connect if it's already done.
I've searched Spring-security solution aswell but the implementations are too far away from the initial files created by JHipster and I don't want to destroy all this.
Well I tried something that work, I don't know if this is how I should have done, but since I've found nothing about that, and it's not documented alot, I'll stick with that solution unless I find a better solution.
// PersistentTokenRememberMeServices.java
protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication
successfulAuthentication) {
String login = successfulAuthentication.getName();
log.debug("Creating new persistent login for user {}", login);
PersistentToken t = new PersistentToken();
t.setSeries(RandomUtil.generateSeriesData());
t.setTokenValue(RandomUtil.generateTokenData());
t.setTokenDate(LocalDate.now());
t.setIpAddress(request.getRemoteAddr());
t.setUserAgent(request.getHeader("User-Agent"));
PersistentToken token = userRepository.findOneByLogin(login).map(u -> {
t.setUser(u);
return t;
}).orElse(null);
if (token == null) {
if (successfulAuthentication.getPrincipal() instanceof LdapUserDetails) {
User ldapUser = new User();
ldapUser.setLogin(login);
ldapUser.setPassword(RandomStringUtils.random(60)); // We use LDAP password, but the password need to be set
ldapUser.setActivated(true);
CustomLdapUserDetails customLdapUserDetails = (CustomLdapUserDetails) successfulAuthentication.getPrincipal();
ldapUser.setEmail(customLdapUserDetails.getEmail());
ldapUser.setFirstName(customLdapUserDetails.getFirstName());
ldapUser.setLastName(customLdapUserDetails.getLastName());
Set<Authority> authorities = new HashSet<>();
authorities.add(this.authorityRepository.findOneByName("ROLE_USER"));
ldapUser.setAuthorities(authorities);
ldapUser.setLangKey("fr");
userRepository.save(ldapUser);
t.setUser(ldapUser);
token = t;
} else {
throw new UsernameNotFoundException("User " + login + " was not found in the database");
}
}
...
}
And I added a contextMapper to get the attributes in the LDAP server
// SecurityConfiguration.java
#Bean
public UserDetailsContextMapper userDetailsContextMapper() {
return new LdapUserDetailsMapper() {
#Override
public UserDetails mapUserFromContext(DirContextOperations ctx, String username, Collection<? extends GrantedAuthority> authorities) {
UserDetails details = super.mapUserFromContext(ctx, username, authorities);
return new CustomLdapUserDetails((LdapUserDetails) details, ctx);
}
};
}
#Inject
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
LdapContextSource contextSource = new LdapContextSource();
contextSource.setUrl(applicationProperties.getLdap().getUrl());
contextSource.setBase(applicationProperties.getLdap().getBase());
contextSource.setUserDn(applicationProperties.getLdap().getUserDn());
contextSource.setPassword(applicationProperties.getLdap().getPassword());
contextSource.afterPropertiesSet(); //needed otherwise you will have a NullPointerException in spring
auth.ldapAuthentication()
.userDetailsContextMapper(userDetailsContextMapper())
.userSearchBase(applicationProperties.getLdap().getSearchBase()) //don't add the base
.userSearchFilter(applicationProperties.getLdap().getSearchFilter())
.contextSource(contextSource)
;
}
For my project I have used Spring 4.2.5, Spring Security 4.0.4 and JSF Mojarra. And I'll throw a custom exception from my custom UserDetailsService.
#Override
public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException{
com.entity.User domainUser = userDAO.getUser(login);
boolean enabled = false;
boolean accountNonExpired = true;
boolean credentialsNonExpired = true;
boolean accountNonLocked = true;
if(!enabled){
throw new DisabledException("User is disabled!");
}
return new User(domainUser.getLogin(),
domainUser.getPassword(),
enabled,
accountNonExpired,
credentialsNonExpired,
accountNonLocked,
getAuthorities(domainUser.getRole().getId()));
}
My LoginBean is:
public String login() {
RequestContext context = RequestContext.getCurrentInstance();
FacesMessage msg = null;
boolean loggedIn = false;
try {
SecurityContext secContext = SecurityContextHolder.getContext();
Authentication auth = new UsernamePasswordAuthenticationToken(username, password);
secContext.setAuthentication(auth);
loggedIn = true;
msg = new FacesMessage(FacesMessage.SEVERITY_INFO, "Welcome", username);
return "/pages/index?faces-redirect=true";
} catch (DisabledException e) {
loggedIn = false;
msg = new FacesMessage(FacesMessage.SEVERITY_ERROR, "Login Error", e.getMessage());
} catch (ServletException e) {
loggedIn = false;
msg = new FacesMessage(FacesMessage.SEVERITY_ERROR, "Login Error", e.getMessage());
}
FacesContext.getCurrentInstance().addMessage(null, msg);
context.addCallbackParam("loggedIn", loggedIn);
return "/login";
}
So if I set "enabled" to true and provide right user credentials all works fine. But if I change it to false I can't see any exception in log and can't login.
I've try with this code. And it works fine but I can see only "Bad credentials" exception in any cases.
HttpServletRequest httpReq = (HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest();
httpReq.login(username, password);
I just started with java and spring, so can you explain me difference between http Req.login(username, password) and getContext.setAuthentication(auth). In first case I can retrieve authenticated user and in second case I get anonymously authenticated user.
Which is right solution for user authentication and custom UserDetailsService.
P.S. I'm using java based config.
Sorry If this question was previously answered. I can't find working solution.
UPDATE 1:
If I comment this
if(!enabled){
throw new DisabledException("User is disabled!");
}
In debug I can see default exception:
org.springframework.security.authentication.DisabledException: User is disabled
but I still can't catch this in my LoginBean. Why?
UPDATE 2:
I made some changes in my spring security config and now I can see locked, expired exceptions in my growl (primefaces) when I'm using HttpServletRequest httpReq.login(username, password).
So how I can customize exception messages?
And I still can't understand difference between HttpServletRequest login and UsernamePasswordAuthenticationToken authentication.
I have created a custom AuthenticationProvider to perform custom security checks. I have also created custom exceptions that inherit from AccountStatusException to notify user status problems such as when the user has not verified his account for an specific period of time.My UserDetails is also acustom implementation.
Here is the code for the security checks I perform. Code that is irrelevant to the case has been omitted.
public class SsoAuthenticationProvider implements AuthenticationProvider {
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = (String) authentication.getPrincipal();
User user = null;
if (username != null) {
user = getUserRepository().findByUserName(username);
if (user != null) {
if (user.getEnabled() != 0) {
if ((user.getUserDetail().getConfirmed() != 0)
|| ((new Date().getTime() - user.getUserDetail().getRequestDate().getTime()) / (1000 * 60 * 60 * 24)) <= getUnconfirmedDays()) {
if (getPasswordEncoder().isPasswordValid(user.getPassword(),
(String) authentication.getCredentials(), user)) {
user.authenticated = true;
user.getAuthorities();
}
} else {
throw new UserNotConfirmedAndTimeExceeded(
"User has not been cofirmed in the established time period");
}
} else {
throw new DisabledException("User is disabled");
}
} else {
throw new BadCredentialsException("User or password incorrect");
}
} else {
throw new AuthenticationCredentialsNotFoundException("No credentials found in context");
}
return user;
}
}
The SsoAuthenticationProvider checks:
That the username is registered (exists in the db)
That the user has confirmed his email
If the user has not confirmed his email, check that he is still in the grace period (this is a few days we give users to confirm their email while letting them access the site)
If the user has not confirmed email and he is not in the grace period, throw security exception to signal these status and reject authentication
The problem is that not all of these exceptions are thrown up the stack up to the controller so it seems impossible to inform the user about the login problem.
Using UserDetails methods such as isEnabled() (and similar) is not a possibility as the semantics of our different user account statuses are completely different.
Is this the right approach to build custom security with custom exceptions? Should i implement sth else to make this work?
To close the previously asked question let me explain what we did.
As I commented to previous responses, using provided methods in UserDetails objectis not feasible as you cannot capture all the login failure semantics with the given methods. In our case these semantics are still very limited but in other cases it could indfinitely extend over time to express different user situations.
The exception approach was finally the best one. The final code looks like this
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username=(String)authentication.getPrincipal();
User user=null;
if(username!=null){
user=getUserRepository().findByUserName(username);
if(user!=null){
if(user.getEnabled()!=0){
if((user.getUserDetail().getConfirmed()!=0)||((new Date().getTime()-user.getUserDetail().getRequestDate().getTime())/(1000 * 60 * 60 * 24))<=getUnconfirmedDays()){
if(getPasswordEncoder().isPasswordValid(user.getPassword(), (String)authentication.getCredentials(), user)){
user.authenticated=true;
user.getAuthorities();
} else {
throw new BadCredentialsException("Password incorrect");
}
}else{
throw new UserNotConfirmedAndTimeExceeded("User has not been cofirmed in the established time period");
}
}else{
throw new DisabledException("User is disabled");
}
}else{
throw new BadCredentialsException("User does not exist");
}
}else{
throw new AuthenticationCredentialsNotFoundException("No credentials found in context");
}
return user;
}
All exceptions are part of the spring security exception stack. This is, those custom exceptions inherit from some existing exception. Then, in your security controller you should check for security exceptions and treat them as desired. For example redirecting to different pages.
Hope this helps!
i think its better to use other method/properties of user detail object for this purpose.
like
isAccountNonExpired()
isAccountNonLocked()
isEnabled()
and if you want to display custom error message then use message properties as explained in this article