There is a controller whose task is to return the user profile to me using the REST API. Code further:
#PostMapping("/me")
public UserProfileResponse getUserProfile(#AuthenticationPrincipal UserAuthenticationPrincipalModel user ) {
return userProfileService.getUserProfile(user.getUserId());
}
I created a model for the User entity. The entity class is created as:
public class User implements UserDetails { ... }
The model has the following structure:
public class UserAuthenticationPrincipalModel extends User {
private String userId;
private String avatarUrl;
public UserAuthenticationPrincipalModel(***.********.entity.User user) {
super(user.getUsername(), user.getPassword(), user.isEnabled(), user.isAccountNonExpired(),
user.isCredentialsNonExpired(), user.isAccountNonLocked(), user.getAuthorities());
this.userId = user.getUserId();
this.avatarUrl = user.getUserPic();
}
// + equals and hashCode
}
In the model, the data that I will ever (or so far plan so) to pull from the AuthPrincipal an authorized user. According to the statement of work, I can’t use the default Principal, I haven’t even tried it. Implementation of UserDetailsService:
#Service
public class UserDetailsServiceImpl extends AbstractMySQLService<User, String, UserRepository> implements UserDetailsService {
private final UserRepository userRepository;
public UserDetailsServiceImpl(final UserRepository userRepository, final UserRepository repository) {
super(repository);
this.userRepository = userRepository;
}
#Override
public UserAuthenticationPrincipalModel loadUserByUsername(final String email) {
User user = userRepository.findByEmail(email);
if (user == null) {
throw new UsernameNotFoundException("Invalid username or user not e: " + email);
}
return new UserAuthenticationPrincipalModel(user);
}
}
Error: Null always flies into methods. Made a lot of additions, which are recommended on the Baeldang and this stack - nothing :(
Please, write a comment, if I should add some more information.
UPD 1:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/ca/**").hasRole("COMPANY_ADMIN")
.antMatchers("/d/**").hasRole("DRIVER")
.antMatchers("/u/**").authenticated()
.antMatchers("/sign_up", "/oauth/token", "/swagger-ui.html", "/resources").permitAll();
}
I can give you a few pointers of how to approach this issue.
Ensure you are using org.springframework.security.core.annotation.AuthenticationPrincipal instead of #org.springframework.security.web.bind.annotation.AuthenticationPrincipal (Both should work but just pre-caution because the later is deprecated)
Now the issue is to isolate the problem to ONE of the following areas so you can concentrate there:
Your UserDetailsServiceImpl is not used
Something wrong with getUserProfile method with #AuthenticationPrincipal
user is not associated with a logged in session.
To identify that, replace your public UserProfileResponse getUserProfile method with the following:
[Do not change anything else]
#Autowired
private UserDetailsService userDetailsService;
#PostMapping("/me")
public void getUserProfile(#AuthenticationPrincipal UserDetails user ) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
System.out.println("principal : " + authentication.getPrincipal());
System.out.println("Implementing class of UserDetails: " + authentication.getPrincipal().getClass());
System.out.println("Implementing class of UserDetailsService: " + userDetailsService.getClass());
}
Check the logs and it will tell you where the problem is and if you can't figure out from it, you can post the outcome here for more help
Update: Answers for point 4 given as below in comments.
principal : anonymousUser
Implementing class of UserDetails : class java.lang.String
Implementing class of UserDetailsService : class
Conclusion : endpoint is not protected and user accessing without logging in
Solution : Protect the endpoint by replacing .antMatchers("/u/**").authenticated() with .antMatchers("/api/u/**").authenticated()
Related
I'm working on a spring boot CRUD RESTful API with an User entity that consists of two parameters : name and id. My test framework is JUnit.
The problem i'm facing is that i don't know how to treat a throwable UserNotFound exception on my services unit tests.
I have possible "User not found by {id}" exceptions in my "List user by id", "Delete user by id" and "Update user by id" as you can see (i'll only list two endpoints to make this shorter) :
#Service
public class DeleteUserService {
#Autowired
UserRepository repository;
public void deleteUser(Long id) {
Optional<User> userOptional = repository.findById(id);
if (!userOptional.isPresent()) {
throw new UserNotFoundException(id);
} else {
repository.deleteById(id);
}
}
}
#Service
public class DetailUserService {
#Autowired
UserRepository repository;
public Optional<User> listUser(Long id) {
Optional<User> user = repository.findById(id);
if (!user.isPresent()) {
throw new UserNotFoundException(id);
} else {
return repository.findById(id);
}
}
}
Nothing wrong so far, my endpoints are working fine.
The UserNotFound code is :
#ControllerAdvice
public class UserNotFoundAdvice {
#ResponseBody
#ExceptionHandler(UserNotFoundException.class)
#ResponseStatus(HttpStatus.NOT_FOUND)
String userNotFoundHandler(UserNotFoundException ex) {
return ex.getMessage();
}
}
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(Long id) {
super("Could not find user with id " + id + ".");
}
}
The unit tests (the main reason this is being written) :
#RunWith(MockitoJUnitRunner.class)
public class DeleteUserServiceTest {
#Mock
private UserRepository userRepository;
#InjectMocks
private DeleteUserService deleteUserService;
#Test
public void whenGivenId_shouldDeleteUser_ifFound(){
User user = new User();
user.setId(89L);
deleteUserService.deleteUser(user.getId());
verify(userRepository).deleteById(user.getId());
}
}
#RunWith(MockitoJUnitRunner.class)
public class DetailUserServiceTest {
#Mock
private UserRepository userRepository;
#InjectMocks
private DetailUserService detailUserService;
#Test
public void whenGivenId_shouldReturnUser_ifFound() {
User user = new User();
user.setId(89L);
Optional<User> userMock = Optional.of(user);
when(userRepository.findById(user.getId())).thenReturn(userMock);
Optional<User> expected = detailUserService.listUser(user.getId());
assertThat(expected).isSameAs(userMock);
verify(userRepository).findById(user.getId());
}
}
As you can see, there's something missing in these unit tests code which is the behavior of the UserNotFound. Perhaps it is not properly mocked or something else's missing in the unit tests code??
Would really appreciate if someone could help me with this one! Sorry if the post's too long, i tried my best to explain it!
If I understand you right you need to test the behavior when the user is not found and you throw an exception.
Here is the link about how to test exception: https://www.baeldung.com/junit-assert-exception
And also additionally you can verify that delete by id or find by id weren't invoked:
verify(userRepository, never()).findById(user.getId());
or
verify(userRepository, Mockito.times(0)).findById(user.getId());
and for the deleteById the same
To test that exception handlers were invoked and worked correctly you need integration tests.
I am trying to create a website that allows the user to update, edit, delete, etc., and I have got to the part of Updating or Editing user’s information. I have tried multiple times using different ways, but I cannot seem to get past the error. I am completely oblivious to Optional<> I just don’t get it. I have read https://docs.oracle.com/javase/8/docs/api/java/util/Optional.html, but i dont understand how it should be coded, its just not clicking. If someone could please inform on how it should be coded in my code and please explain it I would be so grateful. Maybe its because im overworked and have not slept, but i cannot seem to correct this error. This is the error i get on the page when i attempt to edit the information for a user:
There was an unexpected error (type=Internal Server Error, status=500).
For input string: "id"
java.lang.NumberFormatException: For input string: id
//Repository
public interface UserRepository extends CrudRepository<User, Integer> {
}
here is the UserService
//UserService
#Service
#Transactional
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository=userRepository;
}
public void saveMyUser(User user) {
userRepository.save(user);
}
public List<User> showAllUsers(){
List<User> users = new ArrayList<User>();
for(User user: userRepository.findAll()) {
users.add(user);
}
return users;
}
public void deleteMyUser(int id) {
userRepository.deleteById(id);
}
public User editUser (int id) {
return userRepository.findById(id);//I also get an error here as well
}
}
here is the controller
//Application Controller
#Controller
public class ApplicationController {
#Autowired
private UserService userService;
// THIS IS WHERE I GET THE ERROR
#RequestMapping("/edit-user")
public String editUser(#RequestParam int id,HttpServletRequest request) {
/* OPTIONAL<?> is this where i would implement the
optional what do i have to put here exactly?
I tried some ways I read about but its not working for me */
request.setAttribute("user", userService.editUser(id));
request.setAttribute("mode", "MODE_UPDATE");
return "welcome";
}
}
Thank you for the help in advance Im a little frustrated with this because I have been trying to correct this error all night.
There are several ways to convert from an option to an entity. You can use the following:
Use get() method:
public User editUser (int id) {
return userRepository.findById(id).get();
}
Use orElse method:
public User editUser (int id) {
/* new User() is stab if user was not found */
return userRepository.findById(id).orElse(new User());
}
Use orElseThrowMethod:
public User editUser (int id) {
/* Throw exception if user was not found*/
return userRepository.findById(id).orElseThrow(IllegalArgumentException::new));
}
As for controller it will be like this:
#RequestMapping("/edit-user")
public String editUser(#RequestParam int id,HttpServletRequest request) {
User user = userService.editUser(id);
request.setAttribute("user", user);
request.setAttribute("mode", "MODE_UPDATE");
return "welcome";
}
Also there similar question for your topic:
Spring Boot. how to Pass Optional<> to an Entity Class
In JSP I can get username by ${pageContext.request.remoteUser}. But there is also additional info (rating of user) I need to display on every page of my site. How can I access it, considering there is a #Service to get it by username?
For what it's worth I use custom authentication provider:
#Service
public class MyUserDetailsService implements UserDetailsService {
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
return new User(s, "password", Arrays.asList(new SimpleGrantedAuthority("ROLE_USER")));
}
}
<security:authentication-manager>
<security:authentication-provider user-service-ref='myUserDetailsService'/>
</security:authentication-manager>
You could create implementation of AuthenticationSuccessHandler and set an attribute there:
#Component
public class AuthenticationSuccessHandlerImpl implements AuthenticationSuccessHandler {
Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
request.getSession().setAttribute("someDetail", "detailsValue");
response.sendRedirect("/to-whatever-url-you-want")
}
}
Upon successful login, someDetail attribute will be set. Note that you can also obtain currently logged in user from Authentication instance and perform some logic.
You can create a custom UserDetails class (e.g. MyUserDetails) and save the extra information there. In your UserDetailsService, just return this MyUserDetails instead of the normal UserDetail.
public class MyUserDetails extends UserDetail {
private int rating;
... // other properties
... // getter setter
}
#Service
public class MyUserDetailsService implements UserDetailsService {
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
return new MyUserDetails(...);
}
}
In every controller, you can call
(MyUserDetails)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
to the get the current principal/UserDetails, which contains your extra info(e.g. rating of the user).
P.s. If this extra info is related to users, sessions are not the right place to store it, because sessions may expire after closing the browser. If this extra info is just some temporary data, then #Branislav Lazic's answer is correct. Since I can't add a comment, so I have to write the comments to #Branislav Lazic's answer here.
I am pretty new in Spring Security and I have the following problem.
I am working on a Spring Boot prject using Spring Security to protect all the resources into /Extranet/".
So basically my WebSecurityConfig configuration class contains this code:
#Configuration
#EnableWebSecurity
#ComponentScan(basePackageClasses = CustomUserDetailsService.class)
#EnableAutoConfiguration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private UserDetailsService userDetailsService;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/Extranet/**").access("hasRole('ROLE_USER')")
.anyRequest().permitAll()
.and()
.httpBasic()
.and()
.csrf().disable();
}
}
It works fine, infact to access to a resource as /Extranet/login I have to set the Basic Authentication and specify the correct username and password (performing the request using Postman tool to test it).
Ok, this works fine.
In my Spring Security configuration is involed this CustomUserDetails class that implements the Spring Security interface UserDetails.
public class CustomUserDetails extends User implements UserDetails {
private static final long serialVersionUID = 1L;
public CustomUserDetails(User user){
super(user);
}
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
for(UserRole role : this.getUserRoles() ){
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(role.getName());
authorities.add(grantedAuthority);
}
return authorities;
}
#Override
public boolean isAccountNonExpired() {
return true;
}
#Override
public boolean isAccountNonLocked() {
return true;
}
#Override
public boolean isCredentialsNonExpired() {
return true;
}
#Override
public boolean isEnabled() {
return true;
}
#Override
public String getUsername() {
return super.getUsername();
}
}
An instance of this object contains the user details of the user currently logged.
Ok my doubt is: how can I retrieve this object from a controller method? (I think that is should be into the context and that I can retrieve it in some way).
I have tryied to do in this way:
#RestController
#RequestMapping("/Extranet")
public class AccessController {
#RequestMapping(value = "/login", method = RequestMethod.POST)
public ResponseEntity<String> login(CustomUserDetails userInfo) {
System.out.println("login() START");
return ResponseEntity.ok("LOGGED IN");
}
}
but in this way I obtain an exception like this:
[ERROR] 2017-01-23 14:18:04 [com.betrivius.controller.exceptionHandler.ControllerExceptionHandler.handleException(ControllerExceptionHandler.java:106)] [http-nio-8080-exec-1] ControllerExceptionHandler - Request: http://localhost:8080/Extranet/login throws:
org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.betrivius.security.bean.CustomUserDetails]: No default constructor found; nested exception is java.lang.NoSuchMethodException: com.betrivius.security.bean.CustomUserDetails.<init>()
at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:105) ~[spring-beans-4.3.3.RELEASE.jar:4.3.3.RELEASE]
It seems that it can't instantialete the CustomUserDetails object. Why?
From what I know I can retrieve the CustomUserDetails object related to the logged user. How can I do it?
I also try to do in this way:
#RequestMapping(value = "/login", method = RequestMethod.POST)
public ResponseEntity<String> login(Principal principal) {
System.out.println("login() START");
return ResponseEntity.ok("LOGGED IN");
}
In this way the principal parameter is correctly instantiated and contains an instance of the previous CustomUserDetails object containing the information of the logged user.
So is it the correct way to access to the current logged user information?
Another doubt is: Why I can pass this Principal principal parameter to my login() method? What exactly happen under the hood? Who is that effectively pass it to the login() method?
I think that should happen something like this:
1) There is a POST HttpRequest toward the"/Extranet/login" resource. The dispatcher servlet send it to the login() method.
2) The Principal principal was put into the Spring Context after that the user was enabled for this resource (before that the controller method was called) so the Spring factory can retrieve it from the context and pass it to the login() method.
But I am absolutly not sure about it. How exactly works? What am I missing?
You probably need then #AuthenticationPrincipal annotation:
#RequestMapping(value = "/login", method = RequestMethod.POST)
public ResponseEntity<String> login(#AuthenticationPrincipal CustomUserDetails userInfo) {
System.out.println("login() START");
return ResponseEntity.ok("LOGGED IN");
}
If that still doesn't solve the problem, try debugging using the old method:
#RequestMapping(value = "/login", method = RequestMethod.POST)
public ResponseEntity<String> login() {
CustomUserDetails userInfo = (CustomerUserDetails)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
...
}
I've configured the Spring Boot Security as per:
https://spring.io/guides/gs/securing-web/
I am able to login using my credentials perfectly. However, I need to add a checking that the AD user must also belong to a specific AD group (ie. AD-this-is-a-specific-group). On login, if the user does not belong to the specific AD group, then it should return a login error.
I've been searching for hours now and cannot seem to find a clear way to do this in the WebSecurityConfigurerAdapter , am I using the auth.groupSearchFilter correctly?
Here is my code:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
Environment env;
public LdapContextSource contextSource () {
LdapContextSource contextSource= new LdapContextSource();
contextSource.setUrl(env.getRequiredProperty("ldap.url"));
contextSource.setBase(env.getRequiredProperty("ldap.baseDn"));
contextSource.setUserDn(env.getRequiredProperty("ldap.bindDn"));
contextSource.setPassword(env.getRequiredProperty("ldap.batchPassword"));
contextSource.afterPropertiesSet();
return contextSource;
}
#Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth.ldapAuthentication()
.userSearchFilter("(cn={0})")
.groupSearchBase("OU=Account Groups,OU=ITS Security")
.groupSearchFilter("(cn=AD-this-is-a-specific-group)")
.contextSource(contextSource());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().fullyAuthenticated()
.and()
.formLogin();
}
I am sorry for beeing 5 years late for the party but I had the exact same problem with my very simple LDAP authentication implemented in Spring Boot.
I only wanted this:
- Is it the correct username?
- Is it the correct password?
- If yes, is the usr in group MYGROUP?
So my configure method now looks really small. I added the populator in a separate bean, just realize that I needed to add it in "auth.ldapAuthentication" so it would be called.
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.ldapAuthentication()
.userSearchFilter("uid={0}")
.ldapAuthoritiesPopulator(ldapAuthoritiesPopulator())
.groupSearchFilter("(member={0})")
.contextSource(contextSource());
}
#Bean
public LdapAuthoritiesPopulator ldapAuthoritiesPopulator() {
DefaultLdapAuthoritiesPopulator populi = new DefaultLdapAuthoritiesPopulator(contextSource(), "") {
#Override
public Set<GrantedAuthority> getGroupMembershipRoles(String userDn, String username) {
Set<GrantedAuthority> groupMembershipRoles = super.getGroupMembershipRoles(userDn, username);
boolean isMemberOfSpecificAdGroup = false;
for (GrantedAuthority grantedAuthority : groupMembershipRoles) {
if ("ROLE_MYGROUP".equals(grantedAuthority.toString())) {
isMemberOfSpecificAdGroup = true;
break;
}
}
if (!isMemberOfSpecificAdGroup) {
throw new BadCredentialsException("User must be a member of " + "ROLE_MYGROUP");
}
return groupMembershipRoles;
}
};
return populi;
}
#Bean
public DefaultSpringSecurityContextSource contextSource() {
return new DefaultSpringSecurityContextSource("ldap://blabla-some-url:389/dc=something,dc=something,dc=ch");
}
And by the way: The url did not work like mentioned in the Spring Boot guide it only worked like this, like everything in one line:
return new DefaultSpringSecurityContextSource("ldap://blabla-some-url:389/dc=something,dc=something,dc=ch");
And by the way for everyone following that guide: If you connect to an already existing LDAP server you don't need all those "spring.ldap.embedded" application properties.
So thank you alot for your help!
Not sure if this is the best way to do this (in terms of Spring Security's lifecycle), but basically I provided my own DefaultLdapAuthoritiesPopulator, where I only override the getGroupMembershipRoles.
First thing though, I have wrong auth.groupSearchFilter above, it should be:
.groupSearchFilter("(member={0})")
Second, I've created an anonymous class with overridden method (that calls the super and checks for a the membership in the list of roles):
auth
.ldapAuthentication()
.ldapAuthoritiesPopulator(new DefaultLdapAuthoritiesPopulator(contextSource, "OU=Account Groups,OU=ITS Security") {
#Override
public Set<GrantedAuthority> getGroupMembershipRoles(String userDn, String username) {
Set<GrantedAuthority> groupMembershipRoles = super.getGroupMembershipRoles(userDn, username);
boolean isMemberOfSpecificAdGroup = false;
for (GrantedAuthority grantedAuthority : groupMembershipRoles) {
if ("ROLE_AD-this-is-a-specific-group".equals(grantedAuthority.toString())) {
isMemberOfSpecificAdGroup = true;
break;
}
}
if (!isMemberOfSpecificAdGroup ) {
throw new BadCredentialsException("User must be a member of " + "AD-this-is-a-specific-group");
}
return groupMembershipRoles;
}
})
.userSearchFilter("(cn={0})")
.groupSearchBase("OU=Account Groups,OU=ITS Security")
.groupSearchFilter("(member={0})")
.contextSource(contextSource);
I'll put this here since I think it's the easier way without overriding any method.
In user search filter (i'll use yours) add the following if it corresponds to your LDAP structure
Original:
.userSearchFilter("(cn={0})")
Modified to search roles:
.userSearchFilter("(&(cn={0})(memberOf=CN=MYGROUP,OU=GROUP,DC=com,DC=company)")
This searches both the user and the membership
In my case I had to do this because I have 3 possible roles:
(&(cn={0})(|(group1)(group2)(group3)))
As you can see it searches user AND 1 OR more roles
Credit to this question's answer: Spring Security Ldap, log in only users in specified group