Please bare with me as I am learning Spring Data REST as I go. Definitely feel free to suggest a safer approach if what I am proposing here is not the safest approach or even possible.
Problem
A user logs into my Spring Data REST API via Google OAuth2. After logging in, I need to get the user's ID, which is just the value of their primary key, from the User table. The reason I need the ID is to restrict access to endpoints such as /users/{id}. If a user's ID is 1, then he is only allowed to view /users/1, unless he is an Administrator.
Current Login Architecture
This portion of the application works as expected:
OAuth2AuthenticationSuccessHandler.java:
#Component("oauth2authSuccessHandler")
public class OAuth2AuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
#Autowired
private UserDataRestRepository userRepository;
#Autowired
private RandomStringGenerator randomStringGenerator;
#Autowired
private PasswordEncoder passwordEncoder;
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
OAuth2AuthenticationToken authenticationToken = (OAuth2AuthenticationToken) authentication;
String email = authenticationToken.getPrincipal().getAttributes().get("email").toString();
if (!userRepository.existsByEmail(email)) {
// If email not found, create local user account.
String firstName = CaseUtils.toCamelCase(
authenticationToken.getPrincipal().getAttributes().get("given_name").toString().toLowerCase(),
true);
String lastName = CaseUtils.toCamelCase(
authenticationToken.getPrincipal().getAttributes().get("family_name").toString().toLowerCase(),
true);
// Generate temporary username.
BigInteger usernameSuffix = userRepository.getNextValSeqUserUsername();
String username = "User" + usernameSuffix;
// Generate temporary password.
String password = randomStringGenerator.generate(20).toUpperCase();
// Encode Password.
String encodedPassword = passwordEncoder.encode(password);
User newUser = new User();
newUser.setFirstName(firstName);
newUser.setLastName(lastName);
newUser.setUsername(username);
newUser.setUserSetUsername(false);
newUser.setEmail(email);
newUser.setPassword(encodedPassword);
newUser.setUserSetPassword(false);
newUser.setCreated(new Timestamp(System.currentTimeMillis()));
newUser.setDisabled(false);
newUser.setLocked(false);
userRepository.save(newUser);
}
// Redirect to root page.
redirectStrategy.sendRedirect(request, response, "/");
}
}
UserDataRestRepository.java:
#RepositoryRestResource(collectionResourceRel = "users", path = "users")
public interface UserDataRestRepository extends PagingAndSortingRepository<User, Integer>, CrudRepository<User, Integer> {
public boolean existsByEmail(String email);
#Query(value="SELECT NEXT VALUE FOR Seq_User_Username", nativeQuery=true)
public BigInteger getNextValSeqUserUsername();
}
OAuth2LoginSecurityConfig.java:
#Configuration
#EnableWebSecurity
public class OAuth2LoginSecurityConfig {
#Autowired
private AuthenticationSuccessHandler oauth2authSuccessHandler;
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorize -> authorize
.anyRequest()
.authenticated()
)
.oauth2Login()
.successHandler(oauth2authSuccessHandler);
return http.build();
}
}
Database Tables
User:
Role:
UserRole:
Current Post-authentication Problems
Currently, after a user logs in via OAuth2, we do not have access to the user's ID (which is the value of their primary key in the User table) unless we access the Authentication object, get the email address, then get the user's ID from the User table based on the email address. Basically: Select Id from User where Email = user#example.com
Proposed Login Architecture
I am wondering if the UserDetailsService can solve the problem, illustrated in green:
My train of thought is: by getting the user's info from the User table, then loading it into the UserDetailsService, the entire application now has access to the user's ID (primary key) and all of the other info in that row via the UserDetailsService.
Thanks for your help.
I would not include a round-trip to the DB for each incoming request on resource-server(s), this is a waste of resources.
Have you considered using an authorization-server in front of Google and either directly bound to your user database, or capable of calling a web-service to fetch additional data? Many propose it, along with "login with Google".
You would have complete control of access and ID tokens claims: you can add about anything as private claim, but the request to the DB (or Web-Service) happens only once per token issuance (and not once per request to a resource-server). And if you define an Authentication of your own (easy, just extend AbstractAuthenticationToken), you can put accessors to cast those private claims from Object to something more relevant to your security / domain model.
I demo something similar (add a private claim to access-token with a value returned by a web-service and then use it for access-control with custom Authentication and security DSL) in this project.
Related
Spring Authorization Server OAuth 2.1.
How can i programatically simulate the authorization_code grant?
Since all grants except for authorization_code and client_credentials have been dropped this has become quite a headache.
The scenario calls for a #Scheduled job to login as a specific user where the client credentials are encoded properties within the server performing the login.
The user roles are important when executing downstream resources and is considered a regular user of the registered Client.
Using the Password grant was perfect for this scenario in OAuth 2.0.
Before i start hacking our Spring Auth server and implement a Password grant for registered resources or maybe overloading the client_credentials for user_credentialed payloads.
Quite a pain if you ask me, so please enlighten me? Are there any patterns for implementing this that i have not yet discovered?
While I'm curious what specific use case you have that needs to perform tasks as a particular user (as opposed to a single confidential client), it should still be possible with customization.
maybe overloading the client_credentials for user_credentialed payloads
This approach makes the most sense to me as a way to adapt supported flows in OAuth 2.1 to emulate a deprecated flow like the resource owner password grant. You can use a variation of this github gist, extending it with your user's authorities if needed. One possible solution might look like the following:
#Component
public final class DaoRegisteredClientRepository implements RegisteredClientRepository {
private final RegisteredClient registeredClient;
private final UserDetailsService userDetailsService;
public DaoRegisteredClientRepository(RegisteredClient registeredClient, UserDetailsService userDetailsService) {
this.registeredClient = registeredClient;
this.userDetailsService = userDetailsService;
}
#Override
public void save(RegisteredClient registeredClient) {
throw new UnsupportedOperationException();
}
#Override
public RegisteredClient findById(String id) {
return this.registeredClient.getId().equals(id) ? this.registeredClient : null;
}
#Override
public RegisteredClient findByClientId(String clientId) {
UserDetails userDetails;
try {
userDetails = this.userDetailsService.loadUserByUsername(clientId);
} catch (UsernameNotFoundException ignored) {
return null;
}
return RegisteredClient.from(this.registeredClient)
.clientId(userDetails.getUsername())
.clientSecret(userDetails.getPassword())
.clientSettings(ClientSettings.builder().setting("user.authorities", userDetails.getAuthorities()).build())
.build();
}
}
This uses a single client registration, but makes use of a UserDetailsService to resolve a subject representing your user's username and a secret which is actually the user's password. You would then need to provide an #Bean of type OAuth2TokenCustomizer<JwtEncodingContext> to access the user.authorities setting and add those authorities to the resulting access token (JWT) using whatever claim your resource server expects them in.
Alternatively, you could just override the scopes parameter of the returned RegisteredClient if desired.
I had the similar problem and ended up creating a password grant emulation servlet filter. Please refer to my example:
https://github.com/andreygrigoriev/spring_authorization_server_password_grant
I've used Spring Security multiple times on several projects, including 3 legged OAuth2 authentication on Zuul API Gateway, etc. All works brilliant and official documentation is very neat and simple.
But there is one point that I still don't get from docs. Imagine you have a spring based Resource Server with several ID Providers, and also you have your own user database and form login.
Thus, users can be authenticated either via form login or via one of IDPs (let's say Google or Facebook).
The question is: how to match Authentication from any of your IDPs to Authentication object that is enhanced by/mapped to your local user?
I.e.: Alice has an account in your system (in your database). She goes into her "profile" and declares that she also has a Google or Facebook account. OK, done, you save this info somewhere in your system.
Now, when Alice login into your system via social network, what spring API do you use to understand that Alice entered via Google is exact same Alice that is already registered in your DB? In what API do you enhance her Authentication with authorities based on your DB?
Thanks in advance, guys
The way this is typically done is by creating a composite that contains both the OidcUser object and your domain object. For example:
#Component
public class MyOAuth2UserService implements OAuth2UserService<OidcUserRequest, OidcUser> {
private final OidcUserService delegate = new OidcUserService();
#Override
public OidcUser loadUser(OidcUserRequest oidcUserRequest) {
// the information that comes back from google, et al
OidcUser oidcUser = this.delegate.loadUser(oidcUserRequest);
// the information from your DB
MyUser user = this.myRepository.findUserByXYZ(oidcUser.getXYZ());
return new MyOidcUser(user, oidcUser);
}
private static class MyOidcUser extends MyUser implements OidcUser {
private final OidcUser delegate;
public MyOidcUser(MyUser user, OidcUser oidcUser) {
super(user);
this.delegate = oidcUser;
}
// ... implement delegate methods
}
}
Note that XYZ is some attribute that allows you to know that the user from Google is the user from your system. Maybe that's the email address, for example.
The benefit to this extra bit of work is that Spring Security will place this MyOidcUser object into Authentcation#getPrincipal. So now, if you need to get your domain bits, you do (MyUser) authentication.getPrincipal(), but if you need the OIDC bits, you do (OidcUser) authentication.getPrincipal(). Depending on your use cases, you may be able to do something as simple as:
#GetMapping("/endpoint1")
public String endpoint1(#AuthenticationPrincipal MyUser myUser) {
// ...
}
#GetMapping("/endpoint2")
public String endpoint2(#AuthenticationPrincipal OidcUser oidcUser) {
URL issuer = oidcUser.getIdToken().getIssuer();
// ...
}
I have two web applications (A and B).
The first web application (A) is used as a reverse proxy using spring-cloud.
I'm using spring-session to store the sessions in a redis database for both applications.
The problem
When I modify a field (e.g name) of the current (logged in) user, the current logged in user object is not updated immediately and as a result, when I'm trying to retrieve current logged in user in a next call (via #AuthenticationPrincipal) I get a non-updated user object.
My custom user details object:
public class CustomUserDetails extends my.package.User implements org.springframework.security.core.userdetails.UserDetails, java.io.Serializable {
// ...
}
How can I update the current user object immediately?
Recently I've had the similar issue and I resolved it in the following manner:
1.Created a custom Authentication class
public class MyCustomAuthentication implements Authentication {
private UserDetails userDetails;
public MyCustomAuthentication(UserDetails userDetails) {
this.userDetails = userDetails;
}
...
#Override
public Object getDetails() { return userDetails; }
#Override
public Object getPrincipal() { return userDetails; }
#Override
public boolean isAuthenticated() { return true; }
...
}
update userDetails object with some fresh data (I guess, 'name' in your case)
Set new authentication created from userDetails in SecurityContextHolder
SecurityContextHolder.getContext().setAuthentication(new MyCustomAuthentication(userDetails));
Hope you will find that helpful.
I am new to Spring security and learnt the concepts of AuthenticationInterceptor, AuthenticationManager, UserLoginService. I know how to implement the userloginservice for checking a valid user in the system. The problem is we have two types of users 1)Specialists 2) Users and their credentials are stored in two different tables. The implementation of UserDetailsService should check both tables to authenticate and get the details of the user. Is there possibility to use multiple authentication providers and use the appropriate one based upon the type of user? or Do I have to use the same authentication provider to query both tables to find out the user validity? What is the best way to handle this scenarios when the user has to be searched in two tables?
I think you can do sth like that:
#Autowired
private UsersDAO usersDAO;
#Autowired
private OtherDAO otherDAO;
#Override
#Transactional
public UserDetails loadUserByUsername(final String username) throws UsernameNotFoundException {
Users user = usersDAO.find(username);
Other oth = otherDAO.find(username);
// your queries
if(check if your desire is correct based on user){
List<GrantedAuthority> authorities = buildUserAuthority(user.getUserRole());
return buildUserForAuthentication(user, authorities);
} else {
//...
}
}
I am using spring security for the authentication purposes in my project wherein after successful authentication, I get the principal object inside which the various details are stored.
This principal object is passed to various methods which allow the entries to be reflected in the database against the current user. In short, principal helps me in giving principal.getName() everywhere i need it.
But now when I login through spring social then I do not have principal object of Principal in hand, instead I have implemented MyPrincipal class --->
public class MyPrincipal implements Principal {
public String name;
public boolean flag;
public boolean isflag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
public void setName(String name) {
this.name = name;
}
#Override
public String getName() {
return name;
}
}
Then in the social login handler, I am adding the current username and flag value to myPrincipal object, and forwarding the user to the same home page where the spring security forwards in case of normal login.
MyPrincipal myPrincipal = new MyPrincipal();
myPrincipal.name = username;
myPrincipal.socialFlag = true;
modelMap.addAttribute("myPrincipal", myPrincipal);
return new ModelAndView("forward:/home");
Adding this object in session by annotating class with
#SessionAttributes({"myPrincipal"})
Now from here on-wards I want the flow to be handed over to the home page with all the functionality working for the user correctly. But each method is taking Principal principal as argument, just like this -->
#RequestMapping(value = {"/home"}, method = RequestMethod.POST)
#ResponseBody
public ModelAndView test(ModelMap modelMap, Principal principal) {
String name = principal.getName();
}
There are two different things going around in both cases-
Normal login is giving me principal directly but social login is giving me it in session attributes.
I do not want to pass principal as parameters even in case of normal spring security login, instead here also I want to put it in session attribute.
How can I do this and where to make the changes when I have implemented my own authentication provider.
I don't think I fully understand...However, in general it shouldn't be necessary to pass principal instances around. Use org.springframework.security.core.context.SecurityContextHolder.getContext() to get a hold of the context then call SecurityContext.getAuthentication().getPrincipal() or SecurityContext.getAuthentication().getDetails().