How to get the list of users properly with Spring? - java

I want to get the list of all authenticated users.
I took the basic spring-security example from the official Spring site.
As it was recommended in other relative questions (51992610), I injected the DefaultSimpUserRegistry into the code.
Still, the list is empty.
#Configuration
public class UsersConfig {
final private SimpUserRegistry userRegistry = new DefaultSimpUserRegistry();
#Bean
#Primary
public SimpUserRegistry userRegistry() {
return userRegistry;
}
}
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/home").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll();
}
#Bean
#Override
public UserDetailsService userDetailsService() {
UserDetails user =
User.withDefaultPasswordEncoder()
.username("u")
.password("11")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
}
#RestController
public class WebSocketController {
#Autowired
private final SimpUserRegistry simpUserRegistry;
public WebSocketController(SimpUserRegistry simpUserRegistry) {
this.simpUserRegistry = simpUserRegistry;
}
#GetMapping("/users")
public String connectedEquipments() {
return this.simpUserRegistry
.getUsers()
.stream()
.map(SimpUser::getName)
.collect(Collectors.toList()).toString();
}
}
Build jar, launch locally, login, enter http://localhost:8080/users. Result:
[]
The full code may be taken from the Spring site.
The topics on SimpUserRegistry are so rare, I can't find a full example with it. The similar posts are unanswered yet (48804780, 58925128).
Sorry, I am new to Spring, is SimpUserRegistry the correct way to list users with Spring? If so, how to use it properly? Thanks.

Within your question, you're trying a few things:
You're setting up InMemoryUserDetailsManager with a list of allowed users (in your case a user called u).
You're using SimpUserRegistry to get a list of all connected users through Spring messaging (for example using WebSockets).
If you're just trying to get a list of all users, and you're not using WebSockets, then the second approach won't work.
If you're trying to get a list of all users that are stored within InMemoryUserDetailsManager, then the answer is that it's not possible to get that list. InMemoryUserDetailsManager uses an in-memory Map to store all users, and it doesn't expose that list.
If you really want such a list, you'll have to create a custom in-memory UserDetailsService, for example:
#Service
public class ListingInMemoryUserDetailsService implements UserDetailsService {
private final Map<String, InMemoryUser> users;
public ListingInMemoryUserDetailsService() {
this.users = new HashMap<>();
}
public ListingInMemoryUserDetailsService(UserDetails... userDetails) {
this.users = stream(userDetails)
.collect(Collectors.toMap(UserDetails::getUsername, InMemoryUser::new));
}
public Collection<String> getUsernames() {
return users
.values()
.stream()
.map(InMemoryUser::getUsername)
.collect(toList());
}
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return Optional
.ofNullable(users.get(username))
.orElseThrow(() -> new UsernameNotFoundException("User does not exist"));
}
}
In this example, InMemoryUser is an implementation of the UserDetails interface. When you create a custom implementation like that, you'll have to configure it with Spring Security:
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder);
}
Alternatively, if you're interested in retrieving a list of all created sessions, there's a better approach. First, you'll have to create a SessionRegistry bean:
#Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
Then, you'll have to configure Spring Security to set up sessions, and to use your SessionRegistry to do that:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/home").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login").permitAll()
.and()
.logout().permitAll()
// Add something like this:
.sessionManagement()
.maximumSessions(1)
.sessionRegistry(sessionRegistry);
}
After that, you can autowire SessionRegistry, and use the getAllPrincipals() method:
#GetMapping("/users")
public Collection<String> findUsers() {
return sessionRegistry
.getAllPrincipals()
.stream()
.map(this::getUsername)
.flatMap(Optional::stream)
.collect(toList());
}
private Optional<String> getUsername(Object principal) {
if (principal instanceof UserDetails) {
return Optional.ofNullable(((UserDetails) principal).getUsername());
} else {
return Optional.empty();
}
}
This will list all usernames of users that logged in within the application, and had a session. This also includes expired sessions, so you may want to filter on those as well.

At the time of Class Loading, your simpUserRegistry will be null. At starting, you will not get list of users. Instead of calling simpUserRegistry in connectedEquipments() method, call userRegistry() method by Using USersConfig class. Something like below code.
#GetMapping("/users")
public String connectedEquipments() {
return userRegistry()
.getUsers()
.stream()
.map(SimpUser::getName)
.collect(Collectors.toList()).toString();
}

Related

Java Spring Security login success at first and fail at second attempt

i am trying to create a java spring security website but i have troubles with the login. At the first login attempt everything is fine and i am redirected to the correct page. But when i close the browser window, go to the login page and try to log in with the same user again, i am redirected to the login page. I am not able to login with the same user again but am able to login with a different user. So i can always login with a specific user once, but not twice.
At the second login attempt the following error is shown:
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
Here is my SecurityConfiguration:
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
MySimpleUrlAuthenticationSuccessHandler successHandler;
#Override
public void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.permitAll()
.successHandler(successHandler)
.and()
.logout();
}
}
My SuccessHandler:
public class MySimpleUrlAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
Set<String> roles = AuthorityUtils.authorityListToSet(authentication.getAuthorities());
if (roles.contains("ROLE_ADMIN")) {
response.sendRedirect("consultantIndex.html");
}
if (roles.contains("ROLE_USER")) {
response.sendRedirect("index.html");
}
}
}
And my UserDetailsService:
#Service
public class MyUserDetailsService implements UserDetailsService {
private Map<String, User> roles = new HashMap<>();
#PostConstruct
public void init() {
roles.put("admin", new User("admin", "{noop}pass", getAuthority("ROLE_ADMIN")));
roles.put("user", new User("user", "{noop}pass", getAuthority("ROLE_USER")));
}
#Override
public UserDetails loadUserByUsername(String username) {
return roles.get(username);
}
private List<GrantedAuthority> getAuthority(String role) {
return Collections.singletonList(new SimpleGrantedAuthority(role));
}
}
Can someone please enlighten me why the id is "null" at the second login attempt with a specific user?
----Edit-----
I finally found the solution, i had to change the following lines of the class MyUserDetailsService to:
#Override
public UserDetails loadUserByUsername(String username) {
User user = roles.get(username);
return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), user.getAuthorities());
}
I don't know why specifically this happens only the second time, but in any case, you'll need to define a PasswordEncoder to let Spring Security know with what hashing function it needs to compare passwords.
The error message (java.lang.IllegalArgumentException: There is noPasswordEncodermapped for the id "null") is not really helpful, but is means you forgot to specify the PasswordEncoder.
If you are using BCrypt, for example, you'll need the following bean defined in your SecurityConfiguration.
#Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
You can find an even more detailed explanation here, in a blog post I wrote: https://www.marcobehler.com/guides/spring-security#_authentication_with_spring_security

Why is my oauth2 config not using my custom UserService?

I'm trying to use authentication by google. I am using springboot2, so most of the configuration is automatic. The authentication itself works good, but afterwards I would like to populate Principal with my own data (roles, username, and stuff).
I've created MyUserService that exteds DefaultOauth2UserService, and I am trying to use it as follows:
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
MyUserService myUserService;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2Login()
.userInfoEndpoint()
.userService(myUserService);
}
}
I've checked with debuger, that application never actually uses loadUser methods. And here is implementation of MyUserService:
#Component
public class MyUserService extends DefaultOAuth2UserService {
#Autowired
UserRepository userRepository;
public MyUserService(){
LoggerFactory.getLogger(MyUserService.class).info("initializing user service");
}
#Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
Map<String, Object> attributes = oAuth2User.getAttributes();
String emailFromGoogle = (String) attributes.get("email");
User user = userRepository.findByEmail(emailFromGoogle);
attributes.put("given_name", user.getFirstName());
attributes.put("family_name", user.getLastName());
Set<GrantedAuthority> authoritySet = new HashSet<>(oAuth2User.getAuthorities());
return new DefaultOAuth2User(authoritySet, attributes, "sub");
}
}
Actually the solution was just to add another property for google authentication:
spring.security.oauth2.client.registration.google.scope=profile email
Not sure, what is the default scope, and why entrance to the service is dependent on scope, but without this line the code never reached my custom service.
I think you're missing the #EnableOAuth2Client annotation at the top of your SecurityConfig class.
Regardless, I made an examplewith a Custom user service for oauth2 here https://github.com/TwinProduction/spring-security-oauth2-client-example/ if it helps

Define custom roles for SSO logged in users

I'm following the Spring SSO with Facebook tutorial and would like to define custom roles for the logged in users using an AuthentificationProvider but none of its methods are getting called during startup or runtime of the app.
Is there any simple way to extend this code to grant Facebook users custom roles?
#SpringBootApplication
#EnableOAuth2Sso
public class FbauthTestApplication extends WebSecurityConfigurerAdapter {
public static void main(String[] args) {
SpringApplication.run(FbauthTestApplication.class, args);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/**")
.authorizeRequests()
.antMatchers("/", "/login**", "/webjars/**")
.permitAll()
.and().authorizeRequests()
.antMatchers("/admin.html")
.hasRole("ADMIN")
.anyRequest()
.authenticated()
.and().logout().logoutSuccessUrl("/").permitAll()
.and().csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
#Autowired
MyAuthProvider myAuthProvider;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(myAuthProvider);
}
}
The #EnableOAuth2Sso annotation pulls the configuration from
ResourceServerTokenServicesConfiguration class, where you can inject an AuthoritiesExtractor.
This is the interface which decides on what roles a specific user has. If you're not using the authorization server it is an instance of FixedAuthoritiesExtractor.
#Component
public class MyAuthoritiesExtractor implements AuthoritiesExtractor {
#Autowired
private UserRepository userRepository;
#Override
public List<GrantedAuthority> extractAuthorities(Map<String, Object> map) {
String role = "ROLE_USER";
Optional<Long> principalId = getPrincipalId(map);
if (principalId.isPresent()) {
User user = userRepository.findOne(principalId.get());
role = (user != null && user.isAdmin()) ? "ROLE_ADMIN" : "ROLE_USER";
}
return Collections.singletonList(new SimpleGrantedAuthority(role));
}
private Optional<Long> getPrincipalId(Map<String, Object> map) {
try {
return Optional.of(Long.parseLong(map.getOrDefault("id", "").toString()));
} catch (Exception ex) {
// log
return Optional.empty();
}
}
}
Log in, register a user as admin, log out and re-login and then you have the ROLE_ADMIN.

How do you save users who have logged in with OAuth 2 (Spring)?

My main objective is to store the client-id of the each user, once they login with google. This github repo contains most of what I needed till now. The two main files of concern are OAuthSecurityConfig.java and UserRestController.java.
When I navigate to /user, the Principal contains all the details I need on the user. Thus I can use the following snippets to get the data I need:
Authentication a = SecurityContextHolder.getContext().getAuthentication();
String clientId = ((OAuth2Authentication) a).getOAuth2Request().getClientId();
I can then store the clientId in a repo
User user = new User(clientId);
userRepository.save(user);
The problem with this is that users do not have to navigate to /user. Thus, one can navigate to /score/user1 without being registered.
This API is meant to be a backend for an android application in the future, so a jquery redirect to /user would be insecure and would not work.
Things I have tried:
Attempt 1
I created the following class:
#Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
#Autowired
public CustomUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException(String.format("User %s does not exist!", username));
}
return new UserRepositoryUserDetails(user);
}
}
and overrode the WebSecurityConfigurerAdapterwith:
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserDetailsService);
}
Both overridden methods are not called when a user logs in (I checked with a System.out.println)
Attempt 2
I tried adding .userDetailsService(customUserDetailsService)
to:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
// Starts authorizing configurations.
.authorizeRequests()
// Do not require auth for the "/" and "/index.html" URLs
.antMatchers("/", "/**.html", "/**.js").permitAll()
// Authenticate all remaining URLs.
.anyRequest().fullyAuthenticated()
.and()
.userDetailsService(customUserDetailsService)
// Setting the logout URL "/logout" - default logout URL.
.logout()
// After successful logout the application will redirect to "/" path.
.logoutSuccessUrl("/")
.permitAll()
.and()
// Setting the filter for the URL "/google/login".
.addFilterAt(filter(), BasicAuthenticationFilter.class)
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
Both methods were still not called, and I don't feel like I am any closer to the solution. Any help will be greatly appreciated.
The way to go here is to provide a custom OidcUserService and override the loadUser() method because Google login is based on OpenId Connect.
First define a model class to hold the extracted data, something like this:
public class GoogleUserInfo {
private Map<String, Object> attributes;
public GoogleUserInfo(Map<String, Object> attributes) {
this.attributes = attributes;
}
public String getId() {
return (String) attributes.get("sub");
}
public String getName() {
return (String) attributes.get("name");
}
public String getEmail() {
return (String) attributes.get("email");
}
}
Then create the custom OidcUserService with the loadUser() method which first calls the provided framework implementiation and then add your own logic for persisting the user data you need, something like this:
#Service
public class CustomOidcUserService extends OidcUserService {
#Autowired
private UserRepository userRepository;
#Override
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
OidcUser oidcUser = super.loadUser(userRequest);
try {
return processOidcUser(userRequest, oidcUser);
} catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex.getCause());
}
}
private OidcUser processOidcUser(OidcUserRequest userRequest, OidcUser oidcUser) {
GoogleUserInfo googleUserInfo = new GoogleUserInfo(oidcUser.getAttributes());
// see what other data from userRequest or oidcUser you need
Optional<User> userOptional = userRepository.findByEmail(googleUserInfo.getEmail());
if (!userOptional.isPresent()) {
User user = new User();
user.setEmail(googleUserInfo.getEmail());
user.setName(googleUserInfo.getName());
// set other needed data
userRepository.save(user);
}
return oidcUser;
}
}
And register the custom OidcUserService in the security configuration class:
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private CustomOidcUserService customOidcUserService;
#Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2Login()
.userInfoEndpoint()
.oidcUserService(customOidcUserService);
}
}
Mode detailed explanation can be found in the documentation:
https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#oauth2login-advanced-oidc-user-service
In case of some one else is stuck with this, my solution was to create a custom class extending from
OAuth2ClientAuthenticationProcessingFilter and then override the successfulAuthentication method to get the user authentication details and save it to my database.
Example (kotlin):
On your ssoFilter method (if you followed this tutorial https://spring.io/guides/tutorials/spring-boot-oauth2) or wharever you used to register your ouath clients, change the use of
val googleFilter = Auth2ClientAuthenticationProcessingFilter("/login/google");
for your custom class
val googleFilter = CustomAuthProcessingFilter("login/google")
and of course declare the CustomAuthProcessingFilter class
class CustomAuthProcessingFilter(defaultFilterProcessesUrl: String?)
: OAuth2ClientAuthenticationProcessingFilter(defaultFilterProcessesUrl) {
override fun successfulAuthentication(request: HttpServletRequest?, response: HttpServletResponse?, chain: FilterChain?, authResult: Authentication?) {
super.successfulAuthentication(request, response, chain, authResult)
// Check if user is authenticated.
if (authResult === null || !authResult.isAuthenticated) {
return
}
// Use userDetails to grab the values you need like socialId, email, userName, etc...
val userDetails: LinkedHashMap<*, *> = userAuthentication.details as LinkedHashMap<*, *>
}
}
You can listen to AuthenticationSuccessEvent. For example:
#Bean
ApplicationListener<AuthenticationSuccessEvent> doSomething() {
return new ApplicationListener<AuthenticationSuccessEvent>() {
#Override
void onApplicationEvent(AuthenticationSuccessEvent event){
OAuth2Authentication authentication = (OAuth2Authentication) event.authentication;
// get required details from OAuth2Authentication instance and proceed further
}
};
}

Adding a #HandleBeforeSave method to my #RepositoryEventHandler class removes the underlying #Entiry from my REST API

Problem
I am using spring and in the process I have added a #RepositoryEventHandler(User.class) for updates (PUT) when I go to modify a user.
I would like to be able to set who is making the edits to the User.
I created a #HandleBeforeCreate which works fine for HTTP POST's but as soon as I add the #HandleBeforeSave the User REST API is no longer available. I do not see a stack trace being created.
Question
Am I missing something with regards to creating the #HandleBeforeSave
#RepositoryEventHandler
#Component
#RepositoryEventHandler(User.class)
public class SpringDataRestEventHandler {
private final UserRepository userRepository;
#Autowired
public SpringDataRestEventHandler(UserRepository userRepository) {
this.userRepository = userRepository;
}
#HandleBeforeCreate
public void applyUserInformationUsingSecurityContext(User user) throws {
String name = SecurityContextHolder.getContext().getAuthentication().getName();
User manager = this.userRepository.findByUserName(name);
if (!manager.hasRole("ROLE_MANAGER")) {
throw new Exception("No manager found for user on applyUserInformationUsingSecurityContext.");
}
user.setManager(name);
}
#HandleBeforeSave
public void applyManagerFromSecurityContext(User user) {
System.out.println("calling before save");
}
}
SecurityConfiguration
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private SpringDataJpaUserDetailsService userDetailsService;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(this.userDetailsService)
.passwordEncoder(MCBPasswordEncoder.PASSWORD_ENCODER);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/built/**", "/main.css").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.defaultSuccessUrl("/", true)
.permitAll()
.and()
.httpBasic()
.and()
.csrf().disable() // TODO enable for production
.logout()
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
.logoutSuccessUrl("/");
}
}
In the end the problem was actually related to the 2 repositories I created for the User #Entity. I was getting weird results where the API would show up (with the one repo) and disappear with the other repo.
I have since fixed this by
Use only one repo instead of two Extend Repository instead that
JPARepository
Copy and paste methods that i needed from PagingAndSortingRepository.
Added #PreAuthorize accordingly to specific methods, not to
the class. This was the initial problem as I split it out when I wanted to manipulate the repo outside of the REST api.

Categories

Resources