I have this vanilla spring boot/azure/starter app, connecting to our internal azure service.
https://learn.microsoft.com/de-de/azure/developer/java/spring-framework/configure-spring-boot-starter-java-app-with-azure-active-directory
Generally it works as designed.
What options do i have if i want to add custom roles for authorization?
I want that flow:
Login to azure with user/pw (works as expected)
Load user´s roles from a local database (postgres)
Inject/Add this roles into the list of spring's GrantedAuthority
With spring security we generally use a custom AuthenticationProvider
Currently i have this working code:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends AadWebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http
.authorizeHttpRequests()
.anyRequest()
.authenticated()
.and()
.oauth2Login();
}
}
I want something like this:
#Component
#RequiredArgsConstructor(onConstructor = #__(#Autowired))
#Slf4j
public class ThdAuthenticationProvider implements AuthenticationProvider {
private final
#NonNull
IApplicationUserService userService;
/**
* Performs authentication with the same contract as .
*
* #param authentication the authentication request object.
* #return a fully authenticated object including credentials. May return <code>null</code> if the
* <code>AuthenticationProvider</code> is unable to support authentication of the passed
* <code>Authentication</code> object. In such a case, the next <code>AuthenticationProvider</code> that
* supports the presented <code>Authentication</code> class will be tried.
* #throws AuthenticationException if authentication fails.
*/
#Override
public org.springframework.security.core.Authentication authenticate(org.springframework.security.core.Authentication
authentication)
throws AuthenticationException {
final String name = authentication.getName().toLowerCase();
final String password = authentication.getCredentials().toString();
// go to azure, login with name/password
// come back if sucessfull
List<String> roles = userService.fetchRoles(name);
List<GrantedAuthority> grantedAuth = new ArrayList<>();
grantedAuth.addAll(roles);
return new UsernamePasswordAuthenticationToken(name, password, grantedAuth);
}
EDIT
I ended up this way:
Based on this documentation: https://docs.spring.io/spring-security/site/docs/5.2.12.RELEASE/reference/html/oauth2.html#oauth2login-advanced-map-authorities-oauth2userservice
My custom user service - where the roles will be fetched from database or elsewhere:
#Service
public class UserService {
List<String> fetchUserRoles(String user){
return List.of("Administrator", "Product Owner", "Developer");
}
}
My custom security chain applying these roles:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends AadWebSecurityConfigurerAdapter {
private final UserService userService;
#Autowired
public SecurityConfiguration(UserService userService) {
this.userService = userService;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http
.authorizeHttpRequests()
.anyRequest()
.authenticated()
.and()
.oauth2Login()
.userInfoEndpoint(userInfoEndpointConfig -> {
userInfoEndpointConfig.oidcUserService(this.oidcUserService());
});
}
private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
final OidcUserService delegate = new OidcUserService();
return (userRequest) -> {
// Delegate to the default implementation for loading a user
OidcUser oidcUser = delegate.loadUser(userRequest);
OAuth2AccessToken accessToken = userRequest.getAccessToken();
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
// TODO
// 1) Fetch the authority information from the protected resource using accessToken
// 2) Map the authority information to one or more GrantedAuthority's and add it to mappedAuthorities
// 3) Create a copy of oidcUser but use the mappedAuthorities instead
List<String> dummy = userService.fetchUserRoles("dummy");
dummy.forEach(user -> mappedAuthorities.add((GrantedAuthority) () -> user));
oidcUser = new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo());
return oidcUser;
};
}
}
Spring Boot Azure AD custom roles
Please follow below link it has detail explanation about:
Register web API application and configure API scope
Assign these roles for the user
Register client application in Azure AD and configure API permissions
Reference:
Using Azure AD premium custom roles with spring security for role based access
#thomas-lang Thanks a lot Thomas!!! Your post helped me a lot!
Attaching my variation of the code
User Service
#Service
public class UserService {
private final PeopleService peopleService;
public UserService(PeopleService peopleService) {
this.peopleService = peopleService;
}
public Set<Role> fetchUserRoles(String user, String email){
Person loggedPerson = peopleService.findPersonByEmail(email);
return loggedPerson.getRoles();
}
}
Security Configuration
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfigurationAad extends AadWebSecurityConfigurerAdapter {
private final UserService userService;
#Autowired
public SecurityConfigurationAad(UserService userService) {
this.userService = userService;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http
.authorizeHttpRequests()
.anyRequest()
.authenticated()
.and()
.oauth2Login()
.userInfoEndpoint(userInfoEndpointConfig -> {
userInfoEndpointConfig.oidcUserService(this.oidcUserService());
});
}
private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
final OidcUserService delegate = new OidcUserService();
return (userRequest) -> {
// Delegate to the default implementation for loading a user
OidcUser oidcUser = delegate.loadUser(userRequest);
DecodedToken token = DecodedToken.getDecoded(userRequest.getAccessToken().getTokenValue());
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
// TODO
// 1) Fetch the authority information from the protected resource using accessToken
// 2) Map the authority information to one or more GrantedAuthority's and add it to mappedAuthorities
// 3) Create a copy of oidcUser but use the mappedAuthorities instead
Set<Role> dummy = userService.fetchUserRoles("dummy", token.unique_name);
dummy.forEach(user -> mappedAuthorities.add((GrantedAuthority) () -> String.valueOf(user)));
oidcUser = new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo());
return oidcUser;
};
}
}
DecodedToken implementation I got from here
https://www.lenar.io/how-to-decode-jwt-authentication-token/
Related
I have a Spring boot project which runs authentication with spring oauth2 token provider.
Now there is a idea to support a autentication with Keycloak, so that username and password will be stored in Keycloak and it will provide the access token.
Idea is to keep the oauth token store and provider, and as well to have a keycloak one, but to keep the roles and acces right part in spring. Keycloak will only be used for some users as a token provider instead of Spring one and to have a refresh token. So all the user data and access rights and roles are still be done by spring, from database, only the part where it authenticates username and password will be in Keycloak which provides a token.
#EnableWebSecurity
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private ClientDetailsService clientDetailsService;
private AccessDecisionManager accessDecisionManager;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().accessDecisionManager(accessDecisionManager)
.antMatchers("/service/*").fullyAuthenticated()
.anyRequest().permitAll().and().httpBasic().and().csrf().disable();
}
#Override
#Bean(name = "authenticationManagerBean")
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
#Autowired
public TokenStoreUserApprovalHandler userApprovalHandler(TokenStore tokenStore) {
TokenStoreUserApprovalHandler handler = new TokenStoreUserApprovalHandler();
handler.setTokenStore(tokenStore);
handler.setRequestFactory(new DefaultOAuth2RequestFactory(clientDetailsService));
handler.setClientDetailsService(clientDetailsService);
return handler;
}
#Bean
#Autowired
public ApprovalStore approvalStore(TokenStore tokenStore) throws Exception {
TokenApprovalStore store = new TokenApprovalStore();
store.setTokenStore(tokenStore);
return store;
}
#Bean
public AffirmativeBased accessDecisionManager() {
List<AccessDecisionVoter<?>> accessDecisionVoters = new ArrayList<>();
accessDecisionVoters.add(new ScopeVoter());
accessDecisionVoters.add(new RoleVoter());
accessDecisionVoters.add(new AuthenticatedVoter());
AffirmativeBased accessDecisionManager = new AffirmativeBased(accessDecisionVoters);
return accessDecisionManager;
}
And there is a Custom client service which grants access rights:
#Component
public class CustomClientService implements ClientDetailsService {
private static Map<String, BaseClientDetails> cache = new ConcurrentHashMap<>();
#Autowired
UserService userService;
#Autowired
AccessRightsService accessRightsService;
#Override
public ClientDetails loadClientByClientId(String paramString) throws ClientRegistrationException {
...
Also there is a custom TokenStore class:
public class MyTokenServices extends DefaultTokenServices {
private static Logger log = LoggerFactory.getLogger(MyTokenServices.class);
public UserService userService;
public AccessRightsService accessRightService;
private TokenStore my_tokenStore;
#Override
public void setTokenStore(TokenStore tokenStore) {
// TODO Auto-generated method stub
super.setTokenStore(tokenStore);
my_tokenStore = tokenStore;
}
#Override
#Transactional
public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
OAuth2AccessToken retVal= super.createAccessToken(authentication);
if(retVal instanceof DefaultOAuth2AccessToken) {
DefaultOAuth2AccessToken defRetVal = (DefaultOAuth2AccessToken)retVal;
log.info("New loging request"+ defRetVal.toString());
// defRetVal.setExpiration( Date.from(LocalDateTime.now().plus(8,ChronoUnit.HOURS).atZone(ZoneId.systemDefault()).toInstant()));
my_tokenStore.storeAccessToken(defRetVal, authentication);
}
return retVal;
}
#Override
public OAuth2Authentication loadAuthentication(String accessTokenValue)
throws AuthenticationException, InvalidTokenException {
OAuth2Authentication retVal = super.loadAuthentication(accessTokenValue);
OAuth2Request oldRequest = retVal.getOAuth2Request();
User user = userService.getUserByUsername(oldRequest.getClientId());
if(changeAutheticator(retVal, user)) {
HashSet<GrantedAuthority> authorities = new HashSet<>();
user.getRoles().forEach(a->authorities.add(new SimpleGrantedAuthority(a.getRoleName())));
Set<String> accessRights = accessRightService.getUserAccessRights(user);
if(accessRights != null) {
accessRights.forEach(rihgt->{
authorities.add(new SimpleGrantedAuthority(rihgt));
});
}
OAuth2Request newRequest = new OAuth2Request(retVal.getOAuth2Request().getRequestParameters(),
oldRequest.getClientId(), authorities, oldRequest.isApproved(), oldRequest.getScope(),
oldRequest.getResourceIds(), oldRequest.getRedirectUri(), oldRequest.getResponseTypes(), oldRequest.getExtensions());
retVal = new OAuth2Authentication(newRequest, retVal.getUserAuthentication());
}
return retVal;
}
/**
* Method that check do we need to change authenticator
* #param retVal
* #param user
* #return
*/
private boolean changeAutheticator(OAuth2Authentication auth, User user) {
if(user == null) return false;
if(user != null ) {
if(user.getRoles() != null) {
if(auth.getOAuth2Request()!=null && auth.getOAuth2Request().getAuthorities() != null){
for(Role role:user.getRoles()){
if(!auth.getOAuth2Request().getAuthorities().stream().anyMatch(a->a.getAuthority().equals(role.getRoleName()))){
return true;
}
}
for(GrantedAuthority ga : auth.getOAuth2Request().getAuthorities()) {
if(!user.getRoles().stream().anyMatch(a->a.getRoleName().equals(ga.getAuthority()))){
return true;
}
}
}
}
}
return false;
}
I was trying to implement a multiple authentiaction like the one from other stackoverflow but that was not a solution. Thinking that I should provide a custom authentication provider with Keycloak or still like not having a solution in head.
I use Keycloak to federate other identities: the only issuers that clients and resource-servers trust are Keycloak instances or realms. Other identity sources are hidden behind it.
With that config, roles are put into tokens by Keycloak, just as any other claims (for the last project I worked on, roles referential is LDAP but it could be a custom database table or Keycloak default one).
It's pretty easy to connect Keycloak to your user database and it comes with many features that I don't want to code and maintain (multi-factor authentication, social login, users, clients and resource-servers management screens,...)
I'm struggling with spring security after implemented oauth2 in my application. That's my first experience, but will be glad to hear nice or anger comments. That's the way to improve.
Problem:
During the logging in my application I see that I retrieve from google oidcuser object of DefaultOidcUser class. This object has collection of authorities with 4 items:
Honestly, I don't understand why I get ROLE_USER for this user in my system, cos actually it has ADMIN role in my system (role is created by me).
Generally I have 3 roles in my app: User, Manager, Admin. But can't understand how to correctly set particular role for special user.
Because of this, when I put smth like:
.antMatchers("/api/**").hasRole(Role.ADMIN.name())
.antMatchers("/administration/**").hasRole(Role.ADMIN.name())
I get 403 cos as I understand I get ROLE_USER for each user.
Can you please help me to set roles for each User but from my db (not this default ROLSER_USER from google)?
Also I read about GrantedAuthorities and mapper of grantedAuthorities, but it's a bit unclear for me.
I will be glad to hear any opinion. any useful link to improve my knowledge about this. Cos main goal: to understand how it works.
Other classes of my oauth2 implementation below:
#Getter
#Setter
#RequiredArgsConstructor
public class CustomOidcUser implements OidcUser {
private final OidcUser oidcUser;
private String email;
private String firstName;
private String lastName;
#Override
public Map<String, Object> getClaims() {
return oidcUser.getClaims();
}
#Override
public OidcUserInfo getUserInfo() {
return oidcUser.getUserInfo();
}
#Override
public OidcIdToken getIdToken() {
return oidcUser.getIdToken();
}
#Override
public Map<String, Object> getAttributes() {
return oidcUser.getAttributes();
}
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return oidcUser.getAuthorities();
}
#Override
public String getName() {
return oidcUser.getName();
}
}
#Service
#RequiredArgsConstructor
public class CustomOidcUserService extends OidcUserService {
private final UserRepository repository;
private static final String GOOGLE_KEY_LASTNAME = "family_name";
private static final String GOOGLE_KEY_FIRSTNAME = "given_name";
#Override
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
final OidcUser oidcUser = super.loadUser(userRequest);
CustomOidcUser newUser = new CustomOidcUser(oidcUser);
String email = oidcUser.getAttributes().get("email").toString();
User user = createUserIfNoExist(email, oidcUser);
newUser.setEmail(email);
newUser.setFirstName(user.getFirstName());
newUser.setLastName(user.getLastName());
return newUser;
}
public User createUserIfNoExist(String email, OidcUser oidcUser) {
return repository.findByEmail(email)
.orElseGet(() -> {
User newUser = new User();
newUser.setEmail(email);
newUser.setRole(Role.USER);
newUser.setStartWorkAt(LocalDate.now());
newUser.setLastLoginDate(LocalDateTime.now());
...
#Configuration
#EnableWebSecurity
#RequiredArgsConstructor
#EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final AuthProvider authProvider;
private final OnSuccessHandler onSuccessHandler;
private final CustomOidcUserService customOidcUserService;
private final UserService userService;
protected void configure(HttpSecurity http) throws Exception {
SavedRequestAwareAuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
successHandler.setUseReferer(true);
successHandler.setDefaultTargetUrl("/success");
successHandler.setAlwaysUseDefaultTargetUrl(true);
http
.addFilterAfter(new UserHasManagerFilter(userService), BasicAuthenticationFilter.class)
.cors()
.and()
.csrf()
.disable()
.authorizeRequests()
.antMatchers("/api/**").hasRole(Role.ADMIN.name())
.antMatchers("/administration/**").hasRole(Role.ADMIN.name())
.and()
.httpBasic()
.and()
.formLogin()
.loginPage("/unauthorized")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/success")
.failureUrl("/failed")
.successHandler(successHandler)
.usernameParameter("email")
.passwordParameter("password")
.permitAll()
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/logout_success")
.permitAll()
.and()
.oauth2Login()
.loginPage("/")
.userInfoEndpoint(userInfoEndpoint ->
userInfoEndpoint.oidcUserService(customOidcUserService)
.customUserType(CustomOidcUser.class, "google")
// .userAuthoritiesMapper(userAuthoritiesMapper())
)
.defaultSuccessUrl("/dashboard", true)
.successHandler(onSuccessHandler)
.failureHandler(authenticationFailureHandler())
.permitAll()
.and()
.authorizeRequests()
.antMatchers("/**")
.permitAll();
}
For OIDC by default Spring will map scopes and the default USER role to granted Authorities on OidcUser object.
Your simply returning this in your CustomOidcUser
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return oidcUser.getAuthorities();
}
You need to create a custom GrantedAuthorities collection in your CustomOidcUser object, and then your CustomOidcUserService you need to add the roles from your database to this collection manually.
Collection<GrantedAuthority> authorities ....
authorities.add(new SimpleGrantedAuthority("ROLE_...."));
I am trying to implement a UserCache in my application to avoid to make multiple calls to the User table in the case I am using the basic authentication. I created my CacheConfig following the accepted answer of this topic, in which the CachingUserDetailsService is used to manage the user cache. Bellow is the code of the UserService, CacheConfig and SecurityConfig:
public class UserService implements UserDetailsService {
private final UserRepository userRepository;
public UserService(UserRepository repository) {
this.userRepository = repository;
}
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
AddInUser user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("O usuário " + username + " não pode ser encontrado!"));
UserDetails userDetails = User
.builder()
.username(user.getUsername())
.password(user.getPassword())
.roles("USER")
.build();
return userDetails;
}
#Transactional
public AddInUser save(AddInUser user) {
return userRepository.save(user);
}
}
#EnableCaching
#Configuration
public class CacheConfig {
public static final String USER_CACHE = "userCache";
/**
* Define cache strategy
*
* #return CacheManager
*/
#Bean
public CacheManager cacheManager() {
SimpleCacheManager simpleCacheManager = new SimpleCacheManager();
List<Cache> caches = new ArrayList<>();
//Failure after 5 minutes of caching
caches.add(new GuavaCache(CacheConfig.USER_CACHE,
CacheBuilder.newBuilder().expireAfterAccess(5, TimeUnit.MINUTES).build()));
simpleCacheManager.setCaches(caches);
return simpleCacheManager;
}
#Bean
public UserCache userCache() throws Exception {
Cache cache = cacheManager().getCache("userCache");
return new SpringCacheBasedUserCache(cache);
}
}
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
};
#Autowired
private UserRepository userRepository;
#Autowired
private UserCache userCache;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService())
.passwordEncoder(passwordEncoder());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.configurationSource(request -> new CorsConfiguration().applyPermitDefaultValues())
.and()
.authorizeRequests()
.antMatchers("/api/**")
.authenticated()
.and()
.httpBasic();
}
#Override
#Bean
public UserDetailsService userDetailsService() {
UserService userService = new UserService(userRepository);
CachingUserDetailsService cachingUserService = new CachingUserDetailsService(userService);
cachingUserService.setUserCache(this.userCache);
return cachingUserService;
}
}
The first call works well because it makes the call to the UserRepository. But on the second, it does not make the call to the repository (as expected) but I am getting the following WARN from BCryptPasswordEncoder:
2020-09-24 08:43:51.327 WARN 24624 --- [nio-8081-exec-4] o.s.s.c.bcrypt.BCryptPasswordEncoder : Empty encoded password
The warning is clear in its meaning and it fails to authenticate de user because of the null password. But I cannot understand why the user retried from cache has a null password if it was correctly stored. I am not sure how to solve it using the cache. Any thoughts?
Thank you very much for your help!
#M.Deinum comment is absolutely correct. You can refer to the doc here.
Note that this implementation is not immutable. It implements the
CredentialsContainer interface, in order to allow the password to be
erased after authentication. This may cause side-effects if you are
storing instances in-memory and reusing them. If so, make sure you
return a copy from your UserDetailsService each time it is invoked.
You can check the Spring-security source code if you are curious more:
private boolean eraseCredentialsAfterAuthentication = true;
...
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
// If the parent AuthenticationManager was attempted and successful then it
// will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent
// AuthenticationManager already published it
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
And User.java source code:
#Override
public void eraseCredentials() {
this.password = null;
}
By the way, it looks weird to cache login user that way. During login, it is better to get fresh record from DB instead of from cache. You can use cached user at other place but seldom see it is used during login.
If you really need to do that, you can change the default flag to false as mentioned in doc, just inject AuthenticationManager and call:
setEraseCredentialsAfterAuthentication(false)
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();
}
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
}
};
}