JavaRush /Java Blog /Random EN /We are friends with the usual login via email and OAuth2 ...

We are friends with the usual login via email and OAuth2 in Spring Security using the note service as an example

Published in the Random EN group
While writing my application, I encountered a lack of intelligible articles on how to make the user register both via email and via social networks. There were good tutorials on setting up a classic login form. There were good tutorials on OAuth2 . How to make friends in two ways, there was criminally little information. In the process of searching, we managed to come up with a workable solution. It does not claim to be the ultimate truth, but it performs its function. In this article, I'll show you how to implement a note-taking service from scratch with a similar Spring Security configuration. We are friends with the usual login via email and OAuth2 in Spring Security using the example of a note service - 1Note:it’s good if the reader went through at least a couple of Spring tutorials, because the focus will only be on Spring Security, without detailed explanations of repositories, controllers, etc. Otherwise, a rather big article would turn out to be gigantic. Content
  1. Create a project
  2. Creating entities and application logic
    1. Essences
    2. Repositories
    3. Controllers
    4. Pages
  3. Configuring Spring Security for Classic Login
    1. Basic SecurityConfig
    2. Custom user login
    3. Improving 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

Create a project

We go to start.spring.io and form the basis of the project:
  • Web - running the application on the embedded Tomcat, url mappings, and the like;
  • JPA - database connection;
  • Mustache is a template engine used to generate web pages;
  • Security - application protection. That's 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 of your choice. I use MySQL as a 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 the following:
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

Essences

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 is required for the Spring Security configuration. The user will have an id, username (which 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 use only 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 it belongs to:
@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 do all the dirty work for us. Let's create a package repos, in it we'll 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.
The list of notes should only be accessed by an authorized user. The rest of the 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 the 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 that will become more complicated after the introduction 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";
  }
}
For the login page, we will not write a controller, because it is involved in Spring Security. Instead, we need a special configuration. As usual, let's create another package, call it config, put the class MvcConfig. When we write the Spring Security configuration, it will know which page we mean 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 templating engine to create pages . You can implement another one, it doesn't matter. For meta information that is used on all pages, a meta.mustache file has been created. Bootstrap is also included in it to make the pages of our project look prettier. Pages are created in the "src/main/resources/templates" directory. The files have the mustache extension. Placing the html code directly in the article will make it too big, 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'll create a concise working configuration in a class SecurityConfigthat inherits from WebSecurityConfigurerAdapter, which we'll 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 auto-configured pom.xml contained the version of the Spring Boot parent bean 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 SecurityConfig

Our configuration will be able to:
  1. Encrypt passwords with 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 homepage, registration and login pages. All other queries must be performed by logged in users. Let's assign the previously described "/login" as the login page. Upon successful login, the user will be taken to a page with a list of notes; if they fail, they 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

Self-written 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;
  }
}
UserServiceAs you may have noticed, the class that is in the package is responsible for loading the user services. In our case, it searches for a user not only by the field username, as a built-in implementation, but also by username, 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 form.
@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!

Improving the controller

We have configured Spring Security. It's time to take advantage of this in the note controller. Now each mapping will take an additional Principal parameter, by which it will try to find the user. Why can't you directly inject the class User? Then there will be a conflict due to a mismatch of user types when we write the login through social networks. We provide the necessary flexibility in advance. Now our note controller code 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: CSRF protection is enabled in the project 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

Let's try to run the project.
We are friends with the usual login via email and OAuth2 in Spring Security using the example of a note service - 1
We are friends with the usual login via email and OAuth2 in Spring Security using the example of a note service - 2
We see that a new user has appeared in the database. The password is encrypted.
We are friends with the usual login via email and OAuth2 in Spring Security using the example of a note service - 3
We are friends with the usual login via email and OAuth2 in Spring Security using the example of a note service - 4
We are friends with the usual login via email and OAuth2 in Spring Security using the example of a note service - 5
We are friends with the usual login via email and OAuth2 in Spring Security using the example of a note service - 6
The notes are saved to the database.
We are friends with the usual login via email and OAuth2 in Spring Security using the example of a note service - 7
We see that the project is successfully launched and running. For complete happiness, we lack only the ability to enter 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. Let's start by adding the @EnableOAuth2Client annotation. It will automatically pull up the necessary for login through social networks.

Filter configuration and application.properties

Let's implement an OAuth2ClientContext injection to use in our security configuration.
@Autowired
private OAuth2ClientContext oAuth2ClientContext;
The OAuth2ClientContext is used when creating a filter that validates a 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 in 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 about the client's registration with Google. The @ConfigurationProperties annotation specifies which configuration properties to look out 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 the 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 APIs: 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 app 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 similar to the C:\Windows\System32\drivers\etc\hosts file at the end. The main thing is that the domain should be in its 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 to 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 a freshly created class CustomUserInfoTokenServices. Most can be left unchanged. Before changing the logic of functions, we add as private fields of the class UserRepoandPasswordEncoder. Let's create setters for them. Let's add @Autowired UserRepo userRepo to the SecurityConfig class. Let's see how the pointer to the error disappears in the filter creation method, and rejoice. Why couldn't @Autowired be applied directly on CustomUserInfoTokenServices? Because this class will not pull up the dependency, since it itself is not marked with any Spring annotation, besides, its constructor is created explicitly when the filter is declared. Accordingly, the Spring DI mechanism does not know about it. If we annotate anything in this class with @Autowired we will get a NullPointerException when we use it. But through explicit setters, everything works very well. Once the required components are in place, the main object of interest is the loadAuthentication function, which retrieves a Map<String, Object> with information about the user. It was in it that in my and this project I implemented saving the user who entered through the social network into the database. Since we are using a Google account as the OAuth2 provider, we check if the map contains the Google-specific “sub” field. If it is present, then the information about the user was received correctly. 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 prescribe 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 advance in UserService the loading of a user through Google data, the application will work for both types of users. Let's modify the project's home page controller to redirect logged in users via 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 the addition of an exit button, we carry out the final launch of the project.
We are friends with the usual login via email and OAuth2 in Spring Security using the example of a note service - 8
We are friends with the usual login via email and OAuth2 in Spring Security using the note service as an example - 9
We are friends with the usual login via email and OAuth2 in Spring Security using the example of a note service - 10
We are friends with the usual login via email and OAuth2 in Spring Security using the example of a note service - 11
We are friends with the usual login via email and OAuth2 in Spring Security using the example of a note service - 12
We are friends with the usual login via email and OAuth2 in Spring Security using the example of a note service - 13
We are friends with the usual login via email and OAuth2 in Spring Security using the example of a note service - 14
We are friends with the usual login via email and OAuth2 in Spring Security using the example of a note service - 15
The user successfully logs in both through a regular form and through a Google account. This is what we were aiming for! I hope this article has cleared up certain points in creating a web application, securing it with Spring Security, and combining different login methods. With the complete project code, you can
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION