We added property isBlocked for users.
When following property is set user cannot login on our site.
We want to render error message "your account is temporary blocked..."
But I have not ideas how to pass this message to loginFailed controller method.
I have following spring-security configuration:
public class XXXSecurityServiceImpl implements UserDetailsService {
#Autowired
private TerminalAdminDao terminalAdminDao;
#Override
public UserDetails loadUserByUsername(String adminName) throws UsernameNotFoundException,
DataAccessException {
TerminalAdmin admin = terminalAdminDao.findAdminByEmail(adminName);
UserDetails userDetails = null;
if (admin != null) {
Set<SimpleGrantedAuthority> authorities = new HashSet<SimpleGrantedAuthority>();
for (AdminRole adminRole : admin.getAdminRoles()) {
authorities.add(new SimpleGrantedAuthority(adminRole.getRole()));
}
userDetails = new User(admin.getEmail(), admin.getPassword(), true, true, true, !admin.isBlocked(),
authorities);
}
return userDetails;
}
admin failed controlled method:
#RequestMapping(value = "/loginAdminFailed", method = RequestMethod.GET)
public String loginError(HttpSession session) {
session.setAttribute("login_error", "true");
return "admin/login";
}
How to understand in controller that user is blocked?
Basically you need to register custom authentication provider and custom authentication failure handler to handle authentication exceptions.
Check this SO question.
This works
added to controller:
#RequestMapping(value = "/loginAdminFailed", method = RequestMethod.GET)
public String loginError(HttpSession session, HttpServletRequest request) {
session.setAttribute("message", getErrorMessage(request, "SPRING_SECURITY_LAST_EXCEPTION"));
return "admin/login";
}
// customize the error message
private String getErrorMessage(HttpServletRequest request, String key) {
Exception exception = (Exception) request.getSession().getAttribute(key);
String error = "";
if (exception instanceof BadCredentialsException) {
error = messageSource.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials", null, LOCALE_RU);
} else if (exception instanceof LockedException) {
error = messageSource.getMessage(
"AbstractUserDetailsAuthenticationProvider.accountIsLocked", null, LOCALE_RU);
} else {
error = messageSource.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials", null, LOCALE_RU);
}
return error;
}
But this looks ugly
Related
I'm experiencing this difficulty and I still don't understand what's happening, I have a scenario where an api uses a feing client to obtain a user's profile from an external service, until this moment everything was going well, but when I solved it add the fallback, spring starts to inject the SecurityContextHolder.getContext() always with a null value, and with that the interceptor cannot get the token of the user who made the primary call of the application.
appliction.properties
application.authserver.url=http://localhost:8088/auth
feign.circuitbreaker.enabled=true
Interceptor Class
#Component
#Slf4j
public class FeingClientConfig {
#Value("${app.name}")
private String appName;
#Bean
public RequestInterceptor requestInterceptor() {
return new RequestInterceptor() {
#Override
public void apply(RequestTemplate template) {
if (SecurityContextHolder.getContext().getAuthentication() != null) {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof Jwt) {
var jwt = (Jwt) principal;
log.info("-------------------- service integration -----------------");
log.info(String.format(" Endpoint: %s", template.method() + " " + template.feignTarget().url() + template.url()));
log.info(String.format(" User: %s", jwt.getClaim("user_name").toString()));
log.info(String.format(" Bearer token: %s", jwt.getTokenValue().substring(0, 30) + "....."));
log.info("----------------------------------------------------------");
template.header("Authorization", String.format("Bearer %s", jwt.getTokenValue()));
template.header("x-app-name", appName);
}
}
}
};
}
}
Client
#FeignClient(url = "${application.authserver.url}", name = "authClient",
fallbackFactory = AuthClientFallBackFactory.class)
public interface AuthClient {
#GetMapping("/user/profile")
public UserProfile getProfile();
}
Fallback
#Slf4j
#Component
public class AuthClientFallBackFactory implements FallbackFactory<AuthClient> {
#Override
public AuthClient create(Throwable cause) {
return new AuthClient() {
#Override
public UserProfile getProfile() {
log.error("*** Fallback: error on get profile.", cause);
return new UserProfile();
}
};
}
}
If the feign.circuitbreaker.enabled property was set to false, this ignores the use of the fallback, but the SecurityContextHolder.getContext() object is inject correctly
What am I doing wrong?
Using in this project:
JDK 11
Spring Boot [2.4.2]
I am facing problem related to Session ID. I am using Spring Boot + Mongo DB Authentication.
Below is code and Configuration set:
application.properties
spring.session.store-type=mongodb
SecurityConfig.java
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter{
#Autowired
private PasswordEncoder passwordEncoder;
#Bean
public UserDetailsService mongoUserDetails() {
return new UserDetailsServiceImpl();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
UserDetailsService userDetailsService = mongoUserDetails();
auth
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder);
}
#Override
public void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/api/auth/login", "/api/auth/signup")
.permitAll().anyRequest()
.authenticated();
}
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
PasswordConfig.java
#Configuration
public class PasswordConfig {
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(10);
}
}
HttpSessionConfig.java
//This is to enable Mongo Http Session
#EnableMongoHttpSession(maxInactiveIntervalInSeconds = 5*60)
#Configuration
public class HttpSessionConfig {
#Bean
public JdkMongoSessionConverter jdkMongoSessionConverter() {
//This duration does not work for setting the Inactive Time for Mongo Session
return new JdkMongoSessionConverter(Duration.ofMinutes(30));
}
}
UserDetailsServiceImpl.java
#Service
public class UserDetailsServiceImpl implements UserDetailsService {
#Autowired
private UserRepository userRepository;
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if (user != null) {
List<GrantedAuthority> authorities = getUserAuthority(user.getRoles());
return buildUserForAuthentication(user, authorities);
} else {
throw new UsernameNotFoundException("username not found");
}
}
private UserDetails buildUserForAuthentication(User user, List<GrantedAuthority> authorities)
{
return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), authorities);
}
private List<GrantedAuthority> getUserAuthority(Set<Role> userRoles) {
Set<GrantedAuthority> roles = new HashSet<>();
userRoles.forEach((role) -> {
for (GrantedAuthority grantedAuthority : role.getRole().getGrantedAuthorities()) {
roles.add(grantedAuthority);
}
});
List<GrantedAuthority> grantedAuthorities = new ArrayList<>(roles);
return grantedAuthorities;
}
}
AuthController.java
#RestController
#RequestMapping("/api/auth")
public class AuthController {
private final static int IDX_USERNAME = 0;
private final static int IDX_PWD = 1;
#Autowired
private UserService userService;
#Autowired
private AuthenticationManager authenticationManager;
#PostMapping("/login")
public ResponseEntity<?> authenticateUser(HttpServletRequest req,
#RequestHeader("Authorization") String encUser) {
byte[] decoded = Base64.getDecoder().decode(encUser);
String decodedStr = new String(decoded, StandardCharsets.UTF_8);
String[] userCred = decodedStr.split(":");
String username = userCred[IDX_USERNAME];
String pwd = userCred[IDX_PWD];
Authentication authentication;
try {
authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, pwd));
} catch (BadCredentialsException e) {
return new ResponseEntity(e.getMessage(), HttpStatus.FORBIDDEN);
}
SecurityContext sc = SecurityContextHolder.getContext();
sc.setAuthentication(authentication);
HttpSession session = req.getSession(true);
session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, sc);
return new ResponseEntity(HttpStatus.NO_CONTENT);
}
#PostMapping("/logout")
public ResponseEntity<?> logoutUser(HttpServletRequest request, HttpSession session) {
// SecurityContextHolder.getContext().setAuthentication(null);
// SecurityContextHolder.clearContext();
if (session != null) {
session.invalidate();
}
Cookie[] cookies = request.getCookies();
if (cookies != null && cookies.length > 0) {
for (int i = 0; i < cookies.length; i++) {
cookies[i].setMaxAge(0);
}
}
return new ResponseEntity(HttpStatus.NO_CONTENT);
}
}
EmployeeController
#RestController
#RequestMapping("/employee")
public class EmployeeController {
#Autowired
private EmployeeService service;
#GetMapping("/leaves")
#PreAuthorize("hasAnyRole('ROLE_USER')")
public ResponseEntity showLeaves(#RequestParam String user) {
System.out.println("User: " + user);
Employee emp = service.findLeaves(user);
if (emp == null) {
return new ResponseEntity(HttpStatus.NOT_FOUND);
}
return new ResponseEntity(emp, HttpStatus.OK);
}
}
I Login using /api/auth/login API(AuthController.java) using manual authentication(not using httpBasic) by passing base64 ecnryption of username:password in Authorization Header. Authentication works using mongoDB user table and Spring will return the SESSION in the "set-cookie"
Concern 1
Till this point, its ok. Now when i send the request to /employee/leaves (EmployeeController.java) by sending SESSION received from login response in the Authorization Header, spring boot always returns "Forbidden" even though SESSION object is present in the DB before it does land in to the api
Concern 2
If i send login on the already login user, spring boot generates new session id which is also wrong. I tried using SessionManagement().setMaxSession(1) but did not work. I feel this is happening because of using manual authentication. Just a guess.
NOTE: I want to support login for different users(may be not from the same machine).
One Wierd behavior observed that once in while /employee/leaves works. I don't know how it happened.
I seacrhed a lot and spent around 2-3 days but did not get anything.
Any guidance will be great.
Thanks a lot in Advance
I've created backend for my mobile application with REST API and JWT authentication/authorization.
Then I created android application using Retrofit.
After retrieving JWToken from /login endpoint I've created GET request on a server-side to parse username of currently logged user with token, and then I call method on client-side.
UserController.java (server-side)
#RestController
#RequestMapping(path = "/user")
public class UserController {
private UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
#GetMapping
public String getCurrentUser(#AuthenticationPrincipal Object user) {
user = SecurityContextHolder.getContext().getAuthentication()
.getPrincipal();
return user.toString();
}
}
But I'm not sure if it's a right way to develop it like this.
Let's say I have two tables in my database.
One with login credentials that are being used in Authentication
Second with users personal data
and now I want to display First name and last name of a user.
Now, the only information after login I have is users username that he logged with and if I want to get more information I have to somehow make Queries on client side to:
first - get id of a user where username = username that I got from token
then - get object of users_data where user_id = id from the first query
and I don't think this process should be done on the client side(correct me if I'm wrong, please).
Question
So my question is what should I do fulfill this scenario where I want to get all information about user where I have only his username in client-side app. Should I make changes in my backend, or stick to making queries from mobile app?
(Server-side)
AuthenticationFilter.java
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
#Override
public Authentication attemptAuthentication(
HttpServletRequest request,
HttpServletResponse response
) throws AuthenticationException {
// Mapping credentials to loginviewmodel
LoginViewModel credentials = null;
try {
credentials = new ObjectMapper().readValue(request.getInputStream(), LoginViewModel.class);
} catch (IOException e) {
e.printStackTrace();
}
// Creating login token
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
credentials.getUsername(),
credentials.getPassword(),
new ArrayList<>()
);
// Authenticate user
Authentication auth = authenticationManager.authenticate(authenticationToken);
return auth;
}
#Override
protected void successfulAuthentication(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult
) throws IOException, ServletException {
// Grab current user
UserImpl principal = (UserImpl) authResult.getPrincipal();
// Create JWT Token
String token = JWT.create()
.withSubject(principal.getUsername())
.withExpiresAt(new Date(System.currentTimeMillis() + JwtProperties.EXPIRATION_TIME))
.sign(Algorithm.HMAC512(JwtProperties.SECRET.getBytes()));
// Add token in response(this is syntax of token)
response.addHeader(JwtProperties.HEADER_STRING, JwtProperties.TOKEN_PREFIX + token);
}
}
AuthorizationFilter.java
public class JwtAuthorizationFilter extends BasicAuthenticationFilter {
private UserRepository userRepository;
public JwtAuthorizationFilter(
AuthenticationManager authenticationManager,
UserRepository userRepository
) {
super(authenticationManager);
this.userRepository = userRepository;
}
#Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain
) throws IOException, ServletException {
// Read authorization header with JWT Token
String header = request.getHeader(JwtProperties.HEADER_STRING);
if (header == null || !header.startsWith(JwtProperties.TOKEN_PREFIX)) {
chain.doFilter(request, response);
}
// Try get user data from DB to authorize
Authentication authentication = getUsernamePasswordAuthentication(request);
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
}
private Authentication getUsernamePasswordAuthentication(HttpServletRequest request) {
String token = request.getHeader(JwtProperties.HEADER_STRING);
if (token != null) {
// parse and validate token
String username = JWT.require(Algorithm.HMAC512(JwtProperties.SECRET.getBytes()))
.build()
.verify(token.replace(JwtProperties.TOKEN_PREFIX, ""))
.getSubject();
if (username != null) {
User user = userRepository.findByUsername(username);
UserImpl principal = new UserImpl(user);
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(username, null, principal.getAuthorities());
return auth;
}
return null;
}
return null;
}
}
UserImpl.java
public class UserImpl implements UserDetails {
private User user;
public UserImpl(User user) {
this.user = user;
}
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> authorities = new ArrayList<>();
// Get list of roles (ROLE_name)
this.user.getRoleList().forEach( role -> {
GrantedAuthority authority = new SimpleGrantedAuthority("ROLE_" + role);
authorities.add(authority);
});
return authorities;
}
#Override
public String getPassword() {
return this.user.getPassword();
}
#Override
public String getUsername() {
return this.user.getUsername();
}
}
(Client-side)
Method for parsing username from currently logged in User:
public void getCurrentUser() {
Call<String> call = ApiClient.getUserService(getApplicationContext()).getCurrentUser();
call.enqueue(new Callback<String>() {
#Override
public void onResponse(Call<String> call, Response<String> response) {
if (response.isSuccessful()) {
String user = response.body();
nameOfUserView.setText(user);
}
}
#Override
public void onFailure(Call<String> call, Throwable t) {
nameOfUserView.setText(t.getMessage());
}
});
}
There is a loophole in your logic. Lets see
#1 - It is fine. Based on JWT Token, you are fetching username
#2 - Using token in header, you are fetching other details by sending username.
In #2, what if I send username of any other user instead of logged in user. System will still respond with the details. So, any logged in user will be able to see details of any user.
To handle this, you should use some DTO class which has all required fields say UserReturnData. Structure will be
public class UserReturnData
{
String username;
List<String> roles;
Long id;
//more fields as per requirement
}
Then in your current user call, populate this data based on authorisation header. Do not send any username. Authorisation header should be sufficient to fetch user details. Sample:
public UserReturnData fetchUserDetails()
{
UserReturnData userReturnData = new UserReturnData();
List<String> roles = new ArrayList<String>();
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
userReturnData.setUsername(auth.getName());
Long id = uRepo.findId(auth.getName());
userReturnData.setId(id);
Collection<SimpleGrantedAuthority> authorities = (Collection<SimpleGrantedAuthority>) SecurityContextHolder
.getContext().getAuthentication().getAuthorities();
for (SimpleGrantedAuthority authority : authorities)
{
roles.add(authority.getAuthority());
}
userReturnData.setRoles(roles);
//Populate other required fields here.
return userReturnData;
}
Whenever you need details of logged-in user. You can make call to current user API with only Authorization token and get logged in user information
I want to secure my API's based on the user role, but #PreAuthorize Annotations seem to not work properly. No matter what role the user has the server throws a 403 error. How to make this work ?
This is where I retrieve the User Details in my custom authentication provider:
#Override
protected UserDetails retrieveUser(String userName, UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken){
final String password = (String) usernamePasswordAuthenticationToken.getCredentials();
if (!StringUtils.hasText(password)) {
this.logger.warn("Username {}: no password provided", userName);
}
userName = parseCredentials(userName);
try {
DirContext ctx = ldapConfiguration.openConnection(userName, password);
ctx.close();
} catch (NamingException e) {
throw new LdapException("User not found in Active Directory", e);
} catch (NullPointerException e) {
throw new CredentialsNotProvidedException("Entered data may be null", e);
}
User user = userRepository.findOneByLogin(userName);
if (user == null) {
this.logger.warn("Username {}: user not found", userName);
throw new BadCredentialsException("Invalid Username/Password for user " + userName);
}
final List<GrantedAuthority> auths = new ArrayList<GrantedAuthority>();
GrantedAuthority r = (GrantedAuthority) () -> "ROLE_" + user.getRole().getName().toUpperCase();
auths.add(r);
// enabled, account not expired, credentials not expired, account not locked
UserDetails userDetails = new org.springframework.security.core.userdetails.User(userName, password, true, true, true, true, auths);
return userDetails;
}
This is the controller:
#PreAuthorize("hasRole('ROLE_HR')") //I don't have acces even if I am HR
#RestController
public class SettingsController {
#Autowired
private LocationRepository locationRepository;
#Autowired
private DepartmentRepository departmentRepository;
#Autowired
private RoleRepository roleRepository;
#RequestMapping(value = "/api/locations", method = RequestMethod.POST)
public ResponseEntity addLocation(#RequestBody Location location) {
if (location == null) {
return new ResponseEntity(HttpStatus.BAD_REQUEST);
}
locationRepository.save(new Location(location.getName()));
return new ResponseEntity(HttpStatus.CREATED);
}
#RequestMapping(value = "/api/roles", method = RequestMethod.POST)
public ResponseEntity addRole(#RequestBody Role role) {
if (role == null) {
return new ResponseEntity(HttpStatus.BAD_REQUEST);
}
roleRepository.save(new Role(role.getName()));
return new ResponseEntity(HttpStatus.CREATED);
}
#RequestMapping(value = "/api/departments", method = RequestMethod.POST)
public ResponseEntity addDepartment(#RequestBody Department department) {
if (department == null) {
return new ResponseEntity(HttpStatus.BAD_REQUEST);
}
departmentRepository.save(new Department(department.getName()));
return new ResponseEntity(HttpStatus.CREATED);
}
}
And security config:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
#Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private DatabaseAuthenticationProvider authenticationProvider;
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/js/**", "/css/**", "/img/**");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().loginPage("/login").failureUrl("/login").defaultSuccessUrl("/")
.and().logout().logoutSuccessUrl("/login")
.and().authorizeRequests().antMatchers("/login").permitAll()
/*.antMatchers("/settings.html").access("hasRole('HR')")
.antMatchers("/pendingRequests.html").access("hasRole('MANAGER')")
.antMatchers("/settings.html","/pendingRequests.html").access("hasRole('ADMIN')")*/
.anyRequest().authenticated().and().csrf().disable();
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider).eraseCredentials(false);
}
}
According to your commented line in security config class
.antMatchers("/settings.html").access("hasRole('HR')") user role is HR.
If role is HR then you should use
#PreAuthorize("hasRole('HR')")
and also #PreAuthorize should be placed first and then mention #RestController
#RestController
#PreAuthorize("hasRole('HR')")
public class SettingsController
I have made a user login interface where the user gets authenticated using spring security.
I have made an AuthenticationSuccessHandler which redirects the user to a new page.
I also want to implement a loginController in order to get the name of user logged in as well as displaying error messages for wrong credentials. Here is my Handler code :
public class MySimpleUrlAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
protected final Log logger = LogFactory.getLog(this.getClass());
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
protected MySimpleUrlAuthenticationSuccessHandler() {
super();
}
#Override
public void onAuthenticationSuccess(final HttpServletRequest request, final HttpServletResponse response, final Authentication authentication) throws IOException {
handle(request, response, authentication);
clearAuthenticationAttributes(request);
}
protected void handle(final HttpServletRequest request, final HttpServletResponse response, final Authentication authentication) throws IOException {
final String targetUrl = determineTargetUrl(authentication);
if (response.isCommitted()) {
logger.debug("Response has already been committed. Unable to redirect to " + targetUrl);
return;
}
redirectStrategy.sendRedirect(request, response, targetUrl);
}
protected String determineTargetUrl(final Authentication authentication) {
boolean isUser = false;
boolean isAdmin = false;
final Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (final GrantedAuthority grantedAuthority : authorities) {
if (grantedAuthority.getAuthority().equals("ROLE_USER")) {
isUser = true;
break;
} else if (grantedAuthority.getAuthority().equals("ROLE_ADMIN")) {
isAdmin = true;
break;
}
}
if (isUser) {
return "/static_htm.html";
} else if (isAdmin) {
return "/console.html";
} else {
throw new IllegalStateException();
}
}
And my controller code :
#Controller
public class HelloController {
#RequestMapping(value="/login", method = RequestMethod.GET)
public String printWelcome(ModelMap model, Principal principal ) {
String name = principal.getName();
model.addAttribute("username", name);
model.addAttribute("message", "Spring Security Hello World");
return "static_htm"; //page after successful login
}
#RequestMapping(value="/login", method = RequestMethod.GET)
public String login(ModelMap model) {
return "login"; //login page
}
#RequestMapping(value="/loginfailed", method = RequestMethod.GET)
public String loginerror(ModelMap model) {
//String errormessage = resources.getMessage("login.error", null, null);
model.addAttribute("error", "true");
return "login"; //login page
}
}
The handler works fine but I am not able to get the user name as well as the error message. What should I do to make both the handler and controller work together ?
Any suggestions are greatly appreciated.
From your question I presume you want to get the user name in the login controller.
If not so, feel free to disregard my answer.
You may have gotten it backward actually.
Success handler is somewhat like a custom implementation of "default-target-url".
So it is actually executed after login controller...
When login is successful, and there's no previously requested path (this is implemented by SavedRequestAwareAuthenticationSuccessHandler) then the request will be sent to the "default-target-url".
Or when there's a custom success handler, the success handler will determine the path it goes to.