The post Part-2: Authorising user using Spring Social (Google, Facebook, LinkedIn) and Spring Security appeared first on Little Big Extra.
]]>
In the last part, I had demonstrated how we can use spring-social to authorize user using Facebook, Google and LinkedIn API’s. If you have not read the last part, I would request you to have a look and then come back to this part.
In this part, I am going to use spring security to allow only logged in users or authenticated users to navigate to secure pages, any user attempting to go to secure pages will be redirected to the Login page for authentication.
Once the user is authenticated, we will save his details to in-memory DB and then the user can log out and log in again.
Spring Security framework provides both authentication and authorization feature for applications. It also helps in preventing attacks like session fixation, clickjacking, cross-site request forgery, etc and good thing is that it can be customized easily to fit in different use cases.
In this tutorial, we will add spring security with spring social API to register users and then add the log on and log out functionality.
The first step to start will be adding maven dependencies for Spring-security and also for thymeleaf-extras-spring security for using spring-security tags for displaying logged in user and roles etc on pages.
Add the following dependencies to the POM.xml
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> </dependency> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity4</artifactId> </dependency>
When logging on to social networking platforms, we didn’t need any form as we fetch the user details through the API. However, to register a user we will need a simple form to capture their details. We will create a view called registration.html under src/main/resources/templates/registration.html.
This form will have server-side validation and minimal CSS/JS
Source code of Registration page is as below
<!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" style="width:80%"> <h1>Registration Page</h1> <br /> <form action="#" th:action="@{/registration}" th:object="${userBean}" method="post" > <div class="form-group" > <label for="email" class="control-label col-sm-2">Email*</label>:: <input type="text" th:field="*{email}" placeholder="Enter email"/> <div style="width:33%" th:if="${#fields.hasErrors('email')}" th:errors="*{email}" class="alert alert-danger">Email Error</div> </div> <div class="form-group"> <label for="firstName" class="control-label col-sm-2">First Name*</label>:: <input type="text" th:field="*{firstName}" /> <div style="width:33%" th:if="${#fields.hasErrors('firstName')}" th:errors="*{firstName}" class="alert alert-danger">FirstName Error</div> </div> <div class="form-group"> <label for="lastName" class="control-label col-sm-2">Last Name*</label>:: <input type="text" th:field="*{lastName}" /> <div style="width:33%" th:if="${#fields.hasErrors('lastName')}" th:errors="*{lastName}" class="alert alert-danger">LastName Error</div> </div> <div class="form-group"> <label for="password" class="control-label col-sm-2">Password*</label>:: <input type="text" th:field="*{password}" /> <div style="width:33%" th:if="${#fields.hasErrors('password')}" th:errors="*{password}" class="alert alert-danger">Password Error</div> </div> <div class="form-group"> <label for="passwordConfirm" class="control-label col-sm-2">Confirm Password*</label>:: <input type="text" th:field="*{passwordConfirm}" /> <div style="width:33%" th:if="${#fields.hasErrors('passwordConfirm')}" th:errors="*{passwordConfirm}" class="alert alert-danger">Password Error</div> </div> <div class="form-group"> <label for="title" class="control-label col-sm-2">Title</label>:: <select th:field="*{title}"> <option value="Mr" th:text="Mr"></option> <option value="Mrs" th:text="Mrs"></option> </select> <div th:if="${#fields.hasErrors('title')}" th:errors="*{title}">Title Error</div> </div> <div class="form-group"> <label for="country" class="control-label col-sm-2">Country</label>:: <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="Japan" th:text="Japan"></option> </select> <div th:if="${#fields.hasErrors('country')}" th:errors="*{country}" class="alert alert-danger">Country Error</div> </div> <input type="hidden" name="provider" value="registration" /> <div class="form-group"> <button type="submit" class="btn btn-primary">Register</button> </div> </form> </div> </body> </html>
To serve this Page, we will modify the existing Login controller
@GetMapping("/registration") public String showRegistration(UserBean userBean) { return "registration"; }
Now let’s modify the login page and add the registration link on the home page, also we will add username and password fields so once the user is registered they can also log in.
This HTML fragment will add a username/password field along with an error message, in case of the user not found.
Also, let’s add another code fragment which is to display the Logged In user, the role assigned and a Logout button
<div th:fragment="logout" class="logout" sec:authorize="isAuthenticated()"> Logged in user: <b><span sec:authentication="name"></span></b> | Roles: <b><span sec:authentication="authorities"></span></b> <form action="#" th:action="@{/logout}" method="post"> <button type="submit" class="btn btn-danger btn-sm" <span class="glyphicon glyphicon-log-out"></span> Log out</button> </form> </div>
We will also need to csrf token to our existing forms for Google, Facebook and LinkedIn
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
This is how the Complete login.html looks like.
Source code of login.html
<!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"> <div th:fragment="logout" class="logout" sec:authorize="isAuthenticated()"> Logged in user: <b><span sec:authentication="name"></span></b> | Roles: <b><span sec:authentication="authorities"></span></b> <form action="#" th:action="@{/logout}" method="post"> <button type="submit" class="btn btn-danger btn-sm"> <span class="glyphicon glyphicon-log-out"></span> Log out</button> </form> </div> <br/> <h1>Login Using</h1> <form th:action="@{/login}" method="post" style="display: inline"> <label for="username">Email </label> : <input type="text" id="username" name="username" autofocus="autofocus" placeholder="Enter email"/> <br /> <label for="password">Password</label>: <input type="password" id="password" name="password" /> <br /> <p th:if="${loginError}" class="alert alert-danger">Wrong email or password combination</p> <button type="submit" class="btn btn-primary"> <span class="fa fa-user"></span>Login </button> </form> <form action="/connect/google" method="POST" style="display: inline"> <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" /> <input type="hidden" name="scope" value="profile email" /> <button type="submit" class="btn btn-danger"> Google <span class="fa fa-google-plus"></span> </button> </form> <form action="/connect/facebook" method="POST" style="display: inline"> <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" /> <input type="hidden" name="scope" value="public_profile,email" /> <button type="submit" class="btn btn-primary"> Facebook <span class="fa fa-facebook"></span> </button> </form> <form action="/connect/linkedin" method="POST" style="display: inline"> <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" /> <input type="hidden" name="scope" value="r_basicprofile,r_emailaddress" /> <button type="submit" class="btn btn-primary"> LinkedIn <span class="fa fa-linkedin"></span> </button> </form> <br /> <h3> <p class="bg-important"> <a href="/registration" th:href="@{/registration}">Create Account</a> </p> </h3> </div> </body> </html>
Once the user is authenticated, we will need to save their details in a database. So we will use the annotations like @Entity and @Table to create an in-memory database(HSQLDB) and tables if they don’t exist. The DB details can be easily configured in application.properties,also the DB type can also be changed easily.
We will use the hibernate validator annotations @NotNull,@Size to make sure that input fields are validated on the server side before they can be, you can add client-side javascript if that suits.
package com.login.model; import java.io.Serializable; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; import javax.persistence.Transient; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; import org.hibernate.validator.constraints.Email; @Entity(name = "user") @Table(name = "user") public class UserBean implements Serializable{ private static final long serialVersionUID = 1L; @NotNull(message = "Email cannot be empty") @Email(message = "Email Format is not valid") @Size(min = 3, max = 30, message = "Email can not be empty") @Id private String email; @NotNull(message = "First Name cannot be empty") @Size(min = 3, max = 30, message = "First Name cannot be less than 3 characters") private String firstName; @NotNull(message = "Last Name cannot be empty") @Size(min = 3, max = 30, message = "Last Name cannot be less than 3 characters") private String lastName; private String title; private String country; private String password; @Transient private String passwordConfirm; private String provider; private String image; ......getter/setter methods here ... }
We will be using to retrieve user details and save them.
Now we will create a new interface called UserRepository in package com.login.repository this interface will extend the JPARespository<T, ID> where T is UserBean in our Case and ID is the email(primary key).
We will define the abstract method findByEmail by passing email which has been defined as a primary key in UserBean class. To get the UserBean for a particular method all we need to do is inject the UserRepository and call findByemail method()
package com.login.repository; import org.springframework.data.jpa.repository.JpaRepository; import com.login.model.UserBean; public interface UserRepository extends JpaRepository<UserBean, String> { UserBean findByEmail(String email); }
The key advantage of using Spring Data is that it makes virtually our code DAO implementation-free, only above interface will be able to save and retrieve user details.
This is the crux of this tutorial, we will use the spring security to define which URL’s can be accessed only by insecurely and which one can only be accessed by login user.
We will add a class called SecurityConfig to allow users to
package com.login.security; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; //@formatter:off @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().antMatchers("/css/**", "/connect/**").permitAll() .antMatchers("/secure/**") .authenticated() .and() .formLogin() .loginPage("/login") .defaultSuccessUrl("/secure/user") .failureUrl("/login-error") .permitAll() .and() .logout() .permitAll(); } @Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder()); } } // @formatter:on
As you might have noticed that we have added a bean for BCryptPasswordEncoder, this bean will be used for encrypting the password as hash, which is one of the safest technique to store passwords and decrypting hash is very tough ( Maybe not as tough as mining a BitCoin )
Also, there is another method configureglobal(AuthenticationManagerBuilder auth) which basically defines that we have defined our own custom implementation of UserDetailsService, we will talk about it later.
Since now we want our user.html to be only presented for logged in users we will move user.html from src/main/resources/templates to src/main/resources/templates/secure/user.html
Also, we will add the thymleaf authentication tags to display logged in Username and role.
This is how the complete user.html looks like, make sure it is in src/main/resources/templates/secure/user.html
<!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" /> <meta name="ctx" th:content="${#httpServletRequest.getContextPath()}" /> <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"> <h1>Secure Page</h1> <div th:fragment="logout" class="logout" sec:authorize="isAuthenticated()"> Logged in user: <b><span sec:authentication="name"></span></b> | Roles: <b><span sec:authentication="authorities"></span></b> <form action="#" th:action="@{/logout}" method="post"> <button type="submit" class="btn btn-danger btn-sm"> <span class="glyphicon glyphicon-log-out"></span> Log out</button> </form> </div> <br/> <form th:object="${loggedInUser}" method="post"> <div class="row"> <label for="email">Email :</label> <span th:text="*{email}" /> </div> <div class="row"> <label for="firstName">Name:</label> <span th:text="*{firstName}" /> <span th:text="*{lastName}" /> </div> <div class="row"> <label for="image">Image:</label> <img th:attr="src=@{*{image}}" style="width: 150px; height: 150px;"/> </div> </form> <br /> <a href="/login" th:href="@{/login}" class="btn btn-info btn-lg"> <span class="glyphicon glyphicon-chevron-left"></span> Login using other social Providers </a> </div> </body> </html>
This is the crucial step as would define here what to do when the user puts in his username/password combination.
If we want user authentication by a DAO class, we need to implement the UserDetailsService interface. This interface has loadUserByUsername() method which is used to validate the user, of course, we need to provide the implementation.
Remeber this method in Security config
@Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder()); }
We will write the implementation of method loadUserByUsername.In this method, we will do couple of things
package com.login.security.service.impl; import java.util.HashSet; import java.util.Set; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.login.model.UserBean; import com.login.repository.UserRepository; @Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserRepository userRepository; @Override @Transactional(readOnly = true) public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { UserBean user = userRepository.findByEmail(email); if (user == null) { throw new UsernameNotFoundException("No user found with email: " + email); } Set<GrantedAuthority> grantedAuthorities = new HashSet<>(); grantedAuthorities.add(new SimpleGrantedAuthority("LOGGED_USER")); return new User(user.getEmail(), user.getPassword(), grantedAuthorities); } }
Once the user is logged in by social providers or by registering a user we need to update security context by setting the authentication
As per the spring API – “Authentication represents the token for an authentication request or for an authenticated principal once the request has been processed by the AuthenticationManager.authenticate(Authentication) method.
Once the request has been authenticated, the Authentication will usually be stored in a thread-local SecurityContext managed by the SecurityContextHolder by the authentication mechanism which is being used. An explicit authentication can be achieved, without using one of Spring Security’s authentication mechanisms, by creating an Authentication instance and using the code:
SecurityContextHolder.getContext().setAuthentication(authentication)
Which is exactly we are going to do in our method.
package com.login.autologin; import java.util.HashSet; import java.util.Set; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import com.login.model.UserBean; @Service public class Autologin { public void setSecuritycontext(UserBean userForm) { Set<GrantedAuthority> grantedAuthorities = new HashSet<>(); grantedAuthorities.add(new SimpleGrantedAuthority(userForm.getProvider().toUpperCase())); Authentication authentication = new UsernamePasswordAuthenticationToken(userForm.getEmail(), userForm.getPassword(), grantedAuthorities); SecurityContextHolder.getContext().setAuthentication(authentication); } }
Since now we have most of the things in place, we need to add the controller so when the registration form is submitted,
Also, since in our Security config, we defined a “/login-error” path, we will handle that too in the same controller.
@Autowired private UserRepository userRepository; @Autowired private BCryptPasswordEncoder bCryptPasswordEncoder; @Autowired private Autologin autologin; @PostMapping("/registration") public String registerUser(HttpServletResponse httpServletResponse, Model model, @Valid UserBean userBean, BindingResult bindingResult) { if (bindingResult.hasErrors()) { return "registration"; } userBean.setProvider("REGISTRATION"); // Save the details in DB if (StringUtils.isNotEmpty(userBean.getPassword())) { userBean.setPassword(bCryptPasswordEncoder.encode(userBean.getPassword())); } userRepository.save(userBean); autologin.setSecuritycontext(userBean); model.addAttribute("loggedInUser", userBean); return "secure/user"; } /** If we can't find a user/email combination */ @RequestMapping("/login-error") public String loginError(Model model) { model.addAttribute("loginError", true); return "login"; }
In previous post we had created the FacebookProvider, GoogleProvider and LinkedInProvider now we need to make some changes in them so they
In our class BaseProvider.java, we will add saveUserDetails and autoLoginUser method
@Autowired private BCryptPasswordEncoder bCryptPasswordEncoder; @Autowired private UserRepository userRepository; @Autowired protected Autologin autologin; protected void saveUserDetails(UserBean userBean) { if (StringUtils.isNotEmpty(userBean.getPassword())) { userBean.setPassword(bCryptPasswordEncoder.encode(userBean.getPassword())); } userRepository.save(userBean); } public void autoLoginUser(UserBean userBean) { autologin.setSecuritycontext(userBean); }
In our provider classes(FacebookProvider, GoogleProvider and LinkedInProvider) we just need to add the code to
baseProvider.saveUserDetails(userForm);
baseProvider.autoLoginUser(userForm);
return "secure/user"
You will need to do these changes in all 3 classes(GoogleProvider, FaceBookProvider and LinkedInProvider)
public String getLinkedInUserData(Model model, UserBean userForm) { ConnectionRepository connectionRepository = baseProvider.getConnectionRepository(); if (connectionRepository.findPrimaryConnection(LinkedIn.class) == null) { return REDIRECT_LOGIN; } populateUserDetailsFromLinkedIn(userForm); //Save the details in DB baseProvider.saveUserDetails(userForm); //Login the User baseProvider.autoLoginUser(userForm); model.addAttribute("loggedInUser",userForm); return "secure/user"; }
Remeber, we have added in our
defaultSuccessUrl("/secure/user")SecurityConfig, well this is the page where we want the user to redirect, once they are authenticated.
Also on user/secure.html, we have defined
th:object="${loggedInUser}"this attribute also needs to be initialized once the user is logged in by security config. So the question is how do we initialize the
"${loggedInUser}"model attribute. If we don’t initialise it we will get an error.
@ModelAttribute("loggedInUser"). If this annotation has been used all the RequestMapping method in the controller will be called only after the annotated method.
We can define a method where we will get the Authentication object and then use that object to find the user details.
@ModelAttribute("loggedInUser") public void secure(Model model) { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); UserBean user = userRepository.findByEmail(auth.getName()); model.addAttribute("loggedInUser", user); }
Also, we need to define the method for mapping
@GetMapping("/secure/user")as shown below. Since we need to initialize model attribute
${loggedInUser}only for
@GetMapping("/secure/user")we need to define it in a separate controller class.
package com.login.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import com.login.model.UserBean; import com.login.repository.UserRepository; @Controller public class LoggedInUserController { @Autowired private UserRepository userRepository; @ModelAttribute("loggedInUser") public void secure(Model model) { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); UserBean user = userRepository.findByEmail(auth.getName()); model.addAttribute("loggedInUser", user); } @GetMapping("/secure/user") public String securePage() { return "secure/user"; } }
Since we are using HSQLDB in this example and spring boot will configure most of the things for us, we don’t need to do more than defining a couple of properties for initializing the database. Add these properties in application.properties
spring.jpa.hibernate.ddl-auto=create spring.jpa.show-sql=true
Spring Social and Spring Security are very flexible and easily customizable for different use cases. They both can be used together to provide a seamless and smooth log in experience for users. The above code is just a step in explaining how both the offerings (Spring Social and Spring Security) can be used in unison.
For readers benefit, above code can be cloned from
This is how the project structure looks like
This is how the screen looks like after user is logged in, the logged in user and the roles are being displayed on top of screen.
The post Part-2: Authorising user using Spring Social (Google, Facebook, LinkedIn) and Spring Security appeared first on Little Big Extra.
]]>The post Part -1 : Authorising user using Spring Social (Google, FaceBook and LinkedIn) and Spring Security appeared first on Little Big Extra.
]]>Social Logins are becoming increasingly popular across web applications, they not only offer good user experience are safe and also saves the user from password fatigue. Imagine losing a customer because they can not log on to the application as they cannot remember a password are not bothered to reset passwords etc. A social login is a kind of single sign-on where you use login information of a social network like Facebook, Twitter, Google+ to log on to a third website, instead of creating a new log-in account specially for that website.
Earlier I have written few blogs about Spring-Social features where I had written about few problems like no support for Google, only one user allowed by spring-social etc and also few exceptions which occur and then how to fix them.I thought of collating all the problems and writing a step by step tutorial explaining in detail, on how to register a user using spring social.
The main aim of this tutorial will be to integrate spring-security and spring-social together and create a web application where users can be registered either by
In this first part of the tutorial, our aim will be to get data from spring-social and then display the details on the page, in the second part we will integrate the spring-security where we will ask the user to enter his details and then store on DB.
Assuming that you know how to create a simple spring boot project, let us move on to add maven-dependencies required to run the project. If not, then you can create a spring-starter project and add thymleaf and web support.
The first step would be to add spring-social maven repositories, following dependencies are required
<dependency> <groupId>org.springframework.social</groupId> <artifactId>spring-social-core</artifactId> <version>2.0.0.M2</version> </dependency> <dependency> <groupId>org.springframework.social</groupId> <artifactId>spring-social-config</artifactId> <version>2.0.0.M2</version> </dependency> <dependency> <groupId>org.springframework.social</groupId> <artifactId>spring-social-facebook</artifactId> <version>3.0.0.M1</version> </dependency> <dependency> <groupId>org.springframework.social</groupId> <artifactId>spring-social-linkedin</artifactId> </dependency> <dependency> <groupId>org.springframework.social</groupId> <artifactId>spring-social-google</artifactId> <version>1.0.0.RELEASE</version> </dependency>
In case you get the repository not available error, you might have to add the following repository to your pom.
<repositories> <repository> <id>alfresco-public</id> <url>https://artifacts.alfresco.com/nexus/content/groups/public</url> </repository> </repositories>
To make our web pages look nice we will add some Bootstrap CSS, Font-awesome and Jquery to them and also we will use thymleaf for HTML. Read Here if you want to know more in detail.
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>jquery</artifactId> <version>2.1.1</version> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>bootstrap</artifactId> <version>3.2.0</version> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>webjars-locator</artifactId> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>font-awesome</artifactId> <version>4.7.0</version> </dependency>
To avoid any mistakes, here is the complete POM.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.login</groupId> <artifactId>SpringLogin</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>SpringLogin</name> <description>Spring Login - Example POC</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.8.RELEASE</version> <relativePath /> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.social</groupId> <artifactId>spring-social-core</artifactId> <version>2.0.0.M2</version> </dependency> <dependency> <groupId>org.springframework.social</groupId> <artifactId>spring-social-config</artifactId> <version>2.0.0.M2</version> </dependency> <dependency> <groupId>org.springframework.social</groupId> <artifactId>spring-social-facebook</artifactId> <version>3.0.0.M1</version> </dependency> <dependency> <groupId>org.springframework.social</groupId> <artifactId>spring-social-linkedin</artifactId> </dependency> <dependency> <groupId>org.springframework.social</groupId> <artifactId>spring-social-google</artifactId> <version>1.0.0.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> --> <dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.1</version> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>jquery</artifactId> <version>2.1.1</version> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>bootstrap</artifactId> <version>3.2.0</version> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>webjars-locator</artifactId> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>font-awesome</artifactId> <version>4.7.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.hsqldb</groupId> <artifactId>hsqldb</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> </dependency> </dependencies> <repositories> <repository> <id>alfresco-public</id> <url>https://artifacts.alfresco.com/nexus/content/groups/public</url> </repository> </repositories> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
Once the dependencies have been resolved be will create a View and Controller
To start we will create a view which will have 3 buttons each for Google, Facebook and Login. When the user will click on any of them he will be asked for authorization for the selected provider, upon verification his details will be rendered on the page.
Create a file under src/main/resources/templates/login.html with the following content. Please note that this view has 3 different forms each for different provider
<!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"> <h1>Login Using</h1> <form action="/connect/google" method="POST" style="display: inline"> <input type="hidden" name="scope" value="profile email" /> <button type="submit" class="btn btn-danger"> Google <span class="fa fa-google-plus"></span> </button> </form> <form action="/connect/facebook" method="POST" style="display: inline"> <input type="hidden" name="scope" value="public_profile,email" /> <button type="submit" class="btn btn-primary"> Facebook <span class="fa fa-facebook"></span> </button> </form> <form action="/connect/linkedin" method="POST" style="display: inline"> <input type="hidden" name="scope" value="r_basicprofile,r_emailaddress" /> <button type="submit" class="btn btn-primary"> LinkedIn <span class="fa fa-linkedin"></span> </button> </form> </div> </body> </html>
Let us create a controller class called LoginController in package com.login.controller with a simple method which will map the above page to URL / or /login.
@Controller public class LoginController { @RequestMapping(value = { "/","/login" }) public String login() { return "login"; } }
Now run the above application and at this point, a page like this should appear and of course, it won’t do anything yet.
Once we verify the user via our social provider, we will show the user details on another page. We can store these details in a POJO called UserBean.
Let us define a class called UserBean.java as shown below in package com.login.model
package com.login.model; import java.beans.Transient; import java.io.Serializable; public class UserBean implements Serializable{ private static final long serialVersionUID = 1L; private String firstName; private String lastName; private String email; private String title; private String country; private String password; private String passwordConfirm; private String provider; private String image; public String getEmail() { return email; } public String getImage() { return image; } public void setImage(String image) { this.image = image; } public void setEmail(String email) { this.email = email; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getPasswordConfirm() { return passwordConfirm; } public void setPasswordConfirm(String passwordConfirm) { this.passwordConfirm = passwordConfirm; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getCountry() { return country; } public void setCountry(String country) { this.country = country; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getProvider() { return provider; } public void setProvider(String provider) { this.provider = provider; } }
For Facebook, Google and LinkedIn we will create a Provider class which will be used to do authentication and save user details to the UserBean, which can be later displayed on the page. To start off with we will create a BaseProvider which will have a constructor where all the providers will be initialized.
This BaseProvider is created by injecting the Facebook, Google, LinkedIn and ConnectionRepository repository. These objects are a reference to Spring Social’s Facebook, Google, LinkedIn and ConnectionRepository API binding.
package com.login.social.providers; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.ScopedProxyMode; import org.springframework.social.connect.ConnectionRepository; import org.springframework.social.facebook.api.Facebook; import org.springframework.social.google.api.Google; import org.springframework.social.linkedin.api.LinkedIn; @Configuration @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS) public class BaseProvider { private Facebook facebook; private Google google; private LinkedIn linkedIn; private ConnectionRepository connectionRepository; public BaseProvider(Facebook facebook,Google google, LinkedIn linkedIn, ConnectionRepository connectionRepository) { this.facebook = facebook; this.connectionRepository = connectionRepository; this.google=google; this.linkedIn= linkedIn; } public Facebook getFacebook() { return facebook; } public void setFacebook(Facebook facebook) { this.facebook = facebook; } public ConnectionRepository getConnectionRepository() { return connectionRepository; } public void setConnectionRepository(ConnectionRepository connectionRepository) { this.connectionRepository = connectionRepository; } public Google getGoogle() { return google; } public void setGoogle(Google google) { this.google = google; } public LinkedIn getLinkedIn() { return linkedIn; } public void setLinkedIn(LinkedIn linkedIn) { this.linkedIn = linkedIn; } }
Now let us create provider class for each provider
We will use the below Facebook provider to authorize the user and then access Facebook Data. We will save the fetched user data into our UserBean.
package com.login.social.providers; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.social.connect.ConnectionRepository; import org.springframework.social.facebook.api.Facebook; import org.springframework.social.facebook.api.User; import org.springframework.stereotype.Service; import org.springframework.ui.Model; import com.login.model.UserBean; @Service public class FacebookProvider { private static final String FACEBOOK = "facebook"; private static final String REDIRECT_LOGIN = "redirect:/login"; @Autowired BaseProvider baseProvider ; public String getFacebookUserData(Model model, UserBean userForm) { ConnectionRepository connectionRepository = baseProvider.getConnectionRepository(); if (connectionRepository.findPrimaryConnection(Facebook.class) == null) { return REDIRECT_LOGIN; } populateUserDetailsFromFacebook(userForm); model.addAttribute("loggedInUser",userForm); return "user"; } protected void populateUserDetailsFromFacebook(UserBean userForm) { Facebook facebook = baseProvider.getFacebook(); User user = facebook.userOperations().getUserProfile(); userForm.setEmail(user.getEmail()); userForm.setFirstName(user.getFirstName()); userForm.setLastName(user.getLastName()); userForm.setImage(user.getCover().getSource()); userForm.setProvider(FACEBOOK); } }
We will use the below google provider to authorize the user and then access Google Data. We will save the fetched user data into our UserBean.
package com.login.social.providers; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.social.connect.ConnectionRepository; import org.springframework.social.google.api.Google; import org.springframework.social.google.api.plus.Person; import org.springframework.stereotype.Service; import org.springframework.ui.Model; import com.login.model.UserBean; @Service public class GoogleProvider { private static final String REDIRECT_CONNECT_GOOGLE = "redirect:/login"; private static final String GOOGLE = "google"; @Autowired BaseProvider socialLoginBean ; public String getGoogleUserData(Model model, UserBean userForm) { ConnectionRepository connectionRepository = socialLoginBean.getConnectionRepository(); if (connectionRepository.findPrimaryConnection(Google.class) == null) { return REDIRECT_CONNECT_GOOGLE; } populateUserDetailsFromGoogle(userForm); model.addAttribute("loggedInUser",userForm); return "user"; } protected void populateUserDetailsFromGoogle(UserBean userform) { Google google = socialLoginBean.getGoogle(); Person googleUser = google.plusOperations().getGoogleProfile(); userform.setEmail(googleUser.getAccountEmail()); userform.setFirstName(googleUser.getGivenName()); userform.setLastName(googleUser.getFamilyName()); userform.setImage(googleUser.getImageUrl()); userform.setProvider(GOOGLE); } }
We will use the below LinkedIn provider to authorize the user and then accessLinkedIn Data. We will save the fetched user data into our UserBean.
package com.login.social.providers; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.social.connect.ConnectionRepository; import org.springframework.social.linkedin.api.LinkedIn; import org.springframework.social.linkedin.api.LinkedInProfileFull; import org.springframework.stereotype.Service; import org.springframework.ui.Model; import com.login.model.UserBean; @Service public class LinkedInProvider { private static final String LINKED_IN = "linkedIn"; private static final String REDIRECT_LOGIN = "redirect:/login"; @Autowired BaseProvider socialLoginBean ; public String getLinkedInUserData(Model model, UserBean userForm) { ConnectionRepository connectionRepository = socialLoginBean.getConnectionRepository(); if (connectionRepository.findPrimaryConnection(LinkedIn.class) == null) { return REDIRECT_LOGIN; } populateUserDetailsFromLinkedIn(userForm); model.addAttribute("loggedInUser",userForm); return "user"; } private void populateUserDetailsFromLinkedIn(UserBean userForm) { LinkedIn linkedIn = socialLoginBean.getLinkedIn(); LinkedInProfileFull linkedInUser = linkedIn.profileOperations().getUserProfileFull(); userForm.setEmail(linkedInUser.getEmailAddress()); userForm.setFirstName(linkedInUser.getFirstName()); userForm.setLastName(linkedInUser.getLastName()); userForm.setImage(linkedInUser.getProfilePictureUrl()); userForm.setProvider(LINKED_IN); } }
If you would have noticed by now, spring social doesn’t support Google so to enable autoconfiguration of spring-social-google we need to
Spring social do have a spring-social-google project but it doesn’t have autoconfiguration for Google. So google authorization doesn’t work with spring boot autoconfigure straight away.
To enable autoconfigure of spring-social-google we need to
FacebookAutoConfiguration class you should be able to find it either in org.springframework.boot.autoconfigure.social package in spring-autoconfigure.jar does the autoconfiguration for Facebook, the cheat sheet or the trick would be to copy this File and replace Facebook with Google to enable GoogleAutoConfiguration
Alternatively, copy the below class and put in some package, make sure it is available in classpath(src/main/java or inside another source folder)
package com.login.config.google; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.social.SocialAutoConfigurerAdapter; import org.springframework.boot.autoconfigure.social.SocialWebAutoConfiguration; import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.ScopedProxyMode; import org.springframework.social.config.annotation.EnableSocial; import org.springframework.social.config.annotation.SocialConfigurerAdapter; import org.springframework.social.connect.Connection; import org.springframework.social.connect.ConnectionFactory; import org.springframework.social.connect.ConnectionRepository; import org.springframework.social.connect.web.GenericConnectionStatusView; import org.springframework.social.google.api.Google; import org.springframework.social.google.connect.GoogleConnectionFactory; @Configuration @ConditionalOnClass({ SocialConfigurerAdapter.class, GoogleConnectionFactory.class }) @ConditionalOnProperty(prefix = "spring.social.google", name = "app-id") @AutoConfigureBefore(SocialWebAutoConfiguration.class) @AutoConfigureAfter(WebMvcAutoConfiguration.class) public class GoogleAutoConfiguration { @Configuration @EnableSocial @EnableConfigurationProperties(GoogleProperties.class) @ConditionalOnWebApplication protected static class GoogleConfigurerAdapter extends SocialAutoConfigurerAdapter { private final GoogleProperties properties; protected GoogleConfigurerAdapter(GoogleProperties properties) { this.properties = properties; } @Bean @ConditionalOnMissingBean(Google.class) @Scope(value = "request", proxyMode = ScopedProxyMode.INTERFACES) public Google google(ConnectionRepository repository) { Connection<Google> connection = repository.findPrimaryConnection(Google.class); return connection != null ? connection.getApi() : null; } @Bean(name = { "connect/googleConnect", "connect/googleConnected" }) @ConditionalOnProperty(prefix = "spring.social", name = "auto-connection-views") public GenericConnectionStatusView googleConnectView() { return new GenericConnectionStatusView("google", "Google"); } @Override protected ConnectionFactory<?> createConnectionFactory() { return new GoogleConnectionFactory(this.properties.getAppId(), this.properties.getAppSecret()); } } }
In the same package add the below class, this is needed so when we add secret keys in properties file for google. We wont see any error.
package com.login.config.google; import org.springframework.boot.autoconfigure.social.SocialProperties; import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties(prefix = "spring.social.google") public class GoogleProperties extends SocialProperties{ }
Spring Social is quick and easy to start but it needs some configuring before it can be used for production. The default flow for
The Views are predefined so the user flow is login.html -> connect/facebookConnected.html by default, irrespective of what our submit URL is, the facebook will override the flow and redirect you to connect/facebookConnected.html.
Read Here: How to change the default spring social redirect page flow
We will add a ChangeDefaultFlowController which will override the connectedView method and redirect the flow to “/facebook”, “/google” or “/LinkedIn”
package com.login.controller; import org.springframework.social.connect.ConnectionFactoryLocator; import org.springframework.social.connect.ConnectionRepository; import org.springframework.social.connect.web.ConnectController; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller @RequestMapping("/connect") public class ChangeDefaultFlowController extends ConnectController { public ChangeDefaultFlowController(ConnectionFactoryLocator connectionFactoryLocator, ConnectionRepository connectionRepository) { super(connectionFactoryLocator, connectionRepository); } @Override protected String connectedView(String providerId) { return "redirect:/"+providerId; } }
If you test the Spring Facebook example, accessing FB data you will realise that it only supports one user. So when the first users log in to Facebook, only his details will be shared across all new sessions/users and they won’t be asked for any kind of authentication. One of the forum says that this example is supposed to demonstrate what can be done and is not intended for production use.
Read Here: Spring Social Facebook Authentication Example for multiple users
To fix this problem you need to override a method which always returns a string called “anonymous”.The solution is to override the “anonymous�? as the UserId for each new user/session. So for each session, we can simply return a SessionID, however, it may not be unique enough to identify users, especially if it’sh being cached or stored somewhere in a connection database.
Using a Universally Unique Identifier(UUID) would be a safer bet.So we will store a new UUID in session so it persists between different requests but is alive only till the session is valid. See the below method which does the trick.
package com.login.identifier; import java.util.UUID; import org.springframework.context.annotation.Configuration; import org.springframework.social.UserIdSource; import org.springframework.social.config.annotation.SocialConfigurerAdapter; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; @Configuration public class SessionIdentifier extends SocialConfigurerAdapter { @Override public UserIdSource getUserIdSource() { return new SessionIdUserIdSource(); } private static final class SessionIdUserIdSource implements UserIdSource { @Override public String getUserId() { RequestAttributes request = RequestContextHolder.currentRequestAttributes(); String uuid = (String) request.getAttribute("_socialUserUUID", RequestAttributes.SCOPE_SESSION); if (uuid == null) { uuid = UUID.randomUUID().toString(); request.setAttribute("_socialUserUUID", uuid, RequestAttributes.SCOPE_SESSION); } return uuid; } } }
Now since we have added all the fixes, its time to update the controller so our view can submit the forms to Controller
package com.login.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import com.login.model.UserBean; import com.login.social.providers.FacebookProvider; import com.login.social.providers.GoogleProvider; import com.login.social.providers.LinkedInProvider; @Controller public class LoginController { @Autowired FacebookProvider facebookProvider; @Autowired GoogleProvider googleProvider; @Autowired LinkedInProvider linkedInProvider; @RequestMapping(value = "/facebook", method = RequestMethod.GET) public String loginToFacebook(Model model) { return facebookProvider.getFacebookUserData(model, new UserBean()); } @RequestMapping(value = "/google", method = RequestMethod.GET) public String loginToGoogle(Model model) { return googleProvider.getGoogleUserData(model, new UserBean()); } @RequestMapping(value = "/linkedin", method = RequestMethod.GET) public String helloFacebook(Model model) { return linkedInProvider.getLinkedInUserData(model, new UserBean()); } @RequestMapping(value = { "/","/login" }) public String login() { return "login"; } }
Assuming that you know how to register the API and public/private keys on Google/FB and LinkedIn for this example purpose you can use the below properties file.
Also, notice that server starts on port 3000(don’t change that, since the URL registered with these keys is localhost:3000
spring.social.facebook.appId=384261248599251 spring.social.facebook.appSecret=fd7fa1c5f5a267f463263a0ce7ff2025 spring.social.linkedin.app-id=771mrzk94hye1w spring.social.linkedin.app-secret=iIJFgBf9lCb18zYe spring.social.google.appId=12894100090-tqso3lih5o42isneort886la2pesafmp.apps.googleusercontent.com spring.social.google.appSecret=9xfU16efvxQ-BTMsXT9wOLpw server.port:3000
This is how the project structure will look like
To show the authenticated user on the page, we will display the UserBean using thymleaf. Add the below html page under src/main/resources/static/ as user.html
<!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" /> <meta name="ctx" th:content="${#httpServletRequest.getContextPath()}" /> <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"> <br /><br /><br /> <form th:object="${loggedInUser}" method="post"> <div class="row"> <label for="username">Username:</label> <span th:text="${loggedInUser.email}" /> </div> <div class="row"> <label for="firstName">Name:</label> <span th:text="*{firstName}" /> <span th:text="*{lastName}" /> </div> <div class="row"> <label for="image">Name:</label> <img th:attr="src=@{*{image}}" style="width: 150px; height: 150px;"/> </div> </form> <br /> <a href="/login" th:href="@{/login}" class="btn btn-info btn-lg"> <span class="glyphicon glyphicon-chevron-left"></span> Login using other social Providers </a> </div> </body> </html>
Now run the Project and access the application on http://localhost:3000/login.
You should be greeted by an authentication popup depending upon your provider selected and then you should be redirected to the page where your username, name, profile image will be displayed as below.
Spring Social API helps us in implementing an authentication mechanism with social providers. It is easy to use and we have seen how to configure it to tailor it to our needs.
In the next part we will use spring-security to register users and then only allow logged in users to navigate to secure pages. The link has been provided below for second part.
The post Part -1 : Authorising user using Spring Social (Google, FaceBook and LinkedIn) and Spring Security appeared first on Little Big Extra.
]]>The post Part 3 :Authorising user using Spring Social (Google, Facebook, LinkedIn) and Spring Security appeared first on Little Big Extra.
]]>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.
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.
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
It won’t work because when we logged in through social provider it basically overwrote the password with the blank.
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.
We will do a couple of things in this tutorial the first being
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.
@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);
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.
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()); }
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.
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.
//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.
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"; }
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.
<!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 .
The post Part 3 :Authorising user using Spring Social (Google, Facebook, LinkedIn) and Spring Security appeared first on Little Big Extra.
]]>The post How to change the default spring social redirect page flow appeared first on Little Big Extra.
]]>Spring Boot when plugged with Spring social does provide a quick and easy way to log on to social providers and access the data quickly.
However, it does have lot of shortcomings too like
While it is easy to change the initial view there is no clear documentation available which defines how to change the views.
This bit is easy we just need to copy everything inside the form tag and place it in your login/home page.
All we are doing here is creating a form which posts to
"/connect/facebook"
When we submit the form the code goes to ConnectController class and does the OAuth dance and routes back to
"/connect/facebookConnected.html"
AS mentioned in the previous step the Connect controller class does the OAuth auth behind the scenes and redirects back to
"/connect/facebookConnected.html"
org.springframework.social.connect.web.ConnectControllerClass is an UI controller for managing the account-to-service-provider connection flow.
In this class, all the GET and Post methods have defined to handle the authentication flow
Among all the methods you should see a connectedView method which returns the
"/connect/facebookConnected.html"
protected String connectedView(String providerId) { return getViewPath() + providerId + "Connected"; }
All we need to do is to override this method
Create a class and call it CustomController make sure that it extends
org.springframework.social.connect.web.ConnectController
Add the
@Controllerannotation at class level
public CustomController(ConnectionFactoryLocator connectionFactoryLocator, ConnectionRepository connectionRepository) { super(connectionFactoryLocator, connectionRepository); }
@Override protected String connectedView(String providerId) { return "redirect:/facebook"; }
The above code will redirect to another REST controller where say data can be saved to DB and things like authentication can be done, something like below
@RequestMapping(value = "/facebook", method = RequestMethod.GET) public String loginToFacebook(Model model) { getFacebookUserData(model, new UserForm()); return "facebookPage" }
@Override protected String connectedView(String providerId) { return "home"; }
Complete Custom controller class looks like this
package com.nevado.controller; import org.springframework.social.connect.ConnectionFactoryLocator; import org.springframework.social.connect.ConnectionRepository; import org.springframework.social.connect.web.ConnectController; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller @RequestMapping("/connect") public class CustomController extends ConnectController { public CustomController(ConnectionFactoryLocator connectionFactoryLocator, ConnectionRepository connectionRepository) { super(connectionFactoryLocator, connectionRepository); } @Override protected String connectedView(String providerId) { return "redirect:/facebook"; } }
Follow the video for reference
The post How to change the default spring social redirect page flow appeared first on Little Big Extra.
]]>The post How to access LinkedIn data using spring-social appeared first on Little Big Extra.
]]>Spring Social provides the capability which enables our application to interact with LinkedIn Rest API and get user data. Considering how popular social logins have become recently, it is a nice feature to have on your website.
I would suggest you to first clone the as it does have required page structure and dependencies
Add the spring-social-google dependency
org.springframework.social spring-social-linkedin 1.0.0.RELEASE
Now update the application.properties with your secret and client key
spring.social.linkedin.app-id=771mrzk94hye1w spring.social.linkedin.app-secret=iIJFgBf9lCb18zYe
Under src/main/resources/templates/connect you should have facebookConnect.html and facebookConnected.html, if you have cloned/downloaded the spring tutorial properly.
Again replace facebook with linkedin in HTML and save them as linkedinConnected.html and linkedinConnect.html or copy them as shown below. Make sure the scope is properly defined in your request.
<input type="hidden" name="scope" value="r_basicprofile,r_emailaddress" />
linkedinConnect.html
<html> <head> <title>Hello LinkedIn</title> </head> <body> <h3>Connect to Linkedin</h3> <form action="/connect/linkedin" method="POST"> <input type="hidden" name="scope" value="r_basicprofile,r_emailaddress" /> <div class="formInfo"> <p>You aren't connected to LinkedIn yet. Click the button to connect this application with your account.</p> </div> <p><button type="submit">Connect to LinkedIn</button></p> </form> </body> </html>
linkedinConnected.html
<html> <head> <title>Hello Facebook</title> </head> <body> <h3>Connected to LinkedIn</h3> <p> You are now connected to your LinkedIn account. Click <a href="/linkedin">here</a> to see some entries from your LinkedIn feed. </p> </body> </html>
We need to add a rest controller so it can handle LinkedIn authorization requests
@Controller @RequestMapping("/linkedin") public class LinkedInController { private LinkedIn linkedIn; private ConnectionRepository connectionRepository; public LinkedInController(LinkedIn linkedIn, ConnectionRepository connectionRepository) { this.linkedIn = linkedIn; this.connectionRepository = connectionRepository; } @GetMapping public String helloFacebook(Model model) { if (connectionRepository.findPrimaryConnection(LinkedIn.class) == null) { return "redirect:/connect/linkedin"; } ProfileOperations user = linkedIn.profileOperations(); System.out.println(user); model.addAttribute("linkedInProfile",linkedIn.profileOperations().getUserProfileFull()); return "linkedin"; } }
In src/main/resources add a file called “linkedin.html” at same level as “hello.html”
<html> <head> <title>Hello Facebook</title> </head> <body> <h3>Hello, <span th:text="${linkedInProfile.firstName}"></span><span th:text="${linkedInProfile.lastName}"></span></h3> <h4><span th:text="${linkedInProfile.emailAddress}"></span></h4> <h4><span th:text="${linkedInProfile.id}"></span></h4> <h4><span th:text="${linkedInProfile.location.country}"></span></h4> </body> </html>
Run the application on localhost: port/linkedin and you should be authenticated. Drop comments if you face any problem
If you want to change the default page flow (linkedinconnect.tml to connect/linkedinConnected.html),please follow below link
The post How to access LinkedIn data using spring-social appeared first on Little Big Extra.
]]>The post How to access google data using spring-social and spring boot appeared first on Little Big Extra.
]]>Spring social do have a spring-social-google project but it doesn’t have autoconfiguration for Google. So google authorization doesn’t work with spring boot autoconfigure straight away. I had created a pull request and merged the code to spring repository but got the following reply
Thanks for the PR. We’ve already considered this in #2548 where we decided that it isn’t something that we want to include in Spring Boot. That’s still the case so I’m going to close this. Thanks anyway.
So it doesn’t look like sooner or later they are going to add autoconfigure functionality to Spring-Social-Google
I would suggest you to first clone the as it does have required page structure and dependencies
Add the spring-social-google dependency
<dependency> <groupId>org.springframework.social</groupId> <artifactId>spring-social-google</artifactId> <version>1.0.0.RELEASE</version> </dependency>
Do Ctrl+Shift+T in your IDE(eclipse) and look for FacebookAutoConfiguration class you should be able to find it either in org.springframework.boot.autoconfigure.social package in spring-autoconfigure.jar
Copy this File and replace Facebook with Google. Alternatively, copy the below class and put in some package, make sure it is available in classpath(src/main/java or inside another source folder)
@Configuration @ConditionalOnClass({ SocialConfigurerAdapter.class, GoogleConnectionFactory.class }) @ConditionalOnProperty(prefix = "spring.social.google", name = "app-id") @AutoConfigureBefore(SocialWebAutoConfiguration.class) @AutoConfigureAfter(WebMvcAutoConfiguration.class) public class GoogleAutoConfiguration { @Configuration @EnableSocial @EnableConfigurationProperties(GoogleProperties.class) @ConditionalOnWebApplication protected static class GoogleConfigurerAdapter extends SocialAutoConfigurerAdapter { private final GoogleProperties properties; protected GoogleConfigurerAdapter(GoogleProperties properties) { this.properties = properties; } @Bean @ConditionalOnMissingBean(Google.class) @Scope(value = "request", proxyMode = ScopedProxyMode.INTERFACES) public Google google(ConnectionRepository repository) { Connection connection = repository.findPrimaryConnection(Google.class); return connection != null ? connection.getApi() : null; } @Bean(name = { "connect/googleConnect", "connect/googleConnected" }) @ConditionalOnProperty(prefix = "spring.social", name = "auto-connection-views") public GenericConnectionStatusView googleConnectView() { return new GenericConnectionStatusView("google", "Google"); } @Override protected ConnectionFactory<?> createConnectionFactory() { return new GoogleConnectionFactory(this.properties.getAppId(), this.properties.getAppSecret()); } } }
In the same package add the below class
import org.springframework.boot.autoconfigure.social.SocialProperties; import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties(prefix = "spring.social.google") public class GoogleProperties extends SocialProperties{ }
Now update the application.properties with your secret and client key
spring.social.google.appId=12894100090-tqso3lih5o42isneort886la2pesafmp.apps.googleusercontent.com spring.social.google.appSecret=9xfU16efvxQ-BTMsXT9wO
Under src/main/resources/templates/connect you should have facebookConnect.html and facebookConnected.html, if you have cloned the project properly.Again replace facebook with google in HTML and save them as googleConnected.html and googleConnect.html or copy them as shown below.
googleConnect.html
<html> <head> <title>Hello Google</title> </head> <body> <h3>Connect to Google</h3> <form action="/connect/google" method="POST"> <input type="hidden" name="scope" value="profile" /> <div class="formInfo"> <p>You aren't connected to Google yet. Click the button to connect this application with your account.</p> </div> <p><button type="submit">Connect to Google</button></p> </form> </body> </html>
googleConnected.html
<html> <head> <title>Hello Google</title> </head> <body> <h3>Connected to Google</h3> <p> You are now connected to your Google account. Click <a href="/google">here</a> to see some entries. </p> </body> </html>
We need to add a rest controller so it can handle google authorization requests
@Controller @RequestMapping("/google") public class GoogleController { private Google google; private ConnectionRepository connectionRepository; public GoogleController(Google google, ConnectionRepository connectionRepository) { this.google = google; this.connectionRepository = connectionRepository; } @GetMapping public String helloFacebook(Model model) { if (connectionRepository.findPrimaryConnection(Google.class) == null) { return "redirect:/connect/google"; } Person user = google.plusOperations().getGoogleProfile(); System.out.println(google.isAuthorized()); System.out.println(": "+user.getGivenName() +" : "+ user.getEmailAddresses() +" : "+ user.getFamilyName()); model.addAttribute("googleProfile", user); return "google"; } }
In src/main/resources add a file called “google.html” at same level as “hello.html”
<html> <head> <title>Hello Google</title> </head> <body> <h3>Hello, <span th:text="${googleProfile.givenName}"></span><span th:text="${googleProfile.familyName}"></span></h3> <h4><span th:text="${googleProfile.id}"></span></h4> <div th:each="post:${googleProfile.emailAddresses}"> Email : <b th:text="${post}"></b> <hr/> </div> </body> </html>
Run the application on localhost: port/google and you should be authenticated. Drop comments if you face any problem
The post How to access google data using spring-social and spring boot appeared first on Little Big Extra.
]]>The post Spring Social Facebook Authentication Example for multiple users appeared first on Little Big Extra.
]]>I was working on this and it was easy peasy to get it up and running. However, when I tested it for some time I realised that it only supports one user. So when the first users log in to facebook, only his details will be shared across all new sessions/users and they won’t be asked for any kind of authentication. One of the forum says that this example is supposed to demonstrate what can be done and is not intended for production use.
Spring Boot autoconfigures a lot of things behind the scenes. It does autoconfigure the Facebook, LinkedIn and Twitter properties and sets up the connection factories for social providers. However, the implementation of UserIdSource always returns “anonymous” as the user ID. Once the first Facebook connection is established the second browser will try to find a connection for “anonymous” which it finds and gives you an authorised Facebook object.
Here is the culprit code
@Configuration @EnableSocial @ConditionalOnWebApplication @ConditionalOnMissingClass("org.springframework.security.core.context.SecurityContextHolder") protected static class AnonymousUserIdSourceConfig extends SocialConfigurerAdapter { @Override public UserIdSource getUserIdSource() { return new UserIdSource() { @Override public String getUserId() { return "anonymous"; } }; } }
The solution is to override the “anonymous” as the UserId for each new user/session. So for each session, we can simply return a SessionID, however, it may not be unique enough to identify users, especially if it’sh being cached or stored somewhere in a connection database.
Using a Universally Unique Identifier(UUID) would be a safer bet.So we will store a new UUID in session so it persists between different requests but is alive only till the session is valid. See the below method which does the trick.
@Override public String getUserId() { RequestAttributes request = RequestContextHolder.currentRequestAttributes(); String uuid = (String) request.getAttribute("_socialUserUUID", RequestAttributes.SCOPE_SESSION); if (uuid == null) { uuid = UUID.randomUUID().toString(); request.setAttribute("_socialUserUUID", uuid, RequestAttributes.SCOPE_SESSION); } return uuid; }
You will need to create a class say Socialconfig and make sure it is available in the classpath (src/main/java/).Find below the complete code
import java.util.UUID; import org.springframework.context.annotation.Configuration; import org.springframework.social.UserIdSource; import org.springframework.social.config.annotation.SocialConfigurerAdapter; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; @Configuration public class SocialConfig extends SocialConfigurerAdapter { @Override public UserIdSource getUserIdSource() { return new SessionIdUserIdSource(); } private static final class SessionIdUserIdSource implements UserIdSource { @Override public String getUserId() { RequestAttributes request = RequestContextHolder.currentRequestAttributes(); String uuid = (String) request.getAttribute("_socialUserUUID", RequestAttributes.SCOPE_SESSION); if (uuid == null) { uuid = UUID.randomUUID().toString(); request.setAttribute("_socialUserUUID", uuid, RequestAttributes.SCOPE_SESSION); } return uuid; } } }
Follow this Video
The post Spring Social Facebook Authentication Example for multiple users appeared first on Little Big Extra.
]]>The post How to resolve – org.springframework.social.UncategorizedApiException: bio field is deprecated for versions v2.8 and higher appeared first on Little Big Extra.
]]>I encountered this error when using spring-social-facebook API. I did git clone of Spring tutorial for
I was hoping for it to work straightaway but got blocked by this error.
<dependency> <groupId>org.springframework.social</groupId> <artifactId>spring-social-core</artifactId> <version>2.0.0.M2</version> </dependency> <dependency> <groupId>org.springframework.social</groupId> <artifactId>spring-social-config</artifactId> <version>2.0.0.M2</version> </dependency> <dependency> <groupId>org.springframework.social</groupId> <artifactId>spring-social-facebook</artifactId> <version>3.0.0.M1</version> </dependency>
In case you get an exception saying that Repository not available, please add the following repository to your POM
<repositories> <repository> <id>alfresco-public</id> <url>https://artifacts.alfresco.com/nexus/content/groups/public</url> </repository> </repositories>
If you want to change the default page flow (facebookConnect.html to connect/facebookConnected.html),please follow below link
The post How to resolve – org.springframework.social.UncategorizedApiException: bio field is deprecated for versions v2.8 and higher appeared first on Little Big Extra.
]]>