Table of Contents
Part-3: Authorising user using Spring Social (Google, Facebook, LinkedIn) and Spring Security
Introduction
In Part-1, We discussed how to authenticate with social providers like Facebook, Google, and LinkedIn. Along with that, we changed the default page flow for spring social, enabled it to be used by multiple users and provided support for Google.
In Part-2, We used spring-security to allow only logged in users to see secure pages, also we added registration page to register users.
So what is in this tutorial
Well if you have read and run the application up to Part-2 then let me tell you that there is a bug in the code. Also, there might be a case where social network providers may not send all the required data.
So, what is the bug?
Create an account using registration and then log out, log in again now using social provider(same email id). Now if you try to log in using the email/password combination, you will receive an error.
In Simple terms to replicate the above bug
- Register the user, using create an account and then log out
- Use the same email address and log in by a social provider and log out
- Now try using the email/password combination to log in and it will give an error
It won’t work because when we logged in through social provider it basically overwrote the password with the blank.
Also, the social provider may not provide all the information
All social providers send back the data differently and its very possible they may not send all the fields which you want for e.g Facebook may not return country, while Google may not return Title.
So what will we be doing
We will do a couple of things in this tutorial the first being
- Fix the bug, create a new method in UserRepository to save without password
- Check if we have received all the data if no send the user to a new page to collect all the information.
We will add a method in UserRepository Interface called saveWithoutPassword and in this method, we will save all fields except the password.
This way we will make sure that the passwords saved are not overwritten.
1 2 3 4 5 6 7 8 9 | @Modifying @Transactional @Query("UPDATE user SET firstName = :firstName, lastName = :lastName, country = :country, image = :image " + " WHERE email = :email") void saveWithoutPassword(@Param("firstName") String firstname, @Param("lastName") String lastname, @Param("country") String country, @Param("image") String image, @Param("email") String email); |
Adding method in BaseProvider
In Base Provider class we will check if the password is empty, which indicates the user might have logged in from the Social provider so save all details except passwords by calling the saveWithoutPassword method.
1 2 3 4 5 6 7 8 9 | protected void saveUserDetails(UserBean userBean) { if (StringUtils.isNotEmpty(userBean.getPassword())) { userBean.setPassword(bCryptPasswordEncoder.encode(userBean.getPassword())); } userRepository.saveWithoutPassword(userBean.getFirstName(), userBean.getLastName(), userBean.getCountry(), userBean.getImage(), userBean.getEmail()); } |
Check if all information is available from Social Provider
Every Social Provider has some mandatory and not mandatory fields, which means they might not be capturing all the information. Also, some social providers may not provide all the information, which means that we should check if all information we need from our end is present and if absent ask the user to fill the missing information.
In BaseProvider class, we will add the method to check if all mandatory fields are not null and not blank.
1 2 3 4 5 6 7 8 9 | protected boolean isAllInformationAvailable(UserBean userBean) { return StringUtils.isNotEmpty(userBean.getEmail()) && StringUtils.isNotEmpty(userBean.getFirstName()) && StringUtils.isNotEmpty(userBean.getLastName()) && StringUtils.isNotEmpty(userBean.getTitle()) && StringUtils.isNotEmpty(userBean.getCountry()); } |
and add the below condition in the method before saving, where we check if any information is missing then redirect the user to “incompleteInfo” page.
1 2 3 4 5 6 7 | //Check if all Info has been collected if(!baseProvider.isAllInformationAvailable(userBean)) { model.addAttribute("userBean", userBean); return "incompleteInfo"; } |
the complete method will look like this, you will need to do same for all social providers(Facebook, Google, and LinkedIn). Below is an example of class FaceBookProvider method.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | public String getFacebookUserData(Model model, UserBean userBean) { ConnectionRepository connectionRepository = baseProvider.getConnectionRepository(); if (connectionRepository.findPrimaryConnection(Facebook.class) == null) { return REDIRECT_LOGIN; } //Populate the Bean populateUserDetailsFromFacebook(userBean); //Check if all Info has been collected if(!baseProvider.isAllInformationAvailable(userBean)) { model.addAttribute("userBean", userBean); return "incompleteInfo"; } //Save the details in DB baseProvider.saveUserDetails(userBean); //Login the User baseProvider.autoLoginUser(userBean); model.addAttribute("loggedInUser",userBean); return "secure/user"; } |
Adding Incomplete Info
Add the incompleteInfo.html in src/main/resources/templates folder. Basically, in this HTML file using thymleaf constructs, we will check which fields of Userbean has not been populated and render the HTML fields where they are null.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 | <!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <title>Login</title> <meta name="description" content="" /> <meta name="viewport" content="width=device-width" /> <base href="/" /> <link rel="stylesheet" type="text/css" href="/webjars/bootstrap/css/bootstrap.min.css" /> <script type="text/javascript" src="/webjars/jquery/jquery.min.js"></script> <script type="text/javascript" src="/webjars/bootstrap/js/bootstrap.min.js"></script> <link rel="stylesheet" href="/webjars/font-awesome/css/font-awesome.min.css"></link> </head> <body> <div class="container"> <h2>Sorry, but we could not find all the information so please fill the following fields</h2> <form th:action="@{/registration}" th:object="${userBean}" method="post"> <table> <tr th:unless="${userBean.email}"> <td>Email:</td> <td><input type="text" th:field="*{email}" /></td> <td th:if="${#fields.hasErrors('email')}" th:errors="*{email}">Name Error</td> </tr> <tr th:if="${userBean.email}"> <input type="hidden" th:field="*{email}" /> </tr> <tr th:unless="${userBean.title}"> <td>Title:</td> <td><select th:field="*{title}"> <option value="Mr" th:text="Mr"></option> <option value="Mrs" th:text="Mrs"></option> </select></td> <td th:if="${#fields.hasErrors('title')}" th:errors="*{title}">Title Error</td> </tr> <tr th:if="${userBean.title}"> <input type="hidden" th:field="*{title}" /> </tr> <tr th:unless="${userBean.firstName}"> <td>Email:</td> <td><input type="text" th:field="*{firstName}" /></td> <td th:if="${#fields.hasErrors('firstName')}" th:errors="*{email}">Name Error</td> </tr> <tr th:if="${userBean.firstName}"> <input type="hidden" th:field="*{firstName}" /> </tr> <tr th:unless="${userBean.lastName}"> <td>Email:</td> <td><input type="text" th:field="*{lastName}" /></td> <td th:if="${#fields.hasErrors('lastName')}" th:errors="*{lastName}">Name Error</td> </tr> <tr th:if="${userBean.lastName}"> <input type="hidden" th:field="*{lastName}" /> </tr> <tr th:unless="${userBean.country}"> <td>Country:</td> <td><select th:field="*{country}"> <option value="India" th:text="India"></option> <option value="UK" th:text="UK"></option> <option value="US" th:text="US"></option> <option value="Iraq" th:text="Iraq"></option> </select></td> <td th:if="${#fields.hasErrors('country')}" th:errors="*{country}">Country Error</td> </tr> <tr th:if="${userBean.country}"> <input type="hidden" th:field="*{country}" /> </tr> <input type="hidden" th:field="*{password}" /> <input type="hidden" th:field="*{passwordConfirm}" /> <tr> <td><button type="submit">Submit</button></td> </tr> </table> </form> </div> </body> </html> |
That’s all, we have successfully created a login application which integrates with the different social providers and also allows us to register new users.
The above code can be .
Congrats on a well documented, fully fledged social login application.
I got the app working with my app-keys for both facebook and Google.
Should you find the time, I’d be interested in seeing the same app, using Spring boot 2 and spring social 2.
The problem with the code is that, everytime you authorize with any social provider you are redirected to incomplete info page and u have to set for example title and country (it’s not saving to database).
I will look into it.
Great work. Thank you!