In Part1, we looked at the very basic configuration for OAuth2, now we will implement the logic to save user info returned from auth provider to our database. Let’s first create a docker compose file to start a local db, and connect our app to the database.

Database Connection

  1. Add the following dependencies to our “build.gradle”,
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'mysql:mysql-connector-java:8.0.33'

2. Create a docker-compose.yaml like below

version: '3'
services:
db:
hostname: mysql
image: mysql:8.0.23
restart: always
volumes:
- db_data:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: password
ports:
- 3306:3306
adminer:
hostname: mysql
image: adminer
restart: always
ports:
- 8580:8080

volumes:
db_data:

3. Add db connection configuration to our application.yaml

spring:
datasource:
url: jdbc:mysql://localhost:3306/spring_oauth2?allowPublicKeyRetrieval=true&useSSL=false&serverTimezone=UTC&useLegacyDatetimeCode=false&createDatabaseIfNotExist=true
username: root
password: password
jpa:
show-sql: true
hibernate:
ddl-auto: update
naming-strategy: org.hibernate.cfg.ImprovedNamingStrategy

That is it, we have successfully configured database for our application, now we need to create the repository and entities to save/retrieve user info.

Database Artifacts — Entities & Repository classes

  1. We need to create a user entity implementing UserDetails interface
package az.ingress.oauth2.springoauth2.domain;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import lombok.Data;
import org.hibernate.annotations.JdbcTypeCode;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Set;
import java.util.UUID;

@Data
@Entity
public class User implements UserDetails {

@Id
@JdbcTypeCode(java.sql.Types.VARCHAR)
private UUID id;

@OneToMany
private Set<Authority> authorities;

private String password;

private String name;

private String picture;

@Column(unique = true)
private String username;

private boolean accountNonExpired;

private boolean accountNonLocked;

private boolean credentialsNonExpired;

private String provider;

private String providerId;

private boolean enabled;
}

2. We need to create an authority entity implementing GrantedAuthority interface

package az.ingress.oauth2.springoauth2.domain;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import lombok.Data;
import org.hibernate.annotations.JdbcTypeCode;
import org.springframework.security.core.GrantedAuthority;

import java.util.UUID;

@Data
@Entity
public class Authority implements GrantedAuthority {

@Id
@JdbcTypeCode(java.sql.Types.VARCHAR)
private UUID id;

public String authority;
}

3. We nee to create a repository interface to manage User entity operations

package az.ingress.oauth2.springoauth2.repository;

import az.ingress.oauth2.springoauth2.domain.User;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;
import java.util.UUID;

public interface UserRepository extends JpaRepository<User, UUID> {

@EntityGraph(attributePaths = "authorities")
Optional<User> findByUsername(String username);
}

That is all we are ready to save user details now.

Now, here comes the main step, we need to go back to the oauth2 config and customize oauth2Login method.

http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
).oauth2Login(Customizer.withDefaults());

In addition to this, we will need to move the googleClientRegistration configuration into our application.yaml

spring:
datasource:
url: jdbc:mysql://localhost:3306/spring_oauth2?allowPublicKeyRetrieval=true&useSSL=false&serverTimezone=UTC&useLegacyDatetimeCode=false&createDatabaseIfNotExist=true
username: root
password: password
jpa:
show-sql: true
hibernate:
ddl-auto: update
naming-strategy: org.hibernate.cfg.ImprovedNamingStrategy
security:
oauth2:
client:
registration:
google:
clientId: 289048981423-l3u56222hquksvi8e8muml6tt9harhca.apps.googleusercontent.com
clientSecret: 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
redirectUri: "{baseUrl}/login/oauth2/code/{registrationId}"
authorizationUri: "https://accounts.google.com/o/oauth2/v2/auth"
tokenUri: "https://www.googleapis.com/oauth2/v4/token"
userInfoUri: "https://www.googleapis.com/oauth2/v3/userinfo"
jwkSetUri: "https://www.googleapis.com/oauth2/v3/certs"
clientName: "Google"
scope:
- email
- profile
- email
- address
- phone

and remove the clientRegistrationRepository meth from our configuration. Now, our final SecurityConfig class will look like,

package az.ingress.oauth2.springoauth2.config;

import az.ingress.oauth2.springoauth2.service.OAuth2UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Slf4j
@Configuration
@RequiredArgsConstructor
public class SecurityConfig {

private final OAuth2UserService oAuth2UserService;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
log.trace("Configuring http filterChain");
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest()
.authenticated()
).oauth2Login(oauth2 -> oauth2
.userInfoEndpoint(infoEndpoint ->
infoEndpoint.userService(oAuth2UserService)));
return http.build();
}
}

Finally, it is time to implement the OAuth2UserService and include logic to save and update user in our database.

package az.ingress.oauth2.springoauth2.service;

import az.ingress.oauth2.springoauth2.domain.User;
import az.ingress.oauth2.springoauth2.dto.Oauth2UserInfoDto;
import az.ingress.oauth2.springoauth2.dto.UserPrincipal;
import az.ingress.oauth2.springoauth2.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;

import java.util.Optional;
import java.util.UUID;

@Slf4j
@Service
@RequiredArgsConstructor
public class OAuth2UserService extends DefaultOAuth2UserService {

private final UserRepository userRepository;

@Override
@SneakyThrows
public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) {
log.trace("Load user {}", oAuth2UserRequest);
OAuth2User oAuth2User = super.loadUser(oAuth2UserRequest);
return processOAuth2User(oAuth2UserRequest, oAuth2User);
}

private OAuth2User processOAuth2User(OAuth2UserRequest oAuth2UserRequest, OAuth2User oAuth2User) {
Oauth2UserInfoDto userInfoDto = Oauth2UserInfoDto
.builder()
.name(oAuth2User.getAttributes().get("name").toString())
.id(oAuth2User.getAttributes().get("sub").toString())
.email(oAuth2User.getAttributes().get("email").toString())
.picture(oAuth2User.getAttributes().get("picture").toString())
.build();

log.trace("User info is {}", userInfoDto);
Optional<User> userOptional = userRepository.findByUsername(userInfoDto.getEmail());
log.trace("User is {}", userOptional);
User user = userOptional
.map(existingUser -> updateExistingUser(existingUser, userInfoDto))
.orElseGet(() -> registerNewUser(oAuth2UserRequest, userInfoDto));
return UserPrincipal.create(user, oAuth2User.getAttributes());
}

private User registerNewUser(OAuth2UserRequest oAuth2UserRequest, Oauth2UserInfoDto userInfoDto) {
User user = new User();
user.setProvider(oAuth2UserRequest.getClientRegistration().getRegistrationId());
user.setProviderId(userInfoDto.getId());
user.setName(userInfoDto.getName());
user.setUsername(userInfoDto.getEmail());
user.setPicture(userInfoDto.getPicture());
user.setId(UUID.randomUUID());
return userRepository.save(user);
}

private User updateExistingUser(User existingUser, Oauth2UserInfoDto userInfoDto) {
existingUser.setName(userInfoDto.getName());
existingUser.setPicture(userInfoDto.getPicture());
return userRepository.save(existingUser);
}

}

Congratulations, we have successfully implemented the logic to save the user info int our database.

In the next series, we will make the code more production ready and implement the logic to handle stateless request from react client application.

For more https://ingress.academy/en

--

--