How to modify spring SecurityContextHolder.getContext().getAuthentication() object after successful login? - spring

I'm working on spring boot application where i've created a CustomUserDetails class by extending UserDetails as follows..
public class CustomUserDetails
extends org.springframework.security.core.userdetails.User {
private static final long serialVersionUID = 1L;
/**
* The extra field in the login form is for the tenant name
*/
private String tenant;
private Long userId;
private String firstName;
private String middleName;
private String lastName;
private String email;
private String role;
i need to modify tenant details in UserDetails object. For this i've checked following
How to update Spring Security UserDetails impls after successful login?
https://stackanswers.net/questions/how-to-immediately-enable-the-authority-after-update-user-authority-in-spring-security
https://dev.to/onlineinterview/user-account-loginregistration-feature-with-spring-boot--3fc3
And Controller is here where i'm updating authentication object:
#PreAuthorize("hasRole('SUPER_ADMIN')")
#GetMapping(path = "/useTenant/{tenantId}", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public ResponseEntity<ResponseDTO> useTenant(#PathVariable Long tenantId) {
HttpStatus status = HttpStatus.OK;
boolean error = false;
String message = languageMessageService.getMessage(MultiLanguageKey.SUCCESS);
// fetch master tenant by id
Optional<MasterTenant> optional = masterTenantService.findById(tenantId);
if (optional.isPresent()) {
CustomUserDetails customUserDetails = customUserDetailsService.getUserDetail();
//Changing Tenant ID
customUserDetails.setTenant(optional.get().getTenantId());
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication instanceof UsernamePasswordAuthenticationToken) {
// Update Current user by changing tenant id in SecurityContextHolder
UsernamePasswordAuthenticationToken auth = (UsernamePasswordAuthenticationToken) authentication;
auth.setDetails(customUserDetails);
SecurityContextHolder.getContext().setAuthentication(auth);
}
} else {
error = false;
message = languageMessageService.getMessage(MultiLanguageKey.TENANT_NOT_FOUND);
}
return new ResponseEntity<>(new ResponseDTO(error, message), status);
}
My problem is that when i'm hitting another request to perform particular action, i didn't find tenant detail in CustomUserDetails object which is fetched from
SecurityContextHolder.getContext().getAuthentication()
Please let me know how can i update or modify UserDetails object of Authentication and save back so another request get updated CustomUserDetails.

The UserDetails should be set to the Principal of the UsernamePasswordAuthenticationToken rather than Details as suggested by the java docs :
The AuthenticationManager implementation will often return an
Authentication containing richer information as the principal for use
by the application. Many of the authentication providers will create a
UserDetails object as the principal.
Details in UsernamePasswordAuthenticationToken is normally stored user 's IP address or certificate serial number etc.
So change it to :
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication instanceof UsernamePasswordAuthenticationToken) {
// Update Current user by changing tenant id in SecurityContextHolder
UsernamePasswordAuthenticationToken currentAuth = (UsernamePasswordAuthenticationToken) authentication;
CustomUserDetails userDetail = currentAuth.getPrincipal();
customUserDetails.updateTenanet("blablalb");
UsernamePasswordAuthenticationToken updateAuth = new UsernamePasswordAuthenticationToken(userDetail ,
currentAuth.getCredentials(),
currentAuth.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(updateAuth);
}

Related

How to make register and login work correctly in spring security, problems with authentication

I'm trying to make authorization in my project. Registration works great and I get access and refresh tokens, but login throws an exception.
Problem is that in registration I create UsernamePasswordAuthenticationToken with UserEntity and password:
#PostMapping("/register")
public ResponseEntity register(#RequestBody UserSignUpModel userSignUpModel) {
UserEntity user = customUserDetailsService.createUser(userSignUpModel);
Authentication authentication = UsernamePasswordAuthenticationToken.authenticated(user, user.getPassword(), Collections.EMPTY_LIST);
return ResponseEntity.ok(tokenGenerator.createToken(authentication));
}
And in login I create UsernamePasswordAuthenticationToken with email and password so that the daoAuthenticationProvider can process it:
#PostMapping("/login")
public ResponseEntity login(#RequestBody UserSignInModel userSignInModel) {
Authentication authentication = daoAuthenticationProvider.authenticate(UsernamePasswordAuthenticationToken.unauthenticated(userSignInModel.getEmail(), userSignInModel.getPassword()));
return ResponseEntity.ok(tokenGenerator.createToken(authentication));
}
So I receive different authentication objects and can't pass the instanceoff check in TokenGenerator:
public TokenModel createToken(Authentication authentication) {
if (!(authentication.getPrincipal() instanceof UserEntity user)) {
throw new BadCredentialsException(
MessageFormat.format("principal {0} is not of UserEntity or UserDetails type", authentication.getPrincipal().getClass())
);
}
TokenModel tokenModel = new TokenModel();
tokenModel.setUserId(user.getUser_id());
tokenModel.setAccessToken(createAccessToken(authentication));
String refreshToken;
if (authentication.getCredentials() instanceof Jwt jwt) {
Instant now = Instant.now();
Instant expiresAt = jwt.getExpiresAt();
Duration duration = Duration.between(now, expiresAt);
long secondsUntilExpired = duration.toSeconds();
if (secondsUntilExpired < 15) {
refreshToken = createRefreshToken(authentication);
} else {
refreshToken = jwt.getTokenValue();
}
} else {
refreshToken = createRefreshToken(authentication);
}
tokenModel.setRefreshToken(refreshToken);
return tokenModel;
}
Should I write custom authentication provider? If so, do I need to download an entity from the repository and check passwords in it?

Extends Spring security user class

I'm Working on a Spring security project . I try to extends the security.core.userdetails.User class to add more details while registering the users.
User Extended class
public class UserDetails extends User {
private int id;
private String Country;
public UserDetails(String username, String password, Collection<? extends GrantedAuthority> authorities, int id,
String country) {
super(username, password, authorities);
this.id = id;
Country = country;
}
public UserDetails(String username, String password, boolean enabled, boolean accountNonExpired,
boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities,
int id, String country) {
super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
this.id = id;
Country = country;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getCountry() {
return Country;
}
public void setCountry(String country) {
Country = country;
}
I have also added Id and country in my entity class(model class).
But when i try to register the user .
It give an error.org.springframework.dao.DataIntegrityViolationException: PreparedStatementCallback; SQL [insert into users (username, password, enabled) values (?,?,?)]; Field 'id' doesn't have a default value; nested exception is java.sql.SQLException: Field 'id' doesn't have a default value
(The value of id and country is hard coded)
Controller class
try {
List<GrantedAuthority> authority = new ArrayList<>();
authority.add(new SimpleGrantedAuthority(form.getRole()));
String encodedPassword = passwordEncoder.encode(form.getPassword());
UserDetails details = new UserDetails(form.getUsername(), encodedPassword, authority, 10 ,"India");
System.out.println(details.getId()+" "+details.getCountry() +" "+details.getUsername());
System.out.println(details);
detailsManager.createUser(details);
}
OUPUT
10 India alpha#gmail.com
com.example.demo.model.UserDetails [Username=alpha#gmail.com, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_ADMIN]]
I don't know why its is calling the parent class constructor.
The SQL is incorrect. Spring Security's INSERT by default populates the username, password, and enabled columns. However, the users table you created requires an id column as well. Since the query doesn't specify the value, it fails.
You could try extending JdbcUserDetailsManager's various methods to be aware of your id field as well. You'd need to at least extend createUser so it adds the id to the INSERT statement and findUserByUsername so it constructs your custom object.
A better way, though, would be to use Spring Data. This allows your domain object to be independent of Spring Security. Also, Spring Data has much broader SQL support.
It might be helpful to call your class something different than a Spring Security interface. So, let's imagine that your custom class (the one with the id) is called YourUser (instead of UserDetails). Now, you can wire a Spring Data-based UserDetailsService to Spring Security like so:
#Service
public class YourUserRepositoryUserDetailsService implements UserDetailsService {
private final YourUserRepository users; // your Spring Data repository
// ... constructor
#Override
public UserDetails findUserByUsername(String username) {
YourUser user = this.users.findByUsername(username)
.orElseThrow(() -> new UserNotFoundException("not found"));
return new UserDetailsAdapter(user);
}
private static class UserDetailsAdapter extends YourUser implements UserDetails {
UserDetailsAdapter(YourUser user) {
super(user); // copy constructor
}
// ... implement UserDetails methods
}
}
This UserDetailsService replaces the JdbcUserDetailsManager that you are publishing.

Return OAuth Token information on user creation endpoint

I'm currently implementing a REST Api using Spring Boot and secured via OAuth2. This is my current situation:
User registration endpoint, a sample success response and the controller method implementation
POST http://<host>/users/register
{
"id": "6061b5c0-a817-4fff-ba1f-c7f4e94080ed",
"name": "Name",
"email": "user#email.com",
"password": "$2a$12$R7yw/HbLmFzMpkzsWOqLp.I.itHRo7B/9MXKNrpArvK/Lfta0Z.I.",
"createdAt": "2019-12-14T22:00:46.682+0000"
...
}
#PostMapping(value = "/register")
public ResponseEntity<User> register(#RequestBody final User user) {
final User createdUser = mUsersService.create(user);
return new ResponseEntity<>(createdUser, HttpStatus.CREATED);
}
The token endpoint and a sample response
POST http://<host>/oauth/token
{
"access_token": "03bd76f0-20bd-45ef-9adb-b0903345e590",
"token_type": "bearer",
"refresh_token": "a1022bbd-407a-4899-b1b0-20a889ed0419",
"expires_in": 82373,
"scope": "read write"
}
How can I return the token JSON (as returned by the /oauth/token endpoint) on the /users/register endpoint.
Use the below method,
private OAuth2AccessToken getToken(LoginResource loginResource) {
String access_token_url = "http://localhost:8080/api/oauth/token";
ResourceOwnerPasswordResourceDetails resourceDetails = new ResourceOwnerPasswordResourceDetails();
resourceDetails.setGrantType("password");
resourceDetails.setAccessTokenUri(access_token_url);
//-- set the clients info
resourceDetails.setClientId("clientId");
resourceDetails.setClientSecret("clientSecret");
// set scopes
List<String> scopes = new ArrayList<>();
scopes.add("read");
scopes.add("write");
scopes.add("trust");
resourceDetails.setScope(scopes);
//-- set Resource Owner info
resourceDetails.setUsername(loginResource.getUserName());
resourceDetails.setPassword(loginResource.getPassword());
OAuth2RestTemplate oAuth2RestTemplate = new OAuth2RestTemplate(resourceDetails);
return oAuth2RestTemplate.getAccessToken();
}
In the end, after inspecting the data that goes into the /oauth/token endpoint, I came to the following solution:
private static final String PARAM_GRANT_TYPE = "grant_type";
private static final String PARAM_USERNAME = "username";
private static final String PARAM_PASSWORD = "password";
private static final String PARAM_GRANT_TYPE_VALUE_PASSWORD = "password";
#Autowired
private UsersService mUsersService;
#Autowired
private TokenEndpoint mTokenEndpoint;
#PostMapping(value = "/register")
public ResponseEntity<OAuth2AccessToken> register(final Principal principal,
#RequestParam final String name,
#RequestParam final String email,
#RequestParam final String password) throws HttpRequestMethodNotSupportedException {
final User user = mUsersService.create(name, email, password);
final Map<String, String> tokenParameters = new HashMap<>(3);
tokenParameters.put(PARAM_GRANT_TYPE, PARAM_GRANT_TYPE_VALUE_PASSWORD);
tokenParameters.put(PARAM_USERNAME, email);
tokenParameters.put(PARAM_PASSWORD, password);
return mTokenEndpoint.postAccessToken(principal, tokenParameters);
}
What I do is invoke the POST token endpoint with the forwarded Principal (in my case the client authentication) and the normal password grant_type parameters.

How to get the username (email in my case) in Spring Security [UserDetails/String]

I would like to get the e-mail which is username in my application to set the user which send a message. I decided to use typical method i.e. principal and getUsername():
#PostMapping("/messages/{id}")
#ResponseStatus(HttpStatus.CREATED)
public MessageDTO addOneMessage(#RequestBody MessageRequest messageRequest, #PathVariable ("id") Long id) {
checkIfChannelExists(id);
String content = messageRequest.getContent();
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
String username = ((UserDetails) principal).getUsername();
Employee author = employeeRepository.findByEmail(username).get();
Message message = new Message(content, author, id);
messageRepository.save(message);
return new MessageDTO(message);
}
And MessageRequest.java:
#Data
#NoArgsConstructor
#AllArgsConstructor
public class MessageRequest {
#NonNull
private String content;
}
But, in this way I still get:
"message": "java.lang.String cannot be cast to org.springframework.security.core.userdetails.UserDetails"
What is wrong in my implementation? To be more precise, I use Postman to test POST requests:
{
"content": "something"
}
If you only need to retrieve the username you can get it through Authentication ie.
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();
instead of typecasting to your class springcontext provide the almost all details about the user.
If you want the controller to get the user name to test.
please use this code.
//using authentication
#RequestMapping(value = "/name", method = RequestMethod.GET)
#ResponseBody
public String currentUserName(Authentication authentication) {
return authentication.name();
}
//using principal
#RequestMapping(value = "/name", method = RequestMethod.GET)
#ResponseBody
public String currentUserName(Principal principal) {
return principal.getName();
}

Update User's first name and last name in principal

I am updating user's information like first name and last name and I am getting first name and last name in all the pages for welcome message.
I have two controllers one for ajax request mapping and the other for normal request mapping.
Normal request mapping controller have this method. In this controller all page navigation is present and some request mapping which are not ajax calls
private String getPrincipalDisplay() {
GreenBusUser user = null;
String userName = "";
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
user = (GreenBusUser) principal;
userName = user.getFirstName() + " " + user.getLastName();
} else {
userName = "";
}
return userName;
}
This is how I am getting the username on every page by return string of this function I am adding it in ModelMap object.
When I update user's information I am doing in ajax request mapping.
#RequestMapping(value = "/restify/updateUserData", method = RequestMethod.PUT, headers = "Accept=application/json")
public ServiceResponse forgotPassword(#RequestBody Object user)
{
//logger.debug("getting response");
return setDataPut("http://localhost:7020/forgotPassword",user);
}
user is an Object type which has json data. Now how do I retrieve data from object and update my first name and last name in principal.
This is my GreenBusUser class
public class GreenBusUser implements UserDetails
{
private static final long serialVersionUID = 1L;
private String username;
private String password;
private Collection<? extends GrantedAuthority> grantedAuthorities;
private String firstName;
private String lastName;
public GreenBusUser(String username,String password,Collection<? extends GrantedAuthority> authorities,String firstName, String lastName)
{
this.username = username;
this.password = password;
this.grantedAuthorities = authorities;
this.firstName=firstName;
this.lastName=lastName;
this.grantedAuthorities.stream().forEach(System.out::println);
}
public Collection<? extends GrantedAuthority> getAuthorities()
{
return grantedAuthorities;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public String getPassword()
{
return password;
}
public String getUsername()
{
return username;
}
public boolean isAccountNonExpired()
{
return true;
}
public boolean isAccountNonLocked()
{
return true;
}
public boolean isCredentialsNonExpired()
{
return true;
}
public boolean isEnabled()
{
return true;
}
}
UPDATE:::::
I have updated your code and applied some part of your answer into mine but still I ran into a problem
#RequestMapping(value="/updateUser",method=RequestMethod.GET)
public String updateUser(ModelMap model) {
UserInfo user = getUserObject();
GreenBusUser newGreenBususer = null;
List<User> list = new ArrayList<User>();
list = FetchDataService.fetchDataUser("http://localhost:8060/GetuserbyUserName?username=" + getPrincipal(), user.getUsername(), user.getPassword());
logger.debug("new user list ----->>>"+list.size());
User newuser=(User)list.get(0);
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
SecurityContextHolder.getContext().getAuthentication().getPrincipal(), SecurityContextHolder.getContext().getAuthentication().getCredentials());
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
newGreenBususer=(GreenBusUser)principal;
logger.debug("newGreenBususerDetails---->>>"+newGreenBususer.toString());
newGreenBususer.setFirstName(newuser.getFirstName());
newGreenBususer.setLastName(newuser.getLastName());
if(newGreenBususer.getFirstName()!=null) {
logger.debug("got my first name");
}
if(newGreenBususer.getLastName()!=null) {
logger.debug("got my last name");
}
auth.setDetails(newGreenBususer);
SecurityContext context = SecurityContextHolder.getContext();
context.setAuthentication(auth);
SecurityContextHolder.setContext(context);
model.addAttribute("user", getPrincipalDisplay());
model.addAttribute("userData", list);
model.addAttribute("check", true);
return "GreenBus_updateProfile_User";
}
At first it sets the firstname and lastname to GreenBusUser and then there is setDetails method when I reload the page it says No user found when I am calling getUserObject() method at the top of this method.
private X2CUser getUserObject() {
X2CUser userName = null;
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
userName = ((X2CUser) principal);
} else {
logger.info("No user found");
}
return userName;
}
If you are updating the password, then it will be good to logout the user and tell him to relogin.
Try this code .. It might help you.
UsernamePasswordAuthenticationToken authReq = new UsernamePasswordAuthenticationToken(user, pass);
Authentication auth = authManager.authenticate(authReq);
SecurityContext sc = SecurityContextHolder.getContext();
securityContext.setAuthentication(auth);
I have finally resolved my problem though I have later added some code in my question part in UPDATE section.
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
newGreenBususer=(GreenBusUser)principal;
newGreenBususer.setFirstName(newuser.getFirstName());
newGreenBususer.setLastName(newuser.getLastName());
Yes that's all need to be done.
This part--->>
auth.setDetails(newGreenBususer);
SecurityContext context = SecurityContextHolder.getContext();
context.setAuthentication(auth);
SecurityContextHolder.setContext(context);
set new context making security pointing to null when I reload still not clear because I am setting the details before reload so its like I get new context but I have set the new user details.
Though I have finally resolved my problem but if anyone could shed some light why it was happening then I will accept his/her answer.
Thanks alot for your support. Keep Learning!

Resources