Spring Social, Create connection for existing local users on signin - spring

I am trying to allow existing local users to signin using Spring Social, primarily google, and automatically create a new userconnection if email matches local account.
I have only found examples for implicit signup (requires/creates a new account), new user signup (default action if SpringSocial fails to find a UserConnection) and the connect option while the current user is logged in.
How can I extend SpringSocial to let me check for existing social user / local user email match, create a user connection to existing local user and then sign them into both local and social?

You can use the implicit sign up to find an account instead of creating a new one.
Modifying the code found for implicit sign up, you can do something similar to the following:
public class AccountConnectionSignUp implements ConnectionSignUp {
private final AccountRepository accountRepository;
public AccountConnectionSignUp(AccountRepository accountRepository) {
this.accountRepository = accountRepository;
}
public String execute(Connection<?> connection) {
UserProfile profile = connection.fetchUserProfile();
Account account = accountRepository.findAccountByEmail(profile.getEmail());
return account == null ? null : account.getUsername();
}
}
You will need to implement Account and AccountRepository to support an email property, then use the repository to find account by email. If the repository cannot find an account, return null.

Related

Spring Security OAuth2 with multiple IDPs and form login: matching users

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();
// ...
}

Spring security oauth2: create user if not exists, provision user

Here is an excerpt from spring oauth2 tutorial:
How to Add a Local User Database
Many applications need to hold data about their users locally, even if
authentication is delegated to an external provider. We don’t show the
code here, but it is easy to do in two steps.
Choose a backend for your database, and set up some repositories (e.g.
using Spring Data) for a custom User object that suits your needs and
can be populated, fully or partially, from the external
authentication.
Provision a User object for each unique user that logs in by
inspecting the repository in your /user endpoint. If there is already
a user with the identity of the current Principal, it can be updated,
otherwise created.
Hint: add a field in the User object to link to a unique identifier in
the external provider (not the user’s name, but something that is
unique to the account in the external provider).
So, in the user controller we have the following code:
#RequestMapping("/user")
public Map<String, Object> user(Principal user) {
Map<String, Object> map = new HashMap<String, Object>();
// for a facebook the name is facebook id, not an actual name
map.put("name", user.getName());
map.put("roles", AuthorityUtils.authorityListToSet(((Authentication) user)
.getAuthorities()));
return map;
}
Dave Syer(spring maintainer) suggests to:
Downcast the Principal to an Authentication (or possibly
OAuth2Authentication and grab the userAuthentication from that) and
then look in the details property. If your user was authenticated
using a UserInfoTokenServices you will see the Map returned from the
external provider's user info endpoint.
But for me that seems unnatural by two reasons:
Why should we do that in controller call...it smells.
Why should we do all that instanceof and casts..smells as well.
Wouldn't it be better to plugin the OAuth2AuthenticationManager instead: org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationManager
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// ...
OAuth2Authentication auth = tokenServices.loadAuthentication(token);
// ...
auth.setDetails(authentication.getDetails());
auth.setAuthenticated(true);
// here we could've create our own Authentication object
// to set as Principal with application specific info.
// Note: no casts are required since we know the actual auth type
return auth;
}
What's the best way of creating a user after oauth2 dance completed?

Spring social oauth how to get sign in provider in SocialUserDetailsService?

In my Spring Social app, I'm trying to integrate certain Social Login functionalities. After being redirected from, for example Twitter, Spring calls the following to look up the user.
public class SimpleSocialUserDetailsService implements SocialUserDetailsService {
#Override
public SocialUserDetails loadUserByUserId(String userId) throws UsernameNotFoundException, DataAccessException {
/*
Commented
*/
}
However, since I will have multiple social login providers, the userId alone is not enough for me to look up the user in my database. I need at least the sign in provider or access token.
Is there anyway to get the sign in provider, or more information, in SocialUserDetailsService? Any other way to solve my problem would be great!
Spring Social is rather Agnostic to the Sign in Providers when properly implemented. I believe you are confused on the flow of Spring Social. At the point you describe spring social has already looked up the connections table and presumably found a record, so it looks up your user table for the user matching with userId (as referenced in the connections table) This is usually associated with the username.
This connection <-> User matching is done in the SocialAuthenticationProvider before calling the SocialUserDetails loadUserByUserId method.
So the SocialAuthenticationProvider already does what you ask for by querying the usersConnectionRepository and comparing the provider connection to find the appropriate user.
Now for your case you would can go ahead and override the user service that you have setup. As long as the userId used on the doPostSignUp call matches the one you look up in the loadUserByUserId, the proper user will be retrieved.
This is a sample:
Wherever your signup logic is executed, you call the doPostSignup and pass the desired user id (Username or another uniquely identifiable String)
ProviderSignInUtils.doPostSignUp(variableForNewUserObject.getId().toString(), request);
Now you Override the loadUserByUserId in SimpleSocialUserDetailsService
#Autowired
private UserDetailsService userDetailsService;
#Override
public SocialUserDetails loadUserByUserId(String userId) throws UsernameNotFoundException, DataAccessException {
UserDetails userDetails = userDetailsService.loadUserByUsername(userId);
return (SocialUserDetails) userDetails;
}

How to get custom User object from AuthenticationFailureBadCredentialsEvent / AuthenticationSuccessEvent object

I'm trying to show no of invalid attempts of user using spring security.I'm using a custom User class to get additional user details other than username and password. I've created two listener classes i.e. AuthenticationSuccessEventListener & AuthenticationFailureListener to update user's invalid attempts.
Now in the onApplicationEvent method i'm trying to get custom User object (CustomUserDetails) like shown below:
#Component
public class AuthenticationFailureListener implements ApplicationListener<AuthenticationFailureBadCredentialsEvent> {
#Autowired
private ILoginDAO loginDAO ;
#Override
public void onApplicationEvent(AuthenticationFailureBadCredentialsEvent event) {
CustomUserDetails user = (CustomUserDetails)event.getAuthentication().getPrincipal();//I get ClassCastException here.
String strUserID = user.getUserID();
CustomUserDetails customUser = loginDAO.loadUserByUsername(strUserID);
if (customUser != null){
...
} } }
event.getAuthentication().getPrincipal() returns a String i.e. username which i'm trying to cast it to CustomUserDetails (custom User class) and i get error.
P.S - I'm entering userid/password in login page and hence i pass userid as parameter for all the methods including loadUserByUsername(strUserID).
How can i get my custom User object in the listener class from AuthenticationFailureBadCredentialsEvent / AuthenticationSuccessEvent object?
The event just contains the authentication request object, i.e. the Authentication which was passed to the AuthenticationManager and which failed. So it will contain the submitted username as the principal.
Authentication may have failed for a variety of reasons, including a non-existent username, and in fact doesn't even need to involve a UserDetails object at all, so if you want the full information you will need to load it using the username.
Alternatively you could customize an AuthenticationProvider to perform the additional work you want in the implementation itself, rather than via an event.

Spring Security 2.0 Facebook Connect

I´m trying to implement Facebook login to our Spring Framework 2.5 based application which has usual login/password authentication already.
I got to this part of code in my controller where i have user´s Facebook data in userFb object:
String code = request.getParameter("code");
String accessToken = fbStuffKeeper.retrieveAccessToken(code);
facebookClient = new DefaultFacebookClient(accessToken);
User userFb = facebookClient.fetchObject("me", User.class);
I can check if user is already in DB and if not I am able to store him in a DB and store his FB_uid and other necessary credentials.
Problem is that I want to login this user after some condition if he is already authorized to system.
My current authentication manager decides by this rule:
<authentication-provider>
<jdbc-user-service data-source-ref="dataSource"
users-by-username-query="select username, password, confirmed, 1 AS enabled FROM person WHERE confirmed=1 and username=?"
authorities-by-username-query="select username, authority from person where username=?"/>
<password-encoder hash="md5"/>
</authentication-provider>
So my question is: How do I login user found in DB who tries to connect using Facebook? Another authentication provider? Is there any way to do his login using some similar code as is mine mentioned one?
Thank you very much.
***EDIT:
Thank you very much. It looks it might help. I have now one user in DB which I want to login. I made this code in the controller:
User userFb = facebookClient.fetchObject("me", User.class);
Person person = personDao.getPersonByFbUid(userFb.getId());
GrantedAuthority[] grantedAuthorities = new GrantedAuthorityImpl[1];
grantedAuthorities[0] = new GrantedAuthorityImpl(person.getAuthority());
Authentication a = new FacebookAuthenticationToken(grantedAuthorities, person);
a.setAuthenticated(true);
SecurityContextHolder.getContext().setAuthentication(a);
I also created FacebookAuthenticationToken:
public class FacebookAuthenticationToken extends AbstractAuthenticationToken {
private Person person;
public FacebookAuthenticationToken(GrantedAuthority[] authorities, Person person) {
super(authorities);
this.person = person;
}
public Object getCredentials() {
return person;
}
public Object getPrincipal() {
this.person;
} /* + Person getter and setter */
And all works fine now. Thank you once more for your help.
One possible way is following. Although it may not be perfect, but it works well.
Create a FacebookAuthenticationToken class, that extends AbstractAuthenticationToken and holds your facebook user object (assuming it to be FacebookUser).
After your code you mentioned above, append the following code right after it into the same controller:
String code = request.getParameter("code");
String accessToken = fbStuffKeeper.retrieveAccessToken(code);
facebookClient = new DefaultFacebookClient(accessToken);
User userFb = facebookClient.fetchObject("me", User.class);
// Find FacebookUser with current FB_uid, create new if found none, load its information in an instance of FacebookUser
Authentication a = new FacebookAuthenticationToken(fbUserObject);
a.setAuthenticated(true);
SecurityContextHolder.getContext().setAuthentication(a);
// your authentication is complete, redirect to some other page

Resources