I want to customize login API in spring boot. For a single kind of user, I created a implementation of UserDetailsService and it worked perfectly fine. Now, I want to create 3 different kinds of users, i.e., 3 different authorities. I don't think a single implementation can help me here. If I create 3 different implementations, and try using #Qualifier, how do I call a specific implementation ?
Any sort of help is appreciated! Below is the code for Login Endpoint of single kind Of user.
private static Logger logger = LogManager.getLogger();
#Value("${jwt.expires_in}")
private int EXPIRES_IN;
#Autowired
AuthenticationManager authManager;
#Autowired
TokenHelper tokenHelper;
#Autowired
ObjectMapper objectMapper;
#Autowired
PrincipalRepository principalRepository;
private boolean isAuthenticated(Authentication authentication) {
return authentication != null && !(authentication instanceof AnonymousAuthenticationToken) && authentication.isAuthenticated();
}
#PostMapping("/principal")
public ResponseEntity<Object[]> loginPrincipal(#RequestParam(name ="username") String username,
#RequestParam(name ="password") String password){
logger.info("In login api");
if(StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
logger.error("Invalid Request!");
return ResponseEntity.badRequest().header("reason", "bad request").body(null);
}
UsernamePasswordAuthenticationToken authReq =
new UsernamePasswordAuthenticationToken(username, password);
Authentication authentication = authManager.authenticate(authReq);
boolean isAuthenticated = isAuthenticated(authentication);
if (!isAuthenticated) {
logger.error("Not authenticated");
return ResponseEntity.badRequest().body(null);
}
Principal principal = null;
try {
principal = principalRepository.findByUserName(username);
}catch(Exception e) {
logger.error("Couldn't retrieve user");
return ResponseEntity.badRequest().header("reason", "username not found").body(null);
}
String jwt = tokenHelper.generateToken( username );
SecurityContextHolder.getContext().setAuthentication(authentication);
UserTokenState userTokenState = new UserTokenState(jwt, EXPIRES_IN);
return ResponseEntity.accepted().body(new Object[] {userTokenState, principal.getPrincipalID()});
}
Below is the code for UserDetailsService Implementation:
#Service
public class UserDetailServiceImpl implements UserDetailsService {
#Autowired
private PrincipalRepository principalRepository;
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println("Loading user from db");
Principal principal = principalRepository.findByUserName(username);
if( principal == null){
System.out.println("User not found");
throw new UsernameNotFoundException("No user found. Username tried: " + username);
}
Set<GrantedAuthority> grantedAuthorities = new HashSet<GrantedAuthority>();
grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_PRINCIPAL"));
System.out.println("All done");
return new org.springframework.security.core.userdetails.User(principal.getUserName(), principal.getPassword(), grantedAuthorities);
}
}
Here, I am fetching a principal from db, because this implementation is principal-specific. I wanna create similar implementations for Student and Teacher and use them accordingly.
You don't need to create more than one implementation for UserDetailsService. Student, Teacher are also users, only one thing will differ these users is "authorities"(role & authorities) in the application if we look at from general view. Spring Security firstly checks "username" and "password" for authentication and after successful authentication, it checks "authorities" for authorization process in order to allow to use resources(methods, and etc) according to the business logic of the application.
Related
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
this is my AuthenticationProvider in authorization server
#Service
public class UmUserAuthenticationProvider implements AuthenticationProvider {
#Autowired
#Qualifier("UmUserDetailsService")
private UserDetailsService userDetailService;
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// TODO Auto-generated method stub
String username = authentication.getName();
String password = (String) authentication.getCredentials();
long userId = (new SecurityUtil()).checkUser(umUserMapper, username, password);
if (userId <= 0) {
throw new BadCredentialsException("login failed");
}
UserDetails user = userDetailService.loadUserByUsername(username);
//I've try different ways to put user detail in here
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(user, null,
user.getAuthorities());
auth.setDetails(user);
return auth;
}
#Override
public boolean supports(Class<?> authentication) {
// TODO Auto-generated method stub
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
and this is my resource server, I cant get what I set in Authentication,
getPrincipal is String
getDetails is type of org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails
#RequestMapping("/test")
public class UserController {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
#RequestMapping(value = "/test", method = RequestMethod.GET)
public String getRouters() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
logger.info(auth.getPrincipal().toString());
logger.info(auth.getDetails().toString());
JSONObject jo = new JSONObject();
return jo.toString();
}
the logger print
so How cat I get the custom detail in Authentication?
In the end, I solve this problem in a trick way
whatever I set in UsernamePasswordAuthenticationToken, I got the type of String, So I fill it with json string;
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
new JSONObject(user).toString(), password, userDetailService.getAuthorities(username));
so I can parse json string at the controller.
But I still want to know what cause it
I think the problem is, that you are setting the UserDetails on authentication, but it is overriden by the default.
A custom authentication converter might be the solution.
public class CustomAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {
#Override
public AbstractAuthenticationToken convert(#NotNull final Jwt jwt) {
String username = jwt.getClaimAsString("username");
UserDetails user = userDetailService.loadUserByUsername(username);
return new UsernamePasswordAuthenticationToken (jwt, user, userDetailService.getAuthorities(username)));
}
}
I have a question regarding how I can automatically log in a user after it has registered with my Spring Boot app. The user's password is saved into the MySQL DB using Bcrypt.
This is a method I have to properly create and save a new user, and it seems to work fine :
#Autowired
private UserRepository userRepository;
#Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
public User save(User user, Role role) {
user.setPassword(bCryptPasswordEncoder.encode(user.getPassword()));
user.setRoles(new HashSet<>(Arrays.asList(role)));
userRepository.save(user);
return user;
}
This is the method I execute to try to log in a newly created user :
public boolean login(String username, String password) {
//password is plaintext and is what was POST-ed from the HTML form
UserDetails userDetails = loadUserByUsername(username);
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities());
authenticationManager.authenticate(usernamePasswordAuthenticationToken);
if (usernamePasswordAuthenticationToken.isAuthenticated()) {
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
logger.debug(String.format("Logged in %s successfully!", username));
return true;
} else {
logger.debug(String.format("Failed to login %s", username));
return false;
}
}
Now, when it hits the line:
authenticationManager.authenticate(usernamePasswordAuthenticationToken);
it will always complain:
00:36:47.635 [http-nio-5000-exec-10] DEBUG o.s.web.servlet.DispatcherServlet - Could not complete request
org.springframework.security.authentication.BadCredentialsException: Bad credentials
I thought this was strange. In my WebSecurityConfigurerAdapter , I have these set up:
#Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userServiceManager).passwordEncoder(bCryptPasswordEncoder());
}
Could someone please let me know if I might have missed anything?
Thanks
I am new to spring security and have used jhipster in which i have configured db and LDAP based authentications. Now i have integrated it with OAuth client using #enableOAuthSso. I can able to authenticate using external OAuth Idp (Okta) and it is redirecting to my application and my principle is getting updated and i can access resources through rest. But my userDetails object not getting populated.
#Inject
public void configureGlobal(AuthenticationManagerBuilder auth) {
try {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
auth
.ldapAuthentication()
.ldapAuthoritiesPopulator(ldapAuthoritiesPopulator)
.userDnPatterns("uid={0},ou=people")
.userDetailsContextMapper(ldapUserDetailsContextMapper)
.contextSource(getLDAPContextSource());
} catch (Exception e) {
throw new BeanInitializationException("Security configuration failed", e);
}
}
I have check by going deep where its getting failed and found out the following
public static String getCurrentUserLogin() {
SecurityContext securityContext = SecurityContextHolder.getContext();
Authentication authentication = securityContext.getAuthentication();
String userName = null;
if (authentication != null) {
log.info("authentication is not null");
if (authentication.getPrincipal() instanceof UserDetails) { //failing here
log.info("principle is instance of userdetails");
UserDetails springSecurityUser = (UserDetails) authentication.getPrincipal();
log.info(springSecurityUser.getUsername());
userName = springSecurityUser.getUsername();
} else if (authentication.getPrincipal() instanceof String) {
userName = (String) authentication.getPrincipal();
}
}
return userName;
}
Its failing at the line
if(authentication.getPrincipal() instanceof UserDetails)
What is the possible and the best way to handle this to update user details object.
Update:
#Transactional(readOnly = true)
public User getUserWithAuthorities() {
log.info("======inside getUserWithAuthorities =================");
log.info("current user is :::::::"+SecurityUtils.getCurrentUserLogin());
Optional<User> optionalUser = userRepository.findOneByLogin(SecurityUtils.getCurrentUserLogin());
User user = null;
if (optionalUser.isPresent()) {
user = optionalUser.get();
user.getAuthorities().size(); // eagerly load the association
}
return user;
}
Its trying to fetch the user from db. But the user is not present in the database
Similar to the LDAP tip, I would reocmmend creaing an OktaUserDetails class and casting the principal. Then you can keep most of the authentication code the same. The LDAP code example is below, the format of OktaUserDetails would depend on the JSON response
} else if (authentication.getPrincipal() instanceof LdapUserDetails) {
LdapUserDetails ldapUser = (LdapUserDetails) authentication.getPrincipal();
return ldapUser.getUsername();
}
To save information received from an Oauth2 resource, declare a PrincipalExtractor Bean in your SecurityConfiguration. This lets you parse the response in a custom manner. A basic example is below (source).
#Bean
public PrincipalExtractor principalExtractor(UserRepository userRepository) {
return map -> {
String principalId = (String) map.get("id");
User user = userRepository.findByPrincipalId(principalId);
if (user == null) {
LOGGER.info("No user found, generating profile for {}", principalId);
user = new User();
user.setPrincipalId(principalId);
user.setCreated(LocalDateTime.now());
user.setEmail((String) map.get("email"));
user.setFullName((String) map.get("name"));
user.setPhoto((String) map.get("picture"));
user.setLoginType(UserLoginType.GOOGLE);
user.setLastLogin(LocalDateTime.now());
} else {
user.setLastLogin(LocalDateTime.now());
}
userRepository.save(user);
return user;
};
}
I am trying to cache the UserDetails loadUserByUsername(String username)
the problem is that after the caching the results comes with the correct user but
the password is always set to null but it was not null when cached
#Service
public class MyUserDetailsService implements UserDetailsService {
#Autowired
UserRepository userRepository;
#Cacheable(value="usersLogged" ,key="#username" ,unless="#result.password==null")
#Override
public org.springframework.security.core.userdetails.User loadUserByUsername(
String username) throws UsernameNotFoundException {
try {
// User user = userRepository.getUserByEmail(username); Switch to id
// token base
User user = userRepository.findOne(username);
if (user == null) {
throw new UsernameNotFoundException(
"Invalid username/password.");
}
boolean accountNonExpired = true;
boolean credentialsNonExpired = true;
boolean accountNonLocked = user.isActive();
String userN = user.getId(); // the suer is in the system
String pass = user.getPassword();
Collection<? extends GrantedAuthority> authorities = AuthorityUtils
.createAuthorityList(user.getRole().toString());
org.springframework.security.core.userdetails.User userBuild = new org.springframework.security.core.userdetails.User(
userN, pass, user.isEnabled(), accountNonExpired,
credentialsNonExpired, accountNonLocked, authorities);
return userBuild;
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
return null;
// throw new
// UsernameNotFoundException("Invalid username/password.");
}
}
}
Seems like spring cache have problems caching when public visibility
password is protected
In the manual
When using proxies, you should apply the cache annotations only to methods with public visibility. If you do annotate protected, private or package-visible methods with these annotations, no error is raised, but the annotated method does not exhibit the configured caching settings. Consider the use of AspectJ (see below) if you need to annotate non-public methods as it changes the bytecode itself