Spring Security OAuth2: Complete Social Login Guide (Google, GitHub)
Implement OAuth2 social login in Spring Boot. Learn Google, GitHub authentication, custom OAuth2 providers, and combining with JWT.
Moshiour Rahman
Advertisement
Why OAuth2 Social Login?
Social login offers major benefits:
| Benefit | Description |
|---|---|
| Better UX | No password to remember |
| Higher conversion | Faster signup = more users |
| Less security burden | Provider handles password security |
| Verified emails | Most providers verify email |
OAuth2 Flow Overview

Project Setup
Dependencies
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
</dependencies>
Google OAuth2 Setup
1. Create Google Cloud Project
- Go to Google Cloud Console
- Create a new project
- Navigate to “APIs & Services” → “Credentials”
- Click “Create Credentials” → “OAuth 2.0 Client IDs”
- Configure consent screen first
- Select “Web application”
- Add authorized redirect URI:
http://localhost:8080/login/oauth2/code/google - Copy Client ID and Client Secret
2. Application Configuration
spring:
security:
oauth2:
client:
registration:
google:
client-id: ${GOOGLE_CLIENT_ID}
client-secret: ${GOOGLE_CLIENT_SECRET}
scope:
- email
- profile
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
GitHub OAuth2 Setup
1. Create GitHub OAuth App
- Go to GitHub → Settings → Developer settings → OAuth Apps
- Click “New OAuth App”
- Fill in:
- Application name: Your App Name
- Homepage URL:
http://localhost:8080 - Authorization callback URL:
http://localhost:8080/login/oauth2/code/github
- Copy Client ID and generate Client Secret
2. Application Configuration
spring:
security:
oauth2:
client:
registration:
github:
client-id: ${GITHUB_CLIENT_ID}
client-secret: ${GITHUB_CLIENT_SECRET}
scope:
- user:email
- read:user
Complete Configuration
spring:
security:
oauth2:
client:
registration:
google:
client-id: ${GOOGLE_CLIENT_ID}
client-secret: ${GOOGLE_CLIENT_SECRET}
scope:
- email
- profile
github:
client-id: ${GITHUB_CLIENT_ID}
client-secret: ${GITHUB_CLIENT_SECRET}
scope:
- user:email
- read:user
provider:
github:
user-info-uri: https://api.github.com/user
user-name-attribute: login
app:
oauth2:
authorized-redirect-uris:
- http://localhost:3000/oauth2/redirect
- http://localhost:8080/oauth2/redirect
Database Entities
User Entity with OAuth2 Support
@Entity
@Table(name = "users")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String email;
private String password;
private String name;
private String imageUrl;
@Column(nullable = false)
private Boolean emailVerified = false;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private AuthProvider provider;
private String providerId;
@Enumerated(EnumType.STRING)
private Role role = Role.USER;
@Column(name = "created_at")
private LocalDateTime createdAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
}
}
AuthProvider Enum
public enum AuthProvider {
LOCAL,
GOOGLE,
GITHUB,
FACEBOOK
}
User Repository
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
Optional<User> findByProviderAndProviderId(AuthProvider provider, String providerId);
boolean existsByEmail(String email);
}
Custom OAuth2 User Service
OAuth2UserInfo Abstraction
public abstract class OAuth2UserInfo {
protected Map<String, Object> attributes;
public OAuth2UserInfo(Map<String, Object> attributes) {
this.attributes = attributes;
}
public Map<String, Object> getAttributes() {
return attributes;
}
public abstract String getId();
public abstract String getName();
public abstract String getEmail();
public abstract String getImageUrl();
}
Google User Info
public class GoogleOAuth2UserInfo extends OAuth2UserInfo {
public GoogleOAuth2UserInfo(Map<String, Object> attributes) {
super(attributes);
}
@Override
public String getId() {
return (String) attributes.get("sub");
}
@Override
public String getName() {
return (String) attributes.get("name");
}
@Override
public String getEmail() {
return (String) attributes.get("email");
}
@Override
public String getImageUrl() {
return (String) attributes.get("picture");
}
}
GitHub User Info
public class GithubOAuth2UserInfo extends OAuth2UserInfo {
public GithubOAuth2UserInfo(Map<String, Object> attributes) {
super(attributes);
}
@Override
public String getId() {
return ((Integer) attributes.get("id")).toString();
}
@Override
public String getName() {
return (String) attributes.get("name");
}
@Override
public String getEmail() {
return (String) attributes.get("email");
}
@Override
public String getImageUrl() {
return (String) attributes.get("avatar_url");
}
}
OAuth2UserInfo Factory
public class OAuth2UserInfoFactory {
public static OAuth2UserInfo getOAuth2UserInfo(
String registrationId,
Map<String, Object> attributes) {
if (registrationId.equalsIgnoreCase(AuthProvider.GOOGLE.name())) {
return new GoogleOAuth2UserInfo(attributes);
} else if (registrationId.equalsIgnoreCase(AuthProvider.GITHUB.name())) {
return new GithubOAuth2UserInfo(attributes);
} else {
throw new OAuth2AuthenticationProcessingException(
"Login with " + registrationId + " is not supported");
}
}
}
Custom OAuth2 User Principal
public class UserPrincipal implements OAuth2User, UserDetails {
private Long id;
private String email;
private String password;
private Collection<? extends GrantedAuthority> authorities;
private Map<String, Object> attributes;
public UserPrincipal(Long id, String email, String password,
Collection<? extends GrantedAuthority> authorities) {
this.id = id;
this.email = email;
this.password = password;
this.authorities = authorities;
}
public static UserPrincipal create(User user) {
List<GrantedAuthority> authorities = Collections.singletonList(
new SimpleGrantedAuthority("ROLE_" + user.getRole().name())
);
return new UserPrincipal(
user.getId(),
user.getEmail(),
user.getPassword(),
authorities
);
}
public static UserPrincipal create(User user, Map<String, Object> attributes) {
UserPrincipal userPrincipal = UserPrincipal.create(user);
userPrincipal.setAttributes(attributes);
return userPrincipal;
}
public Long getId() {
return id;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return email;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public Map<String, Object> getAttributes() {
return attributes;
}
public void setAttributes(Map<String, Object> attributes) {
this.attributes = attributes;
}
@Override
public String getName() {
return String.valueOf(id);
}
}
Custom OAuth2 User Service
@Service
@RequiredArgsConstructor
@Slf4j
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
private final UserRepository userRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest)
throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
try {
return processOAuth2User(userRequest, oAuth2User);
} catch (AuthenticationException ex) {
throw ex;
} catch (Exception ex) {
throw new InternalAuthenticationServiceException(
ex.getMessage(), ex.getCause());
}
}
private OAuth2User processOAuth2User(
OAuth2UserRequest userRequest,
OAuth2User oAuth2User) {
String registrationId = userRequest.getClientRegistration()
.getRegistrationId();
OAuth2UserInfo oAuth2UserInfo = OAuth2UserInfoFactory
.getOAuth2UserInfo(registrationId, oAuth2User.getAttributes());
if (StringUtils.isEmpty(oAuth2UserInfo.getEmail())) {
throw new OAuth2AuthenticationProcessingException(
"Email not found from OAuth2 provider");
}
Optional<User> userOptional = userRepository
.findByEmail(oAuth2UserInfo.getEmail());
User user;
if (userOptional.isPresent()) {
user = userOptional.get();
if (!user.getProvider().equals(
AuthProvider.valueOf(registrationId.toUpperCase()))) {
throw new OAuth2AuthenticationProcessingException(
"You're signed up with " + user.getProvider() +
" account. Please use your " + user.getProvider() +
" account to login.");
}
user = updateExistingUser(user, oAuth2UserInfo);
} else {
user = registerNewUser(userRequest, oAuth2UserInfo);
}
return UserPrincipal.create(user, oAuth2User.getAttributes());
}
private User registerNewUser(
OAuth2UserRequest userRequest,
OAuth2UserInfo oAuth2UserInfo) {
String registrationId = userRequest.getClientRegistration()
.getRegistrationId();
User user = User.builder()
.provider(AuthProvider.valueOf(registrationId.toUpperCase()))
.providerId(oAuth2UserInfo.getId())
.name(oAuth2UserInfo.getName())
.email(oAuth2UserInfo.getEmail())
.imageUrl(oAuth2UserInfo.getImageUrl())
.emailVerified(true)
.role(Role.USER)
.build();
log.info("Registering new OAuth2 user: {}", user.getEmail());
return userRepository.save(user);
}
private User updateExistingUser(User existingUser, OAuth2UserInfo oAuth2UserInfo) {
existingUser.setName(oAuth2UserInfo.getName());
existingUser.setImageUrl(oAuth2UserInfo.getImageUrl());
return userRepository.save(existingUser);
}
}
OAuth2 + JWT Integration
Stateless OAuth2 with JWT
@Service
@RequiredArgsConstructor
public class TokenProvider {
@Value("${app.jwt.secret}")
private String jwtSecret;
@Value("${app.jwt.expiration-ms}")
private long jwtExpirationMs;
public String createToken(Authentication authentication) {
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
Date now = new Date();
Date expiryDate = new Date(now.getTime() + jwtExpirationMs);
return Jwts.builder()
.subject(Long.toString(userPrincipal.getId()))
.issuedAt(now)
.expiration(expiryDate)
.signWith(getSigningKey())
.compact();
}
public Long getUserIdFromToken(String token) {
Claims claims = Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
return Long.parseLong(claims.getSubject());
}
public boolean validateToken(String token) {
try {
Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}
private SecretKey getSigningKey() {
byte[] keyBytes = Decoders.BASE64.decode(jwtSecret);
return Keys.hmacShaKeyFor(keyBytes);
}
}
OAuth2 Success Handler (Returns JWT)
@Component
@RequiredArgsConstructor
public class OAuth2AuthenticationSuccessHandler
extends SimpleUrlAuthenticationSuccessHandler {
private final TokenProvider tokenProvider;
private final HttpCookieOAuth2AuthorizationRequestRepository
authorizationRequestRepository;
@Value("${app.oauth2.authorized-redirect-uris}")
private List<String> authorizedRedirectUris;
@Override
public void onAuthenticationSuccess(
HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException {
String targetUrl = determineTargetUrl(request, response, authentication);
if (response.isCommitted()) {
logger.debug("Response already committed. Unable to redirect to " + targetUrl);
return;
}
clearAuthenticationAttributes(request, response);
getRedirectStrategy().sendRedirect(request, response, targetUrl);
}
protected String determineTargetUrl(
HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) {
Optional<String> redirectUri = CookieUtils.getCookie(request, REDIRECT_URI_PARAM_COOKIE_NAME)
.map(Cookie::getValue);
if (redirectUri.isPresent() && !isAuthorizedRedirectUri(redirectUri.get())) {
throw new BadRequestException(
"Unauthorized Redirect URI");
}
String targetUrl = redirectUri.orElse(getDefaultTargetUrl());
String token = tokenProvider.createToken(authentication);
return UriComponentsBuilder.fromUriString(targetUrl)
.queryParam("token", token)
.build().toUriString();
}
protected void clearAuthenticationAttributes(
HttpServletRequest request,
HttpServletResponse response) {
super.clearAuthenticationAttributes(request);
authorizationRequestRepository.removeAuthorizationRequestCookies(request, response);
}
private boolean isAuthorizedRedirectUri(String uri) {
URI clientRedirectUri = URI.create(uri);
return authorizedRedirectUris.stream()
.anyMatch(authorizedRedirectUri -> {
URI authorizedURI = URI.create(authorizedRedirectUri);
return authorizedURI.getHost().equalsIgnoreCase(clientRedirectUri.getHost())
&& authorizedURI.getPort() == clientRedirectUri.getPort();
});
}
}
OAuth2 Failure Handler
@Component
@RequiredArgsConstructor
public class OAuth2AuthenticationFailureHandler
extends SimpleUrlAuthenticationFailureHandler {
private final HttpCookieOAuth2AuthorizationRequestRepository
authorizationRequestRepository;
@Override
public void onAuthenticationFailure(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception) throws IOException {
String targetUrl = CookieUtils.getCookie(request, REDIRECT_URI_PARAM_COOKIE_NAME)
.map(Cookie::getValue)
.orElse("/");
targetUrl = UriComponentsBuilder.fromUriString(targetUrl)
.queryParam("error", exception.getLocalizedMessage())
.build().toUriString();
authorizationRequestRepository.removeAuthorizationRequestCookies(request, response);
getRedirectStrategy().sendRedirect(request, response, targetUrl);
}
}
Cookie-Based Authorization Request Repository
@Component
public class HttpCookieOAuth2AuthorizationRequestRepository
implements AuthorizationRequestRepository<OAuth2AuthorizationRequest> {
public static final String OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME = "oauth2_auth_request";
public static final String REDIRECT_URI_PARAM_COOKIE_NAME = "redirect_uri";
private static final int COOKIE_EXPIRE_SECONDS = 180;
@Override
public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) {
return CookieUtils.getCookie(request, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME)
.map(cookie -> CookieUtils.deserialize(cookie, OAuth2AuthorizationRequest.class))
.orElse(null);
}
@Override
public void saveAuthorizationRequest(
OAuth2AuthorizationRequest authorizationRequest,
HttpServletRequest request,
HttpServletResponse response) {
if (authorizationRequest == null) {
removeAuthorizationRequestCookies(request, response);
return;
}
CookieUtils.addCookie(response,
OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME,
CookieUtils.serialize(authorizationRequest),
COOKIE_EXPIRE_SECONDS);
String redirectUriAfterLogin = request.getParameter(REDIRECT_URI_PARAM_COOKIE_NAME);
if (StringUtils.isNotBlank(redirectUriAfterLogin)) {
CookieUtils.addCookie(response,
REDIRECT_URI_PARAM_COOKIE_NAME,
redirectUriAfterLogin,
COOKIE_EXPIRE_SECONDS);
}
}
@Override
public OAuth2AuthorizationRequest removeAuthorizationRequest(
HttpServletRequest request,
HttpServletResponse response) {
return this.loadAuthorizationRequest(request);
}
public void removeAuthorizationRequestCookies(
HttpServletRequest request,
HttpServletResponse response) {
CookieUtils.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME);
CookieUtils.deleteCookie(request, response, REDIRECT_URI_PARAM_COOKIE_NAME);
}
}
Complete Security Configuration
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final CustomOAuth2UserService customOAuth2UserService;
private final OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler;
private final OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler;
private final HttpCookieOAuth2AuthorizationRequestRepository cookieAuthorizationRequestRepository;
private final TokenAuthenticationFilter tokenAuthenticationFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.formLogin(form -> form.disable())
.httpBasic(basic -> basic.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/error", "/favicon.ico").permitAll()
.requestMatchers("/auth/**", "/oauth2/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.authorizationEndpoint(authorization -> authorization
.baseUri("/oauth2/authorize")
.authorizationRequestRepository(cookieAuthorizationRequestRepository)
)
.redirectionEndpoint(redirection -> redirection
.baseUri("/login/oauth2/code/*")
)
.userInfoEndpoint(userInfo -> userInfo
.userService(customOAuth2UserService)
)
.successHandler(oAuth2AuthenticationSuccessHandler)
.failureHandler(oAuth2AuthenticationFailureHandler)
)
.exceptionHandling(ex -> ex
.authenticationEntryPoint(new RestAuthenticationEntryPoint())
);
http.addFilterBefore(tokenAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of("http://localhost:3000"));
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(List.of("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
Frontend Integration
React Login Button
const GOOGLE_AUTH_URL = 'http://localhost:8080/oauth2/authorize/google?redirect_uri=http://localhost:3000/oauth2/redirect';
const GITHUB_AUTH_URL = 'http://localhost:8080/oauth2/authorize/github?redirect_uri=http://localhost:3000/oauth2/redirect';
function SocialLogin() {
return (
<div className="social-login">
<a href={GOOGLE_AUTH_URL} className="btn btn-google">
Sign in with Google
</a>
<a href={GITHUB_AUTH_URL} className="btn btn-github">
Sign in with GitHub
</a>
</div>
);
}
OAuth2 Redirect Handler
function OAuth2RedirectHandler() {
const location = useLocation();
const navigate = useNavigate();
useEffect(() => {
const params = new URLSearchParams(location.search);
const token = params.get('token');
const error = params.get('error');
if (token) {
localStorage.setItem('accessToken', token);
navigate('/dashboard');
} else if (error) {
navigate('/login', { state: { error } });
}
}, [location, navigate]);
return <div>Processing login...</div>;
}
Testing OAuth2
@SpringBootTest
@AutoConfigureMockMvc
class OAuth2LoginTest {
@Autowired
private MockMvc mockMvc;
@Test
void shouldRedirectToGoogleForLogin() throws Exception {
mockMvc.perform(get("/oauth2/authorize/google"))
.andExpect(status().is3xxRedirection())
.andExpect(header().string("Location",
containsString("accounts.google.com")));
}
@Test
@WithMockUser
void shouldAccessProtectedResourceWithAuthentication() throws Exception {
mockMvc.perform(get("/api/user/me"))
.andExpect(status().isOk());
}
}
Summary
| Component | Purpose |
|---|---|
OAuth2UserService | Process OAuth2 user data |
OAuth2UserInfo | Abstract user info from providers |
TokenProvider | Generate JWT after OAuth2 login |
SuccessHandler | Redirect with JWT token |
FailureHandler | Handle OAuth2 errors |
| Cookie Repository | Store OAuth2 state |
OAuth2 provides secure, user-friendly authentication. In the next article, we’ll cover method-level security for fine-grained access control.
Series Navigation
- Spring Security Architecture
- Database Authentication
- OAuth2 & Social Login (This Article)
- Method-Level Security
- CSRF, CORS & Security Headers
- Production Security Best Practices
Advertisement
Moshiour Rahman
Software Architect & AI Engineer
Enterprise software architect with deep expertise in financial systems, distributed architecture, and AI-powered applications. Building large-scale systems at Fortune 500 companies. Specializing in LLM orchestration, multi-agent systems, and cloud-native solutions. I share battle-tested patterns from real enterprise projects.
Related Articles
Spring Security Method-Level Authorization: Complete @PreAuthorize Guide
Master method-level security in Spring Boot. Learn @PreAuthorize, @PostAuthorize, SpEL expressions, custom permissions, and domain object security.
Spring BootSpring Boot JWT Authentication: Complete Security Guide
Implement JWT authentication in Spring Boot 3. Learn Spring Security 6, token generation, validation, refresh tokens, and role-based access control.
Spring BootSpring Security CSRF, CORS & Headers: Complete Protection Guide
Master CSRF protection, CORS configuration, and security headers in Spring Boot. Learn when to disable CSRF, configure CORS properly, and add security headers.
Comments
Comments are powered by GitHub Discussions.
Configure Giscus at giscus.app to enable comments.