OAuth2 Client Authentication is a process in which a client application (like a web or mobile app) proves its identity to an OAuth2 authorization server to access resources securely. This prevents unauthorized access, ensures proper permissions, and validates token exchanges. Here’s a quick overview:
- Security: Ensures only legitimate clients can access resources.
- Authorization: Confirms that the client app has the right permissions.
- Token Exchange: Validates the legitimacy of a client before issuing access tokens.
- Authorization Code Flow: For apps needing user authentication (e.g., social login).
- Client Credentials Flow: For machine-to-machine communication using client credentials.
- Token Introspection: Verifies if an access token is still valid.
- Dependencies: Add
spring-boot-starter-oauth2-client
in the project. - Google Cloud Console:
- Create a project.
- Generate OAuth2.0 credentials.
- Configure the consent screen for user permissions.
- Scopes:
- Non-sensitive scopes: Basic profile info (e.g., email).
- Sensitive scopes: More private data (e.g., calendar access).
- Testing and Publishing:
- Test with up to 100 users before publishing.
- Set up authorized JavaScript origins and redirect URIs to control where OAuth requests are allowed.
This setup process involves configuring client secrets, authorized origins, and redirect URIs to ensure secure and controlled OAuth2 flows for applications.
Now you need to configure the Client ID and Client Secret in your application.
Configure Client and Secret
To avoid repeating same thing again and again, let’s use the another resource file that is called application.yml file. Go to your application’s resource package and create a new file (named application.yml). And put that particular value of client-id and client secret.
Note: Indentation is crucial in the application.yml
file for Spring Boot to read the configuration properly. Here’s how it should look with correct indentation:
spring:
security:
oauth2:
client:
registration:
google:
client-id: 234512600655-7u1u31s9oor02glldhpk9om40hl3kndf.apps.googleusercontent.com
client-secret: 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
Each level should be indented with two spaces to maintain proper YAML formatting, which ensures Spring Boot will parse the configuration correctly.
Configure the WebSecurityConfig
After all of these we have to configure our config class as well. Basically we have to add this ‘.oauth2Login()’ in our config class.
.oauth2Login(oauth2Login -> oauth2Login .failureUrl("/login?error=true") .successHandler(oAuth2SuccessHandler) )
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity //by adding this annotation we are telling springboot that we want to we want to configure this SpringSecurityFilterChain
@RequiredArgsConstructor
public class WebSecurityConfig {
private final JWTAuthFilter jwtAuthFilter;
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeHttpRequests(auth->
auth
.requestMatchers("/endpoint","/error","/auth/**","/home.html").permitAll()
.anyRequest().authenticated()) // All other requests require authentication
.csrf(csrfConfig-> // Disable CSRF for simplicity, not recommended for production
csrfConfig
.disable())
.sessionManagement(sessionConfig -> // Disable JSESSIONID for simplicity, not recommended for production
sessionConfig
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.oauth2Login(oauth2Config->oauth2Config
.failureUrl("/login?error=true")
.successHandler(oAuth2SuccessHandler) // to handle the error
);
return httpSecurity.build(); //when you add build this throws an exception
}
@Bean
AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
For OAuth2SuccessHandler we have to create a custom handler class where we extends the SimpleUrlAuthenticationSuccessHandler
class
and override the methodonAuthenticationSuccess
.
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
@RequiredArgsConstructor
@Slf4j
public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private final UserRepo userRepo;
private final UserServiceImpl userService;
private final JWTService jwtService;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
OAuth2AuthenticationToken token = (OAuth2AuthenticationToken) authentication;
DefaultOAuth2User defaultOAuth2User = (DefaultOAuth2User) token.getPrincipal();
String email =defaultOAuth2User.getAttribute("email");
UserEntity user= userService.getUserByEmail(email); //have to create a method in service
if(user == null){ //user with email id not present
UserEntity newUser = UserEntity
.builder() //have to add @Builder in UserEntity
.name(defaultOAuth2User.getAttribute("name"))
.email(email)
.build();
user = userService.save(newUser); //have to create a save method in service
}
String accessToken = jwtService.createAccessToken(user);
String refreshToken = jwtService.createRefreshToken(user);
Cookie cookie = new Cookie("refreshToken",refreshToken);
cookie.setHttpOnly(true); //always add
response.addCookie(cookie);
String frontendUrl = "http://localhost:8080/home.html?token="+accessToken; //to send the access token
// getRedirectStrategy().sendRedirect(request, response, frontendUrl);
//or you can use this
response.sendRedirect(frontendUrl);
}
}
Make some changes in UserService.
import lombok.RequiredArgsConstructor;
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;
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserDetailsService {
private final UserRepo userRepo;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return userRepo
.findByEmail(username)
// .orElse(null);
//or you can throw an exception here
.orElseThrow(()->new ResourceNotFoundException("USER WITH EMAIL "+username+" NOT FOUND"));
}
public UserEntity getUserById(Long userId){
return userRepo.findById(userId).orElseThrow(()->new ResourceNotFoundException("USER ID NOT FOUND: "+userId));
}
public UserEntity getUserByEmail(String mail){ //add this
return userRepo.findByEmail(mail).orElse(null);
}
public UserEntity save(UserEntity newUser) { // add this
return userRepo.save(newUser);
}
}
Create a home.html page
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Home</title>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #f0f0f0;
margin: 0;
}
.container {
text-align: center;
background-color: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
</style>
</head>
<body>
<div class="container">
<h1>Welcome to the Home Page</h1>
<p>You have successfully logged in using Google OAuth2.</p>
</div>
</body>
</html>
Output
After run this application successfully if we go to “/login” (http://localhost:8080/login) in browser login with one email id we can see this. Press ‘Google’.
Here you can see the application and you have to login with your provided email id.
After selecting an email, if you press 'Continue,' Google will share your name and email with the app.
This will take you to the next error page.
When we are not using any SuccessHandler in our code. Basically you can test it if you run your configure class with out adding any SuccessHandler you can see the error.
When we use SuccessHandler then we can fix these errors. You can check that accessToken in jwt.io
. Here if we are not using any separate home.html file.
When we create the home.html page it’s shown this.
Third Party Authentication