Custom Principal with OAuth2 in existing form login application - spring-boot

I'm trying to add OAuth2 login to an existing form login application. So far I've added the required configuration to get Google auth working and the goal is to enable existing username/password users (or new users) to login both ways.
All my controllers rely on my UserDetails implementation:
public class User implements UserDetails {
private Long id;
private String email;
private String password;
private String googleAccessToken;
// ...
And I can get the logged user in controllers like this:
#GetMapping
public String index(#AuthenticationPrincipal User user, Model model) {
So what I've done is to implement my custom OAuth2UserService to fetch the existing user from the database but I can't find the way to set the User as the principal.
In previous versions of the OAuth2 integration it seemed to exist a simpler solution based on PrincipalExtractor but it is no longer available.
#Service
#RequiredArgsConstructor
public class OAuth2UserDetailsService implements OAuth2UserService<OidcUserRequest, OidcUser> {
private final UsersRepository usersRepository;
#Override
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
final OidcUserService delegate = new OidcUserService();
User user;
// Delegate to the default implementation for loading a user
OidcUser oidcUser = delegate.loadUser(userRequest);
switch (userRequest.getClientRegistration().getClientName()) {
case "google":
user = usersRepository.findOneByGoogleAccessToken(userRequest.getAccessToken());
break;
default:
throw new OAuth2AuthenticationException(new OAuth2Error("invalid_token"));
}
// here I should return my user principal
return new DefaultOidcUser(null, null);
}
Any ideas?

Finally solved this returning an instance of OidcUser:
public class UserPrincipal implements UserDetails, OidcUser {
// ...
}
And in the OAuth2UserService:
#Override
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
final OidcUserService delegate = new OidcUserService();
User user;
// Delegate to the default implementation for loading a user
OidcUser oidcUser = delegate.loadUser(userRequest);
// ...
return new UserPrincipal(user, oidcUser);
}

Related

How to set a custom principal object during or after JWT authentication?

I've changed the way a user is authenticated in my backend. From now on I am receiving JWT tokens from Firebase which are then validated on my Spring Boot server.
This is working fine so far but there's one change which I am not too happy about and it's that the principal-object is now a org.springframework.security.oauth2.jwt.Jwt and not a AppUserEntity, the user-model, like before.
// Note: "authentication" is a JwtAuthenticationToken
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Jwt jwt = (Jwt) authentication.getPrincipal();
So, after some reading and debugging I found that the BearerTokenAuthenticationFilter essentially sets the Authentication object like so:
// BearerTokenAuthenticationFilter.java
AuthenticationManager authenticationManager = this.authenticationManagerResolver.resolve(request);
// Note: authenticationResult is our JwtAuthenticationToken
Authentication authenticationResult = authenticationManager.authenticate(authenticationRequest);
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authenticationResult);
SecurityContextHolder.setContext(context);
and as we can see, this on the other hand comes from the authenticationManager which is a org.springframework.security.authentication.ProviderManager and so on. The rabbit hole goes deep.
I didn't find anything that would allow me to somehow replace the Authentication.
So what's the plan?
Since Firebase is now taking care of user authentication, a user can be created without my backend knowing about it yet. I don't know if this is the best way to do it but I intend to simply create a user record in my database once I discover a valid JWT-token of a user which does not exist yet.
Further, a lot of my business logic currently relies on the principal being a user-entity business object. I could change this code but it's tedious work and who doesn't want to look back on a few lines of legacy code?
I did it a bit different than Julian Echkard.
In my WebSecurityConfigurerAdapter I am setting a Customizer like so:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.oauth2ResourceServer()
.jwt(new JwtResourceServerCustomizer(this.customAuthenticationProvider));
}
The customAuthenticationProvider is a JwtResourceServerCustomizer which I implemented like this:
public class JwtResourceServerCustomizer implements Customizer<OAuth2ResourceServerConfigurer<HttpSecurity>.JwtConfigurer> {
private final JwtAuthenticationProvider customAuthenticationProvider;
public JwtResourceServerCustomizer(JwtAuthenticationProvider customAuthenticationProvider) {
this.customAuthenticationProvider = customAuthenticationProvider;
}
#Override
public void customize(OAuth2ResourceServerConfigurer<HttpSecurity>.JwtConfigurer jwtConfigurer) {
String key = UUID.randomUUID().toString();
AnonymousAuthenticationProvider anonymousAuthenticationProvider = new AnonymousAuthenticationProvider(key);
ProviderManager providerManager = new ProviderManager(this.customAuthenticationProvider, anonymousAuthenticationProvider);
jwtConfigurer.authenticationManager(providerManager);
}
}
I'm configuring the NimbusJwtDecoder like so:
#Component
public class JwtConfig {
#Bean
public JwtDecoder jwtDecoder() {
String jwkUri = "https://www.googleapis.com/service_accounts/v1/jwk/securetoken#system.gserviceaccount.com";
return NimbusJwtDecoder.withJwkSetUri(jwkUri)
.build();
}
}
And finally, we need a custom AuthenticationProvider which will return the Authentication object we desire:
#Component
public class JwtAuthenticationProvider implements AuthenticationProvider {
private final JwtDecoder jwtDecoder;
#Autowired
public JwtAuthenticationProvider(JwtDecoder jwtDecoder) {
this.jwtDecoder = jwtDecoder;
}
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
BearerTokenAuthenticationToken token = (BearerTokenAuthenticationToken) authentication;
Jwt jwt;
try {
jwt = this.jwtDecoder.decode(token.getToken());
} catch (JwtValidationException ex) {
return null;
}
List<GrantedAuthority> authorities = new ArrayList<>();
if (jwt.hasClaim("roles")) {
List<String> rolesClaim = jwt.getClaim("roles");
List<RoleEntity.RoleType> collect = rolesClaim
.stream()
.map(RoleEntity.RoleType::valueOf)
.collect(Collectors.toList());
for (RoleEntity.RoleType role : collect) {
authorities.add(new SimpleGrantedAuthority(role.toString()));
}
}
return new JwtAuthenticationToken(jwt, authorities);
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(BearerTokenAuthenticationToken.class);
}
}
This is working fine so far but there's one change which I am not too happy about and it's that the principal-object is now a org.springframework.security.oauth2.jwt.Jwt and not a AppUserEntity, the user-model, like before.
In my application I have circumvented this by rolling my own JwtAuthenticationFilter instead of using BearerTokenAuthenticationFilter, which then sets my User Entity as the principal in the Authentication object. However, in my case this constructs a User barely from the JWT claims, which might be bad practice: SonarLint prompts to use a DTO instead to mitigate the risk of somebody injecting arbitrary data into his user record using a compromised JWT token. I don't know if that is a big deal - if you can't trust your JWTs, you have other problems, IMHO.
I don't know if this is the best way to do it but I intend to simply create a user record in my database once I discover a valid JWT-token of a user which does not exist yet.
Keep in mind that JWTs should be verified by your application in a stateless manner, solely by verifying their signature. You shouldn't hit the database every time you verify them. Therefor it would be better if you create a user record using a method call like
void foo(#AuthenticationPrincipal final Jwt jwt) {
// only invoke next line if reading JWT claims is not enough
final User user = userService.findOrCreateByJwt(jwt);
// TODO method logic
}
once you need to persist changes to the database that involve this user.
Since
SecurityContextHolder.setContext(context);
won't work for
request.getUserPrincipal();
you may create a custom class extending HttpServletRequestWrapper
import java.security.Principal;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
public class UserPrincipalHttpServletRequest extends HttpServletRequestWrapper {
private final Principal principal;
public UserPrincipalHttpServletRequest(HttpServletRequest request, Principal principal) {
super(request);
this.principal = principal;
}
#Override
public Principal getUserPrincipal() {
return principal;
}
}
then in your filter do something like this:
protected void doFilterInternal(HttpServletRequest request){
. . .
// create user details, roles are required
Set<GrantedAuthority> authorities = new HashSet<>();
authorities.add(new SimpleGrantedAuthority("SOME ROLE"));
UserDetails userDetails = new User("SOME USERNAME", "SOME PASSWORD", authorities);
// Create an authentication token
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// follow the filter chain, using the new wrapped UserPrincipalHtppServletRequest
chain.doFilter(new UserPrincipalHttpServletRequest(request, usernamePasswordAuthenticationToken), response);
// all filters coming up, will be able to run request.getUserPrincipal()
}
According Josh Cummings answer in issue #7834 make configuration:
public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
http...
.oauth2ResourceServer(oauth2 -> oauth2.jwt(
jwt -> jwt.jwtAuthenticationConverter(JwtUtil::createJwtUser)))
...
return http.build();
}
and implement factory method, e.g:
public class JwtUtil {
public static JwtUser createJwtUser(Jwt jwt) {
int id = ((Long) jwt.getClaims().get("id")).intValue();
String rawRoles = (String) jwt.getClaims().get("roles");
Set<Role> roles = Arrays.stream(rawRoles.split(" "))
.map(Role::valueOf)
.collect(Collectors.toSet());
return new JwtUser(jwt, roles, id);
}
}
public class JwtUser extends JwtAuthenticationToken {
public JwtUser(Jwt jwt, Collection<? extends GrantedAuthority> authorities, int id) {
super(jwt, authorities);
....
}
}
Take in note, that controller's methods should inject JwtUser jwtUser without any #AuthenticationPrincipal

Springboot multiple login

I'm trying to enable multiple login instead of single person login.
I've developed single person login by following however, don't know how to do multiple login. Anyone please help?
Account.java file:
#Getter
#Setter
public class Account {
private Long id;
private String studentId;
private String password;
}
This is my controller.
#GetMapping("/create") was made to check whether the password is properly hashed or not.
#RestController
public class AccountController {
#Autowired
AccountService accountService;
#GetMapping("/create")
public Account create(){
Account account = new Account();
account.setStudentId("123");
account.setPassword("123");
return accountService.save(account);
}
}
This is my service layer
#Service
public class AccountService implements UserDetailsService {
#Autowired
private AccountRepository accountRepository;
#Autowired
private PasswordEncoder passwordEncoder;
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Account account = accountRepository.findByStudentId(username);
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
return new User(account.getStudentId(), account.getPassword(), authorities);
}
public Account save(Account account) {
account.setPassword(passwordEncoder.encode(account.getPassword()));
return accountRepository.save(account);
}
}
This is my repository setting
#Repository
public class AccountRepository {
private Map<String, Account> accounts = new HashMap<>();
private Random random = new Random();
public Account save(Account account) {
account.setId(random.nextLong());
accounts.put(account.getStudentId(), account);
return account;
}
public Account findByStudentId(String username) {
return accounts.get(username);
}
}
How can I enable multiple users login?
Few tips after seeing your code:
Make a simple login JS page and try to get data on form submit URL(use path variable to read it.)
#RequestMapping(path = "/{create}/{user}")
public String createUser(#PathVariable("id") String id, #PathVariable("pass") String pass) {
// read id & pass then save
}
2.Always decode your password and match .i.e both id & password should be matched.
by this you can create as many user you want.

Spring user authentication does not work after updating related document in Mongo Db

I'm experiencing a rather weird behavior with Spring Security and Mongo Db.
I have a customer User entity that is persisted in Mongo:
#Document(collection = "user")
public class User {
#Id
private ObjectId id;
private String username;
...
}
I implement a UserService that allows to persist such entities in a rather usual way:
#Service
public class UserServiceImpl implements UserService {
#Override
public void save(User user) {
user.setPassword(bCryptPasswordEncoder.encode(user.getPassword()));
user.setAccountNonExpired(true);
user.setAccountNonLocked(true);
user.setCredentialsNonExpired(true);
user.setEnabled(true);
user.addRole(roleRepository.findByName("USER"));
userRepository.save(user);
}
...
}
I also have a SecurityService implementation:
#Service
public class SecurityServiceImpl implements SecurityService {
#Override
public void autologin(String username, String password) { ... }
}
And a UserDetailsService:
#Service
public class UserDetailsServiceImpl implements UserDetailsService {
#Override
#Transactional(readOnly = true)
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if (user == null) {
user = userRepository.findByEmail(username);
if (user == null) {
log.warn("Login attempt with unknown username");
throw new UsernameNotFoundException("not found");
}
}
Set<GrantedAuthority> grantedAuthorities = new HashSet<>();
for (Role role : user.getRoles()) {
grantedAuthorities.add(new SimpleGrantedAuthority(role.getName()));
}
return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(),
user.isEnabled(), user.isAccountNonExpired(),
user.isCredentialsNonExpired(), user.isAccountNonLocked(),
grantedAuthorities);
}
In my controller, I have something like
#RequestMapping(value = "/login", method = RequestMethod.GET)
public String login(HttpServletRequest request, Model model, String error,
String logout, Principal principal) { ... }
This worked great. I can register users, login, logout, re-login. Info is persisted in Mongo and loaded properly. All seemed to be good, until I tried to implement reseting the password..
I implemented simple method in my UserServiceImpl class to perform this, but even by simply using the following implementation a rather weird behavior occurs:
#Override
public void updateUser(User user) {
userRepository.save(user);
}
After calling this method, if I log out, I can no longer login. I can re-start my spring-boot application, and still I cannot login.
This is odd because I can see that the JSON document in Mongo did not change. And this becomes crazy after I realized that if I manually edit the related document in Mongo (without performing any actual change, simply removing and adding the same character and saving it), then the login works again!
If it helps:
MongoDB shell version: 3.2.13, spring boot 1.4.1.RELEASE
Any clue how to fix this?
Not sure but I think you have missed a tiny big thing when updating user.
You encrypted password when you created a user
user.setPassword(bCryptPasswordEncoder.encode(user.getPassword()));
I am assuming your are updating password as well when updating password.
But there is no password encryption when updating user as i can see in your updateUser method (Unless you have encrypted password before calling this method).
And if I am correct then my advise to avoid update password while update user data and make different method to change password.

Implementation for Implicit social signup in spring-boot project

By adding the spring-social signin / signup to my current spring-boot project, through the implementation of the classes ConnectionSignUp and SignInAdapter, the application, after the authorization with the social network website, it's requiring the implementation of a mapping for "/signup" in my controller. In this method, I do basically the same what I already have implemented on the ConectionSignUp method. Anyone knows what I can do to avoid this duplicity, and direct the application to my ConnectionSignUp class instead of one extra method on my controller?
my implementation so far includes the following classes:
ConnectionSIgnUp
#Component
public class CustomConnectionSignUp implements ConnectionSignUp {
#Autowired
private UsuarioDao account;
#Autowired
private JavaMailSender mailSender;
public String execute(Connection<?> connection) {
UserProfile profile = connection.fetchUserProfile();
String user;
try {
Usuario novo = new Usuario(profile.getUsername(),UUID.randomUUID().toString().replaceAll("-", ""),null,null,false,true);
account.insert(novo);
return novo.getLogin();
} catch (Exception e) {
return null;
}
}
}
SignInAdapter
#Component
public class CustomSignInAdapter implements SignInAdapter {
#Autowired
private SocialUserDetailsService socialUserDetailsService;
public String signIn(String userId, Connection<?> connection, NativeWebRequest request) {
SocialUserDetails user = socialUserDetailsService.loadUserByUserId(userId);
if(user != null)
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(user.getUserId(), null, null));
return null;
}
}
SocialUserDetailsService
#Service
public class CustomSocialUserDetailsService implements SocialUserDetailsService {
#Autowired
private UsuarioDao account;
public SocialUserDetails loadUserByUserId(String userId) {
for(Usuario usuario : account.select())
if(usuario.getLogin().equals(userId))
return new SocialUser(usuario.getLogin(), usuario.getSenha(), usuario.isEnabled(), usuario.isAccountNonExpired(), usuario.isCredentialsNonExpired(), usuario.isAccountNonLocked(), usuario.getAuthorities());
return null;
}
}
application.properties
# SPRING SOCIAL (SocialWebAutoConfiguration)
spring.social.auto-connection-views=false
# SPRING SOCIAL FACEBOOK (FacebookAutoConfiguration)
spring.social.facebook.app-id=...
spring.social.facebook.app-secret=...
# SPRING SOCIAL TWITTER (TwitterAutoConfiguration)
spring.social.twitter.app-id=...
spring.social.twitter.app-secret=...
It seems to me that you didn't Inject ConnectionSignUp into UsersConnectionRepository. Therefore Spring tries redirect user to page with sign up form. To perform implicit sign up try to pass your ConnectionSignUp bean to setConnectionSignUp().

How to implement AuditorAware with Spring Data JPA and Spring Security?

We use Hibernate/JPA, Spring, Spring Data and Spring Security in our application. I have a standard User entity which is mapped using JPA. Further, I have a UserRepository
public interface UserRepository extends CrudRepository<User, Long> {
List<User> findByUsername(String username);
}
which follows the Spring Data convention for naming query methods. I have an entity
#Entity
public class Foo extends AbstractAuditable<User, Long> {
private String name;
}
I want to use Spring Data auditing support. (As descripe here.) Hence I created a AuditorService as follows:
#Service
public class AuditorService implements AuditorAware<User> {
private UserRepository userRepository;
#Override
public User getCurrentAuditor() {
String username = SecurityContextHolder.getContext().getAuthentication().getName();
List<User> users = userRepository.findByUsername(username);
if (users.size() > 0) {
return users.get(0);
} else {
throw new IllegalArgumentException();
}
}
#Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}
}
When I create a method
#Transactional
public void createFoo() {
Foo bar = new Foo();
fooRepository.save(foo);
}
Where everything is correctly wired and FooRepository is a Spring Data CrudRepository. Then a StackOverflowError is thrown since the the call to findByUsername seems to trigger hibernate to flush the data to the database which triggers AuditingEntityListener who calls AuditorService#getCurrentAuditor which again triggers a flush and so on.
How to avoid this recursion? Is there a "canonical way" to load the User entity? Or is there a way to prevent Hibernate/JPA from flushing?
I got the same issue and what I did was just change the propagation on the findByUsername(username) method to Propagation.REQUIRES_NEW, I suspected that was a problem with the transactions, so I changed to use a new transaction and that worked well for me. I hope this can help.
#Repository
public interface UserRepository extends JpaRepository<User, String> {
#Transactional(propagation = Propagation.REQUIRES_NEW)
List<User> findByUsername(String username);
}
The solution is not to fetch the User record in the AuditorAware implementation. This triggers the described loop, since a select query triggers a flush (this is the case since Hibernate/JPA wants to write the data to the database to commit the transaction before executing the select), which triggers a call to AuditorAware#getCurrentAuditor.
The solution is to store the User record in the UserDetails provided to Spring Security. Hence I created my own implementation:
public class UserAwareUserDetails implements UserDetails {
private final User user;
private final Collection<? extends GrantedAuthority> grantedAuthorities;
public UserAwareUserDetails(User user) {
this(user, new ArrayList<GrantedAuthority>());
}
public UserAwareUserDetails(User user, Collection<? extends GrantedAuthority> grantedAuthorities) {
this.user = user;
this.grantedAuthorities = grantedAuthorities;
}
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return grantedAuthorities;
}
#Override
public String getPassword() {
return user.getSaltedPassword();
}
#Override
public String getUsername() {
return user.getUsername();
}
#Override
public boolean isAccountNonExpired() {
return true;
}
#Override
public boolean isAccountNonLocked() {
return true;
}
#Override
public boolean isCredentialsNonExpired() {
return true;
}
#Override
public boolean isEnabled() {
return true;
}
public User getUser() {
return user;
}
}
Further, I changed my UserDetailsService to load the User and create UserAwareUserDetails. Now it is possible to access the User instance through the SercurityContextHolder:
#Override
public User getCurrentAuditor() {
return ((UserAwareUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUser();
}
It looks like you use a User entity for two different things:
authentication
audit
I think it will be better to prepare a special AuditableUser for audit purpose (it will have identical username field as original User).
Consider following case: you want to delete some User from database. If all your audit objects are linked to User then they will a) loose author b) may be deleted by cascade too (depends on how the link is implemented). Not sure that you want it.
So by using special AuditableUser you will have:
no recursion
ability to delete some User from the system and preserve all audit info about it
To be honest, You do not actually require one another entity.
For example, I had similar problem and I resolved it in following way:
public class SpringSecurityAuditorAware implements AuditorAware<SUser>, ApplicationListener<ContextRefreshedEvent> {
private static final Logger LOGGER = getLogger(SpringSecurityAuditorAware.class);
#Autowired
SUserRepository repository;
private SUser systemUser;
#Override
public SUser getCurrentAuditor() {
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
SUser principal;
if (authentication == null || !authentication.isAuthenticated()) {
principal = systemUser;
} else {
principal = (SUser) authentication.getPrincipal();
}
LOGGER.info(String.format("Current auditor is >>> %s", principal));
return principal;
}
#Override
public void onApplicationEvent(final ContextRefreshedEvent event) {
if (this.systemUser == null) {
LOGGER.info("%s >>> loading system user");
systemUser = this.repository.findOne(QSUser.sUser.credentials.login.eq("SYSTEM"));
}
}
}
Where SUser is both the class which I use for auditing as well as for the security.
I had maybe different use case than Yours and my approach will be deleted after, but it can be resolved like this.

Resources