In configure() I have:
...
.oauth2Login()
.userInfoEndpoint()
.oidcUserService(oidcUserService);
My CustomIodcUserService class:
#Service
public class CustomOidcUserService extends OidcUserService {
#Autowired
private UserRepository userRepository;
#Override
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
OidcUser oidcUser = super.loadUser(userRequest);
Map attributes = oidcUser.getAttributes();
GoogleOAuth2UserInfo userInfo = new GoogleOAuth2UserInfo(attributes);
updateUser(userInfo);
return oidcUser;
}
private void updateUser(GoogleOAuth2UserInfo userInfo) {
User user = userRepository.findByEmail(userInfo.getEmail()).get();
if(user == null) {
user = new User();
}
user.setEmail(userInfo.getEmail());
user.setName(userInfo.getName());
System.out.println(userRepository.save(user));
}
}
But methods of this class are never called
Has anyone met this or how can I solve this or get UserInfo differently?
https://www.callicoder.com/spring-boot-security-oauth2-social-login-part-2/
This article describes how to do this, only there they used
userInfoEndpoint ()
.userService (customOAuth2UserService)
Related
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/
I get the following error whilst trying to run the application. Any help is appreciated.
"Error starting Tomcat context. Exception: org.springframework.beans.factory.UnsatisfiedDependencyException. Message: Error creating bean with name 'webSecurityConfig': Unsatisfied dependency expressed through field 'userServiceImpl'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'userServiceImpl': Unsatisfied dependency expressed through field 'bCryptEncoder'; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'webSecurityConfig': Requested bean is currently in creation: Is there an unresolvable circular reference?"
2 related files for the issue are as follows:
WebSecurityConfig
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private UserServiceImpl userServiceImpl;
private JwtTokenUtil jwtTokenUtil;
private AuthEntryPoint unauthorizedHandler;
public WebSecurityConfig(AuthEntryPoint unauthorizedHandler, JwtTokenUtil jwtTokenUtil) {
this.unauthorizedHandler = unauthorizedHandler;
this.jwtTokenUtil = jwtTokenUtil;
}
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Autowired
public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userServiceImpl)
.passwordEncoder(encoder());
}
#Bean
public AuthenticationFilter authenticationTokenFilterBean() throws Exception {
return new AuthenticationFilter(userServiceImpl, jwtTokenUtil);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.and()
.csrf()
.disable()
.authorizeRequests()
.antMatchers(
"/auth/*",
"/token/*",
"/webjars/**",
"/",
"/uploads/**",
"favicon.ico"
).permitAll()
.anyRequest().authenticated()
.and()
.exceptionHandling()
.authenticationEntryPoint(unauthorizedHandler)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
}
#Bean
public BCryptPasswordEncoder encoder() {
return new BCryptPasswordEncoder();
}
}
UserServiceImpl
#Service
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
#Autowired
private BCryptPasswordEncoder bCryptEncoder; // Fails when injected by the constructor.
public UserServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}
/**
* Create a new user.
* #param createUserDto
* #return
*/
#Override
public User save(CreateUserDto createUserDto) {
User newUser = new User();
newUser.setEmail(createUserDto.getEmail());
newUser.setFullName(createUserDto.getFullName());
newUser.setPassword(bCryptEncoder.encode(createUserDto.getPassword()));
newUser.setConfirmed(createUserDto.isConfirmed());
newUser.setEnabled(createUserDto.isEnabled());
newUser.setRole(createUserDto.getRole());
return userRepository.save(newUser);
}
#Override
public List<User> findAll() {
List<User> list = new ArrayList<>();
userRepository.findAll().iterator().forEachRemaining(list::add);
return list;
}
#Override
public void delete(String id) {
userRepository.deleteById(id);
}
#Override
public User findByEmail(String email) throws ResourceNotFoundException {
Optional<User> optionalUser = userRepository.findByEmail(email);
if (optionalUser.isEmpty()) {
throw new ResourceNotFoundException(USER_NOT_FOUND_MESSAGE);
}
return optionalUser.get();
}
#Override
public User findById(String id) throws ResourceNotFoundException {
Optional<User> optionalUser = userRepository.findById(id);
if (optionalUser.isEmpty()) {
throw new ResourceNotFoundException(USER_NOT_FOUND_MESSAGE);
}
return optionalUser.get();
}
#Override
public User update(String id, UpdateUserDto updateUserDto) throws ResourceNotFoundException {
User user = findById(id);
if (updateUserDto.getFullName() != null) {
user.setFullName(updateUserDto.getFullName());
}
if (updateUserDto.getEmail() != null) {
user.setEmail(updateUserDto.getEmail());
}
return userRepository.save(user);
}
#Override
public void update(User user) {
userRepository.save(user);
}
#Override
public User updatePassword(String id, UpdatePasswordDto updatePasswordDto) throws ResourceNotFoundException {
User user = findById(id);
if (bCryptEncoder.matches(updatePasswordDto.getCurrentPassword(), user.getPassword())) {
user.setPassword(bCryptEncoder.encode(updatePasswordDto.getNewPassword()));
return userRepository.save(user);
}
return null;
}
#Override
public void updatePassword(String id, String newPassword) throws ResourceNotFoundException {
User user = findById(id);
user.setPassword(bCryptEncoder.encode(newPassword));
userRepository.save(user);
}
public void confirm(String id) throws ResourceNotFoundException {
User user = findById(id);
user.setConfirmed(true);
userRepository.save(user);
}
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<User> userOptional = userRepository.findByEmail(username);
if(userOptional.isEmpty()){
throw new UsernameNotFoundException("Invalid username or password.");
}
User user = userOptional.get();
return new org.springframework.security.core.userdetails.User(
user.getEmail(), user.getPassword(), user.isEnabled(), true, true, user.isConfirmed(), getAuthority(user)
);
}
private Set<SimpleGrantedAuthority> getAuthority(User user) {
Set<SimpleGrantedAuthority> authorities = new HashSet<>();
authorities.add(new SimpleGrantedAuthority(user.getRole().getName()));
user.allPermissions().forEach(permission -> authorities.add(new SimpleGrantedAuthority(permission.getName())));
return authorities;
}
}
You can place the bean factory method for the encoder in a separate configuration class or you can leave it where it is and make the bean factory method static:
#Bean
public static BCryptPasswordEncoder encoder() {
return new BCryptPasswordEncoder();
}
This make it clear to Spring that the encoder does not depend on anything injected into the class instance.
You can try like this:
#Service
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
private final BCryptPasswordEncoder bCryptEncoder;
public UserServiceImpl(UserRepository userRepository, BCryptPasswordEncoder bCryptEncoder) {
this.userRepository = userRepository;
this.bCryptEncoder = bCryptEncoder;
}
[...]
I believe a class annotated with #Configuration will be subjected to executed initially in a Spring application. As we can see you have created a lot of beans that has to be autowired in multiple other classes which will be created eventually probably the classes annotated with service, controller and all.
Now in the WebSecurityConfig there is an autowiring for UserServiceImpl
for which the bean hasn't created yet it will be created after the configuration class is done. And the UserServiceImpl also requires BCryptPasswordEncoder So this appears like a dead lock each of them is dependent on one another. This is what I could decipher.
Also autowiring should be done with Interface as type.
#Autowired
private UserService userServiceImpl;
I think moving the password encoder bean creation to another config class might help to solve this situation.
I resolved this issue by creating a new configuration class as follows;
#Configuration
public class PasswordSecurityConfig {
#Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
I used this class in the WebSecurityConfig as follows;
#Autowired
public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userServiceImpl)
.passwordEncoder(new PasswordSecurityConfig().bCryptPasswordEncoder());
}
I can now run the app without any issues.
I have the following success handler that is part of the WebSecurityConfig.java file:
.successHandler(new AuthenticationSuccessHandler() {
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
CustomOAuth2User oauthUser = (CustomOAuth2User) authentication.getPrincipal();
userAuthService.processOAuthPostLogin(oauthUser.getEmail());
response.sendRedirect("/list");
}
})
At the line where casting is supposed to take place, it gives the error:
java.lang.ClassCastException: class
org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser
cannot be cast to class
com.myapp.myapp.mvc.business.domain.user.CustomOAuth2User
How can I go about solving this?
The CustomOAuth2User class is as follows:
public class CustomOAuth2User implements OAuth2User {
private OAuth2User oauth2User;
public CustomOAuth2User(OAuth2User oauth2User) {
this.oauth2User = oauth2User;
}
#Override
public Map<String, Object> getAttributes() {
return oauth2User.getAttributes();
}
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return oauth2User.getAuthorities();
}
#Override
public String getName() {
return oauth2User.getAttribute("name");
}
public String getEmail() {
return oauth2User.<String>getAttribute("email");
}
}
I think your code are from codejava.net right? I also got the same issues and I fixed with update on the onAuthenticationSuccess function. I change the following code
CustomOAuth2User oauthUser = (CustomOAuth2User) authentication.getPrincipal();
userAuthService.processOAuthPostLogin(oauthUser.getEmail());
with this code
DefaultOidcUser oauthUser = (DefaultOidcUser) authentication.getPrincipal();
String email = oauthUser.getAttribute("email");
userDetailsService.processOAuthPostLogin(email);
And it's working now :)
You are getting this error because you have not defined the scopes explicitly in your app and by default scopes also include openId.
If you define your scopes in the application.properties like below you can cast the principal to CustomOAuth2User .
spring.security.oauth2.client.registration.google.scope=email,profile
If you wish to include openId in your scopes, then you need to cast your principal to DefaultOidcUser an you can create a CustomOidcUserService service like below
#Service
public class CustomOidcUserService extends OidcUserService {
#Autowired private UserRepository userRepository;
#Autowired private RoleRepository roleRepository;
#Override
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
final OidcUser oidcUser = super.loadUser(userRequest);
return oidcUser;
}
I am having a challenge extracting original user details from the LdapUserDetailsImpl such as the getUsername() returns null
I have the following Java classes
Custom User Class
public class AppUserDetails extends LdapUserDetailsImpl {
public AppUserDetails() {
super();
}
private String mail; //mail
}
Custom User Details Mapper
public class AppUserDetailsContextMapper extends LdapUserDetailsMapper {
public AppUserDetailsContextMapper() {
super();
}
#Override
public AppUserDetails mapUserFromContext(DirContextOperations ctx, String username, Collection<? extends GrantedAuthority> authorities) {
UserDetails details = super.mapUserFromContext(ctx, username, authorities);
String mail = ctx.getStringAttribute("mail");
AppUserDetails appUserDetails = new AppUserDetails();
appUserDetails.setMail(mail);
return appUserDetails;
}
}
Web security configuration
#Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
Environment env;
#Bean
public UserDetailsContextMapper userDetailsContextMapper() {
return new AppUserDetailsContextMapper();
}
#Bean
public ActiveDirectoryLdapAuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
ActiveDirectoryLdapAuthenticationProvider provider =
new ActiveDirectoryLdapAuthenticationProvider(
env.getRequiredProperty("spring.ldap.domain"),
env.getRequiredProperty("spring.ldap.urls")
);
provider.setConvertSubErrorCodesToExceptions(true);
provider.setUseAuthenticationRequestCredentials(true);
provider.setUserDetailsContextMapper(userDetailsContextMapper());
return provider;
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.authenticationProvider(activeDirectoryLdapAuthenticationProvider())
.userDetailsService(userDetailsService());
}
#Bean
public AuthenticationManager authenticationManager() {
return new ProviderManager(
Collections
.singletonList(activeDirectoryLdapAuthenticationProvider())
);
}
}
However I am having a serious challenge trying to getting Custom User Details in the controller:
#Controller
public class HomeController {
#GetMapping(value = {"/home"})
public String home(Model model) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
System.out.println(authentication.getPrincipal());
AppUserDetails appUserDetails = (AppUserDetails) authentication.getPrincipal();
System.out.println(appUserDetails);
return "home";
}
}
I am getting a NullPointer exception if I try to any property from the LdapUserDetailsImpl class. However, I am accurately getting all the properties from the AppUserDetails - which extends the LdapUserDetailsImpl class.
Where might I be missing it?
I tried to integrate spring security and spring social. The problem is when I try to create account for him in my system. I would like to avoid creating user with id "anonymousUser".
For expected behaviour when I analyzed code from spring-social-security I expected to call method JdbcUsersConnectionRepository.findUserIdsWithConnection(Connection connection) for creating user and entry in the system. But when I run my code I find out this is not true. Before this action is calling implementation of SocialConfigurer.getUserId(). Great... If we have anonymous user it will be cached... But anyway getUserId is called before anything so authorization doesn't work. So I wrote my own implementation to this thing:
#Configuration
#EnableSocial
public class SocialConfig implements SocialConfigurer {
#Autowired
private DataSource dataSource;
#Autowired
private CustomerService customerService;
#Autowired
private CustomerProviderRepository customerProviderRepository;
#Autowired
private PasswordEncoder passwordEncoder;
#Override
public void addConnectionFactories(ConnectionFactoryConfigurer connectionFactoryConfigurer, Environment environment) {
}
#Override
public UserIdSource getUserIdSource() {
return () -> {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || authentication.getName().equals("anonymousUser")) {
throw new IllegalStateException("Unable to get a ConnectionRepository: no user signed in");
}
return authentication.getName();
};
}
#Override
public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());
repository.setConnectionSignUp(new SocialConnectionSignUpService(customerService, customerProviderRepository, passwordEncoder));
return repository;
}
}
Implementation of SocialCOnnectionSignUpService
public class SocialConnectionSignUpService implements ConnectionSignUp {
private final CustomerService customerService;
private final CustomerProviderRepository customerProviderRepository;
private final PasswordEncoder passwordEncoder;
public SocialConnectionSignUpService(CustomerService customerService, CustomerProviderRepository customerProviderRepository, PasswordEncoder passwordEncoder) {
this.customerService = customerService;
this.passwordEncoder = passwordEncoder;
this.customerProviderRepository = customerProviderRepository;
}
#Override
public String execute(Connection<?> connection) {
ConnectionKey connectionKey = connection.getKey();
UserProfile profile = connection.fetchUserProfile();
Customer customer = customerService.findBy(profile.getEmail());
if(customer == null) {
customer = new Customer();
customer.setEmail(profile.getEmail());
customerService.add(customer);
}
SocialKey key = new SocialKey();
key.setUserId(customer.getId().toString());
key.setProviderUserId(connectionKey.getProviderUserId());
key.setProviderId(connectionKey.getProviderId());
CustomerProvider customerProvider = new CustomerProvider();
customerProvider.setKey(key);
customerProvider.setDisplayName(profile.getName());
customerProviderRepository.save(customerProvider);
return customerProvider.id();
}
}
And configuration of spring security:
#Configuration
#EnableWebSecurity
public class SecurityContext extends WebSecurityConfigurerAdapter {
#Autowired
private DataSource dataSource;
#Autowired
private CustomerRepository customerRepository;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/login/authenticate")
.permitAll()
.and()
.rememberMe()
.and()
.authorizeRequests()
.antMatchers("/signup/social").authenticated()
.antMatchers("/**").permitAll()
.and()
.apply(new SpringSocialConfigurer().postLoginUrl("/signup/social").alwaysUsePostLoginUrl(true))
.and()
.csrf().disable();
}
#Bean
public SocialUserDetailsService socialUserDetailsService() {
return new SocialUserDetailsServiceImpl(customerRepository);
}
}
Do you have any workaround or solution how to pass the problem?