JavaRush /Java Blog /Random EN /Let's introduce regular login via email and OAuth2 to Spr...

Let's introduce regular login via email and OAuth2 to Spring Security using the example of the notes service

Published in the Random EN group
While writing my application, I encountered a lack of clear articles on how to get the user to register both via email and social networks. There were good tutorials on setting up the classic login form. There were good tutorials on OAuth2 . There was criminally little information on how to combine the two methods. During the search process, we were able to come up with a workable solution. It does not claim to be the ultimate truth, but it fulfills its function. In this article I will show how to implement a note storage service with a similar Spring Security configuration from scratch. Let's introduce regular login via email and OAuth2 to Spring Security using the example of the notes service - 1Note: it’s good if the reader has gone through at least a couple of tutorials on Spring, because attention will be focused only on Spring Security, without detailed explanations of repositories, controllers, etc. Otherwise, an already rather large article would turn out to be gigantic. Content
  1. Creating a Project
  2. Creating Entities and Application Logic
    1. Entities
    2. Repositories
    3. Controllers
    4. Pages
  3. Configuring Spring Security for Classic Login
    1. Basic configuration SecurityConfig
    2. Custom user login
    3. Let's improve the controller
    4. Launch
  4. Setting up OAuth2 using Google as an example in Spring Security
    1. Filter configuration and application.properties
    2. Highlights of registering an application with Google Cloud Platform
    3. CustomUserInfoTokenServices
  5. Final launch of the project

Creating a Project

We go to start.spring.io and form the basis of the project:
  • Web - launching an application on the built-in Tomcat, url mappings and the like;
  • JPA - database connection;
  • Mustache is a template engine used to generate web pages;
  • Security - application protection. This is what this article was created for.
Download the resulting archive and unpack it in the folder you need. We launch it in the IDE. You can choose the database at your discretion. I use MySQL as the database for the project, so I add the following dependency to the pom.xml file in the <dependencies> block:
<dependency>
     <groupId>mysql</groupId>
     <artifactId>mysql-connector-java</artifactId>
     <version>5.1.34</version>
</dependency>
The application.properties configuration is currently as follows:
spring.datasource.url=jdbc:mysql://localhost:3306/springsectut?createDatabaseIfNotExist=true&useSSL=false&autoReconnect=true&useLegacyDatetimeCode=false&serverTimezone=UTC&useUnicode=yes&characterEncoding=UTF-8
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.username=yourUsername
spring.datasource.password=yourPassword

spring.jpa.generate-ddl=true
spring.jpa.hibernate.ddl-auto=update

spring.jpa.properties.connection.characterEncoding=utf-8
spring.jpa.properties.connection.CharSet=utf-8
spring.jpa.properties.connection.useUnicode=true

spring.mustache.expose-request-attributes=true

Creating Entities and Application Logic

Entities

Let's create a package entitiesin which we will place the database entities. The user will be described by a class Userthat implements the interface UserDetails, which will be needed for the Spring Security configuration. The user will have an id, username (this is email), password, name, role, activity flag, Google account name and email ( googleNameand googleUsername).
@Entity
@Table(name = "user")
public class User implements UserDetails
{
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private Long id;

  private String username;
  private String password;
  private String name;
  private boolean active;
  private String googleName;
  private String googleUsername;

  @ElementCollection(targetClass = Role.class, fetch = FetchType.EAGER)
  @CollectionTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id"))
  @Enumerated(EnumType.STRING)
  private Set<Role> roles;

    //Геттеры, сеттеры, toString(), equals(), hashcode(), имплементация UserDetails
}
User roles are used to regulate access in Spring Security. Our application will only use one role:
public enum Role implements GrantedAuthority
{
  USER;

  @Override
  public String getAuthority()
  {
     return name();
  }
}
Let's create a note class with id, note title, note body and id of the user to whom it belongs:
@Entity
@Table(name = "note")
public class Note
{
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private Long id;

  private String title;
  private String note;
  private Long userId;

    //Геттеры, сеттеры, toString(), equals(), hashcode()
}

Repositories

To save entities to the database, we need repositories that will do all the dirty work for us. Let's create a package repos, in it we will create interfaces UserRepoinherited NoteRepofrom the interface JpaRepository<Entity, Id>.
@Service
@Repository
public interface UserRepo extends JpaRepository<User, Long>
{}

@Service
@Repository
public interface NoteRepo extends JpaRepository<Note, Long>
{
  List<Note> findByUserId(Long userId);
}

Controllers

Our notes service will have the following pages:
  • Home;
  • Registration;
  • Entrance;
  • List of user notes.
Only an authorized user should have access to the list of notes. The remaining pages are public. Let's create a package controllerscontaining a class IndexControllercontaining the usual get-mapping of the main page. The class RegistrationControlleris responsible for registering the user. Post-mapping takes data from the form, saves the user to the database and redirects to the login page. PasswordEncoderwill be described later. It is used to encrypt passwords.
@Controller
public class RegistrationController
{
  @Autowired
  private UserRepo userRepo;

  @Autowired
  private PasswordEncoder passwordEncoder;

  @GetMapping("/registration")
  public String registration()
  {
     return "registration";
  }

  @PostMapping("/registration")
  public String addUser(String name, String username, String password)
  {
     User user = new User();
     user.setName(name);
     user.setUsername(username);
     user.setPassword(passwordEncoder.encode(password));
     user.setActive(true);
     user.setRoles(Collections.singleton(Role.USER));

     userRepo.save(user);

     return "redirect:/login";
  }
The controller responsible for the notes list page currently contains simplified functionality, which will become more complex after the implementation of Spring Security.
@Controller
public class NoteController
{
  @Autowired
  private NoteRepo noteRepo;

  @GetMapping("/notes")
  public String notes(Model model)
  {
     List<Note> notes = noteRepo.findAll();
     model.addAttribute("notes", notes);

     return "notes";
  }

  @PostMapping("/addnote")
  public String addNote(String title, String note)
  {
     Note newNote = new Note();
     newNote.setTitle(title);
     newNote.setNote(note);

     noteRepo.save(newNote);

     return "redirect:/notes";
  }
}
We won't write a controller for the login page because it is used by Spring Security. Instead, we will need a special configuration. As usual, let's create another package, call it config, and place the class there MvcConfig. When we write the Spring Security configuration, it will know which page we are referring to when we use "/login".
@Configuration
public class MvcConfig implements WebMvcConfigurer
{
  public void addViewControllers(ViewControllerRegistry registry)
  {
     registry.addViewController("/login").setViewName("login");
     registry.setOrder(Ordered.HIGHEST_PRECEDENCE);
  }
}

Pages

I use the Mustache template engine to create pages . You can implement another one, it doesn’t matter. A meta.mustache file has been created for the meta information that is used on all pages. It also includes Bootstrap to make the pages of our project look prettier. Pages are created in the "src/main/resources/templates" directory. The files have the extension mustache. Placing the html code directly in the article will make it too large, so here is a link to the templates folder in the project's GitHub repository .

Configuring Spring Security for Classic Login

Spring Security helps us protect the application and its resources from unauthorized access. We will create a concise working configuration in a class SecurityConfiginherited from WebSecurityConfigurerAdapter, which we will place in the package config. Let's mark it with the @EnableWebSecurity annotation, which will enable Spring Security support, and the @Configuration annotation, which indicates that this class contains some configuration. Note: the automatically configured pom.xml contained version of the Spring Boot parent component 2.1.4.RELEASE, which prevented Security from being implemented in the established way. To avoid conflicts in the project, it is recommended to change the version to 2.0.1.RELEASE.

Basic configuration SecurityConfig

Our configuration will be able to:
  1. Encrypt passwords using BCryptPasswordEncoder:

    @Autowired
    private PasswordEncoder passwordEncoder;
    
    @Bean
    PasswordEncoder passwordEncoder()
    {
      PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
      return passwordEncoder;
    }
  2. Log in using a specially written authentication provider:

    @Autowired
    private AuthProvider authProvider;
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth)
    {
      auth.authenticationProvider(authProvider);
    }
  3. Allow anonymous users access to the home page, registration and login pages. All other requests must be performed by logged-in users. Let’s assign the previously described “/login” as the login page. If the login is successful, the user will be taken to a page with a list of notes; if there is an error, the user will remain on the login page. Upon successful exit, the user will be taken to the main page.

    @Override
    protected void configure(HttpSecurity http) throws Exception
    {
      http
            .authorizeRequests()
            .antMatchers("/resources/**", "/", "/login**", "/registration").permitAll()
            .anyRequest().authenticated()
            .and().formLogin().loginPage("/login")
            .defaultSuccessUrl("/notes").failureUrl("/login?error").permitAll()
            .and().logout().logoutSuccessUrl("/").permitAll();
    }

Custom user login

A self-written one AuthProviderwill allow the user to log in not only by email, but also by username.
@Component
public class AuthProvider implements AuthenticationProvider
{
  @Autowired
  private UserService userService;

  @Autowired
  private PasswordEncoder passwordEncoder;

  public Authentication authenticate(Authentication authentication) throws AuthenticationException
  {
     String username = authentication.getName();
     String password = (String) authentication.getCredentials();

     User user = (User) userService.loadUserByUsername(username);

     if(user != null && (user.getUsername().equals(username) || user.getName().equals(username)))
     {
        if(!passwordEncoder.matches(password, user.getPassword()))
        {
           throw new BadCredentialsException("Wrong password");
        }

        Collection<? extends GrantedAuthority> authorities = user.getAuthorities();

        return new UsernamePasswordAuthenticationToken(user, password, authorities);
     }
     else
        throw new BadCredentialsException("Username not found");
  }

  public boolean supports(Class<?> arg)
  {
     return true;
  }
}
As you may have noticed, the class UserServicelocated in the package is responsible for loading the user services. In our case, it searches for a user not only by the field username, like the built-in implementation, but also by user name, Google account name and Google account email. The last two methods will be useful to us when implementing login via OAuth2. Here the class is given in an abbreviated version.
@Service
public class UserService implements UserDetailsService
{
  @Autowired
  private UserRepo userRepo;

  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
  {
     User userFindByUsername = userRepo.findByUsername(username);
     //Остальные поиски

     if(userFindByUsername != null)
     {
        return userFindByUsername;
     }
     //Остальные проверки
     return null;
  }
}
Note: do not forget to write the necessary methods in UserRepo!

Let's improve the controller

We have configured Spring Security. Now is the time to take advantage of this in your notes controller. Now each mapping will accept an additional Principal parameter, by which it will try to find the user. Why can't I directly inject the class User? Then there will be a conflict due to a mismatch of user types when we write a login through social networks. We provide the necessary flexibility in advance. Our notes controller code now looks like this:
@GetMapping("/notes")
public String notes(Principal principal, Model model)
{
  User user = (User) userService.loadUserByUsername(principal.getName());
  List<Note> notes = noteRepo.findByUserId(user.getId());
  model.addAttribute("notes", notes);
  model.addAttribute("user", user);

  return "notes";
}

@PostMapping("/addnote")
public String addNote(Principal principal, String title, String note)
{
  User user = (User) userService.loadUserByUsername(principal.getName());

  Note newNote = new Note();
  newNote.setTitle(title);
  newNote.setNote(note);
  newNote.setUserId(user.getId());

  noteRepo.save(newNote);

  return "redirect:/notes";
}
Note: the project has CSRF protection enabled by default , so either disable it for yourself (http.csrf().disable()), or do not forget, as the author of the article, to add a hidden field with a csrf token to all post requests.

Launch

We are trying to launch the project.
Let's introduce regular login via email and OAuth2 to Spring Security using the example of the notes service - 1
Let's introduce regular login via email and OAuth2 to Spring Security using the example of the notes service - 2
We see that a new user has appeared in the database. The password is encrypted.
Let's introduce regular login via email and OAuth2 to Spring Security using the example of the notes service - 3
Let's introduce regular login via email and OAuth2 to Spring Security using the example of the notes service - 4
Let's introduce regular login via email and OAuth2 to Spring Security using the example of the notes service - 5
Let's introduce regular login via email and OAuth2 to Spring Security using the example of the notes service - 6
The notes are saved to the database.
Let's introduce regular login via email and OAuth2 to Spring Security using the example of the notes service - 7
We see that the project is successfully launched and running. For complete happiness, we only need the ability to log in through social networks. Well, let's get started!

Setting up OAuth2 using Google as an example in Spring Security

When implementing OAuth2, I relied on this official tutorial from Spring . To support OAuth2, add the following library to pom.xml:
<dependency>
  <groupId>org.springframework.security.oauth.boot</groupId>
  <artifactId>spring-security-oauth2-autoconfigure</artifactId>
  <version>2.0.0.RELEASE</version>
</dependency>
Let's modify our Spring Security configuration in the SecurityConfig. First, let's add the @EnableOAuth2Client annotation. It will automatically pull up what you need to log in via social networks.

Filter configuration and application.properties

Let's inject OAuth2ClientContext to use in our security configuration.
@Autowired
private OAuth2ClientContext oAuth2ClientContext;
OAuth2ClientContext is used when creating a filter that validates the user's social login request. The filter is available thanks to the @EnableOAuth2Client annotation. All we need to do is call it in the correct order, before the main Spring Security filter. Only then will we be able to catch redirects during the login process with OAuth2. To do this, we use FilterRegistrationBean, in which we set the priority of our filter to -100.
@Bean
public FilterRegistrationBean oAuth2ClientFilterRegistration(OAuth2ClientContextFilter oAuth2ClientContextFilter)
{
  FilterRegistrationBean registration = new FilterRegistrationBean();
  registration.setFilter(oAuth2ClientContextFilter);
  registration.setOrder(-100);
  return registration;
}

private Filter ssoFilter()
{
  OAuth2ClientAuthenticationProcessingFilter googleFilter = new OAuth2ClientAuthenticationProcessingFilter("/login/google");
  OAuth2RestTemplate googleTemplate = new OAuth2RestTemplate(google(), oAuth2ClientContext);
  googleFilter.setRestTemplate(googleTemplate);
  CustomUserInfoTokenServices tokenServices = new CustomUserInfoTokenServices(googleResource().getUserInfoUri(), google().getClientId());
  tokenServices.setRestTemplate(googleTemplate);
  googleFilter.setTokenServices(tokenServices);
  tokenServices.setUserRepo(userRepo);
  tokenServices.setPasswordEncoder(passwordEncoder);
  return googleFilter;
}
You also need to add a new filter to the configure(HttpSecurity http) function:
http.addFilterBefore(ssoFilter(), UsernamePasswordAuthenticationFilter.class);
The filter also needs to know that the client has registered via Google. The @ConfigurationProperties annotation specifies which configuration properties to look for in application.properties.
@Bean
@ConfigurationProperties("google.client")
public AuthorizationCodeResourceDetails google()
{
  return new AuthorizationCodeResourceDetails();
}
To complete authentication, you need to specify the Google user information endpoint:
@Bean
@ConfigurationProperties("google.resource")
public ResourceServerProperties googleResource()
{
  return new ResourceServerProperties();
}
Having registered our application in Google Cloud Platform , we will add properties with the appropriate prefixes to application.properties:
google.client.clientId=yourClientId
google.client.clientSecret=yourClientSecret
google.client.accessTokenUri=https://www.googleapis.com/oauth2/v4/token
google.client.userAuthorizationUri=https://accounts.google.com/o/oauth2/v2/auth
google.client.clientAuthenticationScheme=form
google.client.scope=openid,email,profile
google.resource.userInfoUri=https://www.googleapis.com/oauth2/v3/userinfo
google.resource.preferTokenInfo=true

Highlights of registering an application with Google Cloud Platform

Path: APIs and Services -> Credentials OAuth Access Request Window:
  • Application name: Spring login form and OAuth2 tutorial
  • Support email address: your email
  • Scope for Google API: email, profile, openid
  • Authorized domains: me.org
  • Link to the main page of the application: http://me.org:8080
  • Link to app privacy policy: http://me.org:8080
  • Link to application terms of use: http://me.org:8080
Credentials:
  • Type: Web Application
  • Title: Spring login form and OAuth2 tutorial
  • Allowed JavaScript sources: http://me.org, http://me.org:8080
  • Allowed redirect URIs: http://me.org:8080/login, http://me.org:8080/login/google
Note: since Google does not want to work with the address localhost:8080, add the line “127.0.0.1 me.org” or something similar to the file C:\Windows\System32\drivers\etc\hosts at the end. The main thing is that the domain is in a classic form.

CustomUserInfoTokenServices

Did you notice the word Custom in the filter function description? Class CustomUserInfoTokenServices. Yes, we will create our own class with blackjack and the ability to save the user in the database! Using the Ctrl-N keyboard shortcut in IntelliJ IDEA, you can find and see how the UserInfoTokenServicesdefault is implemented. Let's copy its code into the newly created class CustomUserInfoTokenServices. Most of it can be left unchanged. Before changing the logic of the functions, let’s add UserRepoand as private fields of the class PasswordEncoder. Let's create setters for them. Let's add @Autowired UserRepo userRepo to the SecurityConfig class. We look at how the pointer to the error in the filter creation method disappears, and we rejoice. Why couldn't @Autowired be applied directly to CustomUserInfoTokenServices? Because this class will not pick up the dependency, since it itself is not marked with any Spring annotation, and its constructor is created explicitly when the filter is declared. Accordingly, Spring's DI mechanism does not know about it. If we annotate @Autowired on anything in this class, we will get a NullPointerException when used. But through explicit setters everything works very well. After implementing the necessary components, the main object of interest becomes the loadAuthentication function, in which the Map<String, Object> with information about the user is retrieved. It was in this project that I implemented the saving of a user logged in through a social network into the database. Since we are using a Google account as an OAuth2 provider, we check whether map contains the “sub” field that is typical for Google. If it is present, it means that the information about the user was received correctly. We create a new user and save it to the database.
@Override
public OAuth2Authentication loadAuthentication(String accessToken)
     throws AuthenticationException, InvalidTokenException
{
  Map<String, Object> map = getMap(this.userInfoEndpointUrl, accessToken);

  if(map.containsKey("sub"))
  {
     String googleName = (String) map.get("name");
     String googleUsername = (String) map.get("email");

     User user = userRepo.findByGoogleUsername(googleUsername);

     if(user == null)
     {
        user = new User();
        user.setActive(true);
        user.setRoles(Collections.singleton(Role.USER));
     }

     user.setName(googleName);
     user.setUsername(googleUsername);
     user.setGoogleName(googleName);
     user.setGoogleUsername(googleUsername);
     user.setPassword(passwordEncoder.encode("oauth2user"));

     userRepo.save(user);
  }

  if (map.containsKey("error"))
  {
     this.logger.debug("userinfo returned error: " + map.get("error"));
     throw new InvalidTokenException(accessToken);
  }
  return extractAuthentication(map);
}
When using several providers, you can specify different options in one CustomUserInfoTokenServices, and register different classes of similar services in the filter declaration method. Now both User and OAuth2Authentication can act as Principal. Since we took into account in UserService in advance the loading of the user through Google data, the application will work for both types of users. We modify the project's main page controller so that it redirects users logged in using OAuth2 to the notes page.
@GetMapping("/")
public String index(Principal principal)
{
  if(principal != null)
  {
     return "redirect:/notes";
  }
  return "index";
}

Final launch of the project

After minor cosmetic changes and adding an exit button, we carry out the final launch of the project.
Let's introduce regular login via email and OAuth2 to Spring Security using the example of the notes service - 8
Let's introduce regular login via email and OAuth2 to Spring Security using the example of the notes service - 9
Let's introduce regular login via email and OAuth2 to Spring Security using the example of the notes service - 10
Let's introduce regular login via email and OAuth2 to Spring Security using the example of the notes service - 11
Let's introduce regular login via email and OAuth2 to Spring Security using the example of the notes service - 12
Let's introduce regular login via email and OAuth2 to Spring Security using the example of the notes service - 13
Let's introduce regular login via email and OAuth2 to Spring Security using the example of the notes service - 14
Let's introduce regular login via email and OAuth2 to Spring Security using the example of the notes service - 15
The user successfully logs in both through the regular form and through a Google account. This is what we wanted! I hope this article has cleared up some points about creating a web application, securing it with Spring Security, and combining different login methods. With the full project code you can
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION