Course topics

By WebNest Studio

Spring Boot Tutorial

OAuth2 Login with Google and GitHub

"Sign in with Google" and "Sign in with GitHub" buttons are now expected on almost every consumer site. They remove password handling from your application entirely: the provider authenticates the user, and your app receives a verified identity.

Spring Security's OAuth2 Client support implements the full Authorization Code flow (with PKCE) and OpenID Connect for you. In this lesson you will register apps with Google and GitHub, configure Spring Boot with a few properties, read the logged-in user's details, link social accounts to your own user table, and call provider APIs on the user's behalf.

How the Authorization Code Flow Works

When the user clicks "Login with Google", the browser is redirected to Google with your client id, the requested scopes, a random state value and a PKCE challenge. The user signs in at Google and approves access. Google redirects back to /login/oauth2/code/google with a short-lived authorization code. Spring Security exchanges that code (plus the PKCE verifier and your client secret) for tokens in a server-to-server call, validates the ID token, loads the user's profile, and creates an authenticated session. Your code never sees the user's Google password.

OAuth2 vs OpenID Connect

OAuth2 is an authorization protocol: it gives your app an access token to call APIs. OpenID Connect (OIDC) adds authentication on top: a signed ID token that states who the user is. Google supports OIDC, so you get an OidcUser with verified email and name. GitHub supports plain OAuth2, so Spring calls GitHub's user-info API and gives you an OAuth2User with GitHub's attributes.

Registering Your Application with Providers

Each provider needs a client id and secret. The redirect URI you register must exactly match Spring's default pattern {baseUrl}/login/oauth2/code/{registrationId}.

  • Google: Google Cloud Console → APIs & Services → Credentials → OAuth client ID → Web application. Redirect URI: http://localhost:8080/login/oauth2/code/google.
  • GitHub: Settings → Developer settings → OAuth Apps → New OAuth App. Callback URL: http://localhost:8080/login/oauth2/code/github.
  • Store secrets in environment variables or a secret manager, and register separate apps for development and production.

Linking Social Logins to Your Own Users

Most apps still need their own user record for orders, preferences and roles. Implement a custom OAuth2UserService (or OidcUserService for Google) that delegates to the default service, then finds or creates a local user by provider and provider user id, and returns a principal carrying your application's roles. Match on the provider's stable user id (sub for Google, id for GitHub), not on email alone, because emails can change and not every provider verifies them.

Calling Provider APIs with the Access Token

The tokens returned by the provider are stored in an OAuth2AuthorizedClient. Inject it into a controller with @RegisteredOAuth2AuthorizedClient("github"), or configure a RestClient with OAuth2ClientHttpRequestInterceptor so that the access token is attached (and refreshed) automatically.

Examples

Dependencies and provider configuration

Java
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security-oauth2-client</artifactId>
</dependency>

# application.yml — Google and GitHub are pre-defined providers,
# so only the client id and secret are required
spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: ${GOOGLE_CLIENT_ID}
            client-secret: ${GOOGLE_CLIENT_SECRET}
            scope: openid, profile, email
          github:
            client-id: ${GITHUB_CLIENT_ID}
            client-secret: ${GITHUB_CLIENT_SECRET}
            scope: read:user, user:email
Output
(Visiting http://localhost:8080/login now shows a generated page with "Google" and "GitHub" links.)

Security configuration with a custom login page

Java
@Configuration
public class OAuthLoginConfig {

    @Bean
    SecurityFilterChain web(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/", "/login", "/css/**", "/error").permitAll()
                .anyRequest().authenticated())
            .oauth2Login(oauth -> oauth
                .loginPage("/login")
                .defaultSuccessUrl("/dashboard", true))
            .logout(logout -> logout.logoutSuccessUrl("/"));
        return http.build();
    }
}

<!-- templates/login.html (Thymeleaf) -->
<a href="/oauth2/authorization/google">Continue with Google</a>
<a href="/oauth2/authorization/github">Continue with GitHub</a>
Output
GET /dashboard (not logged in) -> 302 /login
Click "Continue with GitHub"  -> 302 https://github.com/login/oauth/authorize?response_type=code&client_id=...&scope=read:user%20user:email&state=...
GitHub redirects back          -> /login/oauth2/code/github?code=...&state=... -> 302 /dashboard

Reading the logged-in user

Java
@Controller
public class DashboardController {

    @GetMapping("/dashboard")
    public String dashboard(@AuthenticationPrincipal OAuth2User user, Model model) {
        // Google (OIDC) exposes "name" and "email"; GitHub exposes "login", "name", "avatar_url"
        model.addAttribute("name", user.getAttribute("name"));
        model.addAttribute("avatar", user.getAttribute("avatar_url"));
        return "dashboard";
    }

    @GetMapping("/api/me")
    @ResponseBody
    public Map<String, Object> me(OAuth2AuthenticationToken auth) {
        return Map.of(
            "provider", auth.getAuthorizedClientRegistrationId(),
            "name", auth.getName(),
            "attributes", auth.getPrincipal().getAttributes().keySet());
    }
}
Output
GET /api/me (after GitHub login)
{"provider":"github","name":"1234567","attributes":["login","id","avatar_url","name","email", ...]}

Linking the social account to a local user with application roles

Java
@Service
public class LinkingOAuth2UserService extends DefaultOAuth2UserService {

    private final AppUserRepository users;

    public LinkingOAuth2UserService(AppUserRepository users) {
        this.users = users;
    }

    @Override
    @Transactional
    public OAuth2User loadUser(OAuth2UserRequest request) {
        OAuth2User remote = super.loadUser(request);
        String provider = request.getClientRegistration().getRegistrationId();
        String providerId = String.valueOf(remote.getAttributes().get("id"));

        AppUser local = users.findByProviderAndProviderId(provider, providerId)
            .orElseGet(() -> users.save(AppUser.fromSocial(provider, providerId,
                remote.getAttribute("email"), remote.getAttribute("name"))));

        Set<GrantedAuthority> authorities = local.getRoles().stream()
            .map(r -> new SimpleGrantedAuthority("ROLE_" + r))
            .collect(Collectors.toSet());

        return new DefaultOAuth2User(authorities, remote.getAttributes(), "id");
    }
}

// register it
.oauth2Login(oauth -> oauth.userInfoEndpoint(ui -> ui.userService(linkingOAuth2UserService)))
Output
First GitHub login  -> INSERT INTO app_users (provider, provider_id, email, name, ...) VALUES ('github', '1234567', ...)
Second GitHub login -> existing user found, authorities=[ROLE_USER]

Calling the GitHub API on behalf of the user

Java
@GetMapping("/api/github/repos")
@ResponseBody
public String repos(@RegisteredOAuth2AuthorizedClient("github") OAuth2AuthorizedClient client) {
    return RestClient.create("https://api.github.com")
        .get()
        .uri("/user/repos?per_page=5")
        .headers(h -> h.setBearerAuth(client.getAccessToken().getTokenValue()))
        .retrieve()
        .body(String.class);
}
Output
[{"name":"spring-shop","private":false,...},{"name":"notes","private":true,...}]

Common Mistakes

  • Registering a redirect URI that does not exactly match /login/oauth2/code/{registrationId}, causing a redirect_uri_mismatch error at the provider.
  • Identifying users by email only; use the provider name plus the provider's stable user id.
  • Committing client secrets to source control instead of using environment variables or a secret manager.
  • Assuming every provider returns an email — GitHub hides it unless the user:email scope is granted and the email is public or fetched from /user/emails.
  • Using OAuth2 login for a pure JSON API consumed by mobile apps; APIs should be resource servers that accept tokens, while the login flow runs in the client or a backend-for-frontend.

Key Points to Remember

  • spring-boot-starter-security-oauth2-client plus client id/secret properties gives you a complete Authorization Code login flow.
  • Google uses OpenID Connect (OidcUser); GitHub uses plain OAuth2 (OAuth2User from the user-info endpoint).
  • Login links are /oauth2/authorization/{registrationId}; callbacks go to /login/oauth2/code/{registrationId}.
  • Link social identities to local users in a custom OAuth2UserService to attach your own roles.
  • Use OAuth2AuthorizedClient to call provider APIs with the user's access token.

Practice the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.

Use your local JDK or project IDE for these examples. Codelab currently runs Python and HTML/CSS/JavaScript; framework examples may need project dependencies.