Guide to Social Login using OAuth 2.0 in Java Spring Boot

Jeff posted on  (updated on )

This article is intended for developers who wish to add "Log in with Google (or any other website) account" button to their website.

One of the popular ways to do Social Login is using OAuth 2.0 and OpenID Connect standard. This article will focus on how to leverage them in a Java Spring Boot project, but the concept applies to any tech stack.

Reddit

You want Social Login? Take a detour first...

I know you want to jump right into downloading an OAuth 2.0 library and add Social Login button to your website and call it a day. I have been there, later I had to spend the time learning how OAuth 2.0 works to properly make any change to my website's sign in flow. So in this article, I will encourage you to take a detour and understand how they work first.

Preparation may quicken the process.

Now, to add Social Login, we need OpenID Connect, which is built on top of OAuth 2.0, so let's talk about OAuth 2.0.

The concepts

What is OAuth 2.0

OAuth 2.0 is a framework that define procedures around authorization with third-party applications. It's latest version (as of 2024 Feb 16th) is 2.0, 2.1 is on the horizon but don't worry about as it doesn't change what we are covering here.

Two things worth pointing out about OAuth 2.0:

  • It's not a concrete implementation, it's like a blueprint, so you will have to use community-provided libraries that implement this framework (Java Spring OAuth2, Javascript Passport OAuth 2, to name a few).
  • It's only for AUTHORIZATION, it cannot do AUTHENTICATION. OpenID Connect fills the gap with authentication capability.

Authorization vs Authentication

In area outside security, people generally use these two terms interchangeably, but since we are dealing with sensitive information here, it's best to understand the difference and use them correctly.

  1. Authentication: This is the process of verifying the identity of a user, device, or system. It often involves a username and password, but can involve other methods such as biometric data. Essentially, authentication is the answer to the question, "Are you who you say you are?"
  2. Authorization: Once a user, device, or system has been authenticated, the next step is to determine what they are allowed to do. This process is called authorization. For instance, a user might be allowed to read and write to a particular directory, but not to another. Authorization answers the question, "What are you allowed to do?"

Using real life examples:

  1. Authentication: In a airport, you show your passport to prove who you are.
  2. Authorization: In a movie theater, you show your movie ticket to prove you have authorization to enter, but the ticket doesn't say who you are.

Now you know why we need OAuth 2.0 + OpenID Connect to make Social Login.

Roles in OAuth 2.0

To perform operations like signing in/authorizing, we need actors/entities. These are called roles in the OAuth 2.0 spec[1]. These are the core concepts and will be mentioned again and again, definitely worth remembering.

1. resource owner

An entity capable of granting access to a protected resource. When the resource owner is a person, it is referred to as an end-user.

2. resource server

The server hosting the protected resources, capable of accepting and responding to protected resource requests using access tokens.

3. client

An application making protected resource requests on behalf of the resource owner and with its authorization. The term "client" does not imply any particular implementation characteristics (e.g., whether the application executes on a server, a desktop, or other devices).

4. authorization server

The server issuing access tokens to the client after successfully authenticating the resource owner and obtaining authorization.

These are abstract words, let see a real world example of using OAuth 2.0 and what role each entity plays.

Example of OAuth 2.0

Since OAuth 2.0 is about authorization, we can't use an example of social login just yet. I failed to find a real world example, so let's imagine there is this scenario:

You are on OneDrive.com, you want to import some files from your Google Drive, how could OAuth 2.0 help make this happen?

Here's the possible flow from user's perspective:

  1. On OneDrive.com, user clicks "Import from Google Drive"
  2. User is directed to GoogleDrive.com and asked to log in with their Google account
  3. After login, user is asked to confirm if they want to share Google Drive access with OneDrive.com
  4. After confirming, user is directed back to OneDrive.com, and find OneDrive.com can access their Google Drive files, so they can choose what files to import.

In this example, each entity plays the following role from OAuth 2.0:

Roles OneDrive

  • You are the Resource Owner because you have the say whether to grant OneDrive the access to your Google Drive.
  • OneDrive is the Client because they make request - on behalf of you - to Google Drive, to retrieve your Google Drive content.
  • Google Drive is Authorization Server because they authenticate you, then issue access token, which authorizes 3rd party to access your content.
  • Google Drive is also Resource Server because they serve your Google Drive content, and validate incoming request by checking access token.

In this example, OAuth 2.0 helps authorize OneDrive to access your content on Google Drive, yet your Google account & password is never shared with OneDrive, increasing security.

Protocol in OAuth 2.0

With roles defined, we can abstract the steps involved in above flow, called Protocol Flow in OAuth 2.0.

Protocol

Again, these are abstract words. let's replace it with "real world" example above.

Protocol real

Authorization Grant

You may notice the step called "Authorization Grant" from above picture. It's a concept from OAuth 2.0: An authorization grant is a credential representing the resource owner's authorization (to access its protected resources) used by the client to obtain an access token.

There are a few different Authorization Grant defined in OAuth 2.0, the most popular one is called Authorization Code, which is suitable for any application with a backend (i.e. has a server continuously running somewhere, the point is average customer won't have access to it so developer could config secrets that are required by this type of Authorization Grant).

In this article we will only talk about this particular type of Authorization Grant: Authorization Code. All above examples are using this type.

This concludes all you need to know about OAuth 2.0. We will look at the implementation details later when building the demo project.

What is OpenID Connect

Now we know OAuth 2.0 can only do authorization, although it's pretty cool to share content from one website to another without sharing your password, how do we do Social Login?

Well, if the content we share is the account profile, then we have Social Login. OpenID Connect[2] adds a simple identity layer on top of the OAuth 2.0 protocol, and allows client to retrieve resource owner's info from authorization server, thus authenticating resource owner.

What's the addition to OAuth 2.0?

Now alone with access token, OpenID Connect will let Authorization Server returns an ID Token, Client could use this ID Token to retrieve Resource Owner's info. See below, OpenID Connect related changed has been marked.

OIDC

Real world example

Since this is a popular way to achieve Social Login, you can find examples from literally anywhere. Reddit for example:

Reddit

Can you identify the roles of each entity in such login flow? Can you identify the interactions between entities?

Show me the code

Now, armed with knowledge about OpenID Connect + OAuth 2.0, you are ready to develop a real world application with Social Login. In the following sections, I will guide you to build a full stack application using Java Spring Boot, and add Social Login to it.

Setup project

You will need Java SDK installed. Do that, then let's create a Java Spring Boot application by

  1. Open https://start.spring.io/
  2. Select "Maven" under project (Gradle is fine but I prefer Maven personally)
  3. Select "Java" under language
  4. Select latest Spring Boot version (mine is 3.2.2 at the time of writing)
  5. Give the project a name you like
  6. Leave default value for Packaging and Java version

On the right hand side panel, add following dependencies

  1. Spring Web
  2. Spring Security
  3. OAuth2 Client
  4. Thymeleaf

The final result should look like this

Init Java Spring Boot

Click "Generate", and unzip the into a folder. Open the project in a editor of your flavor. I am using IntelliJ IDEA 'cause I prefer to clock out on time.

You could try start the application with following command

# Build
./mvnw compile

# Run
./mvnw spring-boot:run

Now open your browser and visit http://localhost:8080/login . You should see a form asking for username and password. This indicates the application is running as intended.

Config Java Spring Security

Given our dependency, by default, the project will support Form Login (which generates the form you see at http://localhost:8080/login) and Basic Login. Since we will only use Social Login, let's disable these two.

Create a file called SecurityConfig.java under src/main/java/com/example/oauthdemo/config/

Copy paste following content:

package com.example.oauthdemo.config;  
  
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.config.annotation.web.configuration.EnableWebSecurity;  
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;  
import org.springframework.security.web.SecurityFilterChain;  
  
@EnableWebSecurity  
@Configuration  
public class SecurityConfig {  
  
    @Bean  
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {  
        // Disable form login  
        http.formLogin(AbstractHttpConfigurer::disable);  
        // Disable basic login  
        http.httpBasic(AbstractHttpConfigurer::disable);  
  
        return http.build();  
    }  
}

Now compile and run, you should see /login page is no longer available.

Now let's enable OAuth 2.0 + OpenID Connect. Because the spring-boot-starter-oauth2-client comes with support for OpenID Connect, we only need to enable a single package.

// Disable form login  
http.formLogin(AbstractHttpConfigurer::disable);  
// Disable basic login  
http.httpBasic(AbstractHttpConfigurer::disable);  
  
// NEW: Enable OAuth 2.0 + OpenID Connect
http.oauth2Login(Customizer.withDefaults());
  
return http.build();

If you are curious about the complete configuration options for http.oauth2Login, check OAuth2LoginConfigurer.java

Now if you run the application again, you would see it fails to run and complains about

... WebSecurityConfiguration required a bean of type 'org.springframework.security.oauth2.client.registration.ClientRegistrationRepository'

This config makes our application a Client (OAuth 2.0 role, remember?), and it fails to run because it can't find any information about the client, which we will provide next. This is actually a two way communication, we need to make Client and Authorization Server know about each other.

Register Client

You want user of your website to see a login page something like this:

Auth0

Now this is the part where we choose what social site of the Social Login we want to support. It could be Google/GitHub/Microsoft/Facebook/... you name it. Here I would recommend https://auth0.com/ , this is a service that consolidates all kinds of social login, if you register with them, you can easily add whatever social site you want.

Auth0 Provider

In the following section I will be using Auth0 as the social login provider.

Register Client (Your App actually) with Auth0

  1. Click "Create Application" under side panel "Applications"
  2. Select "Regular Web Applications"
  3. Give it a name
  4. Done

Now head over to the setting tab, notice the Client ID and Client Secret value, this will be the username and password for your Client.

New App

Now head over to "Application URIs" section on this setting page

  1. add http://localhost:8080/login/oauth2/code/auth0 to "Allowed Callback URLs"
  2. add http://localhost:8080/ to "Allow Logout URLs"

Will explain later what are these.

This finishes registering your app with Auth0.

Config Authorization Server

Now let's give Auth0's info to our application, open our application's property file src/main/resources/application.yml

This file can be in many format, I find yml format easier to read. So if your project is using application.properties, rename it to application.yml

Copy paste below configuration, remember to replace client-id, client-secret and issuer-uri (Domain) with value from your Auth0 app.

spring:  
  security:  
    oauth2:  
      client:  
        registration:  
          auth0:  
            client-id: <Your Auth0 App's client-id> 
            client-secret: <Your Auth0 App's client-secret>
            authorization-grant-type: authorization_code  
            scope: openid,profile,email  
        provider:  
          auth0:  
            issuer-uri: <Your Auth0 App's Domain>

What we are doing here?

  1. We register a client with Java Spring Security called auth0
    1. This client will use Authorization Code type
    2. This client will request access to openid, user profile and email
  2. We register a provider (think it as Authorization Server) also called auth0
    1. We can reach this provider at issuer-uri

If you are curious about the complete property options, check Source Code: spring security config

Now start the app and go to http://localhost:8080/login, you should see a link to your Auth0 App, click it and you will see

Auth0

After logging in with Auth0, you will be redirected back to your app, but since nothing is here we see an error. Don't worry, your Social Login is almost complete now!

Add home page to display user profile

Let's create a home page to print some info about logged in user.

Create src/com/example/oauthdemo/controller/HomeController.java With following content

package com.example.oauthdemo.controller;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.Map;

@Controller
public class HomeController {

    private final static ObjectMapper mapper = new ObjectMapper().registerModule(new JavaTimeModule());

    @GetMapping("/")
    public String home(final Model model, @AuthenticationPrincipal final OidcUser oidcUser) {
        if (oidcUser != null) {
            model.addAttribute("profile", oidcUser.getClaims());
            model.addAttribute("profileJson", claimsToJson(oidcUser.getClaims()));
        }
        return "index";
    }

    private String claimsToJson(final Map<String, Object> claims) {
        try {
            return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(claims);
        } catch (JsonProcessingException ignored) {

        }
        return "Error parsing claims to JSON.";
    }
}

This controller will handle GET request to / route, and render a index view. Let's create this index view.

Create src/main/resources/templates/index.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <title>OAuth Demo</title>
</head>
<body>
<div th:if="${profile}">
    <div>
        <h1>You are logged in</h1>
        <a href="/logout">Log out</a>
    </div>

    <img th:src="${profile.get('picture')}"/>
    <h2>Name: <span th:text="${profile.get('name')}"/></h2>
    <p>Email: <span th:text="${profile.get('email')}"/></p>
    <pre><code th:text="${profileJson}"></code></pre>
</div>
<div th:if="!${profile}">
    <h1>You are NOT logged in.</h1>
    <a href="/login">Login</a>
</div>
</body>
</html>

Launch your app, you should see user profile printed out.

Add logout

Now you might notice that, if you log out then log in, you do not see the Auth0 page. This is because even though we log user out of our website, they are still logged in with Auth0. This may not be the desired behavior, so let's log user out of Auth0 as well when logging out.

Open src/main/java/com/example/oauthdemo/config/SecurityConfig.java

package com.example.oauthdemo.config;  
  
import org.springframework.beans.factory.annotation.Value;  
import org.springframework.context.annotation.Bean;  
import org.springframework.context.annotation.Configuration;  
import org.springframework.security.config.Customizer;  
import org.springframework.security.config.annotation.web.builders.HttpSecurity;  
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;  
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;  
import org.springframework.security.web.SecurityFilterChain;  
import org.springframework.security.web.authentication.logout.LogoutHandler;  
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;  
  
import java.io.IOException;  
  
@EnableWebSecurity  
@Configuration  
public class SecurityConfig {  
  
    @Value("${spring.security.oauth2.client.provider.auth0.issuer-uri}")  
    private String issuer;  
    @Value("${spring.security.oauth2.client.registration.auth0.client-id}")  
    private String clientId;  
  
    @Bean  
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {  
        // Disable form login  
        http.formLogin(AbstractHttpConfigurer::disable);  
        // Disable basic login  
        http.httpBasic(AbstractHttpConfigurer::disable);  
  
        // Enable OAuth Client  
        http.oauth2Login(Customizer.withDefaults());  
  
        // Add a handler to also logout Auth0 session  
        http.logout(logout -> logout.addLogoutHandler(logoutHandler()));  
  
        return http.build();  
    }  
  
    private LogoutHandler logoutHandler() {  
        return (request, response, authentication) -> {  
            try {  
                final String baseUrl = ServletUriComponentsBuilder.fromCurrentContextPath().build().toUriString();  
                response.sendRedirect(issuer + "v2/logout?client_id=" + clientId + "&returnTo=" + baseUrl);  
            } catch (IOException e) {  
                throw new RuntimeException(e);  
            }  
        };  
    }  
}

That's it, you have Social Login now.

Under the hood...

Let's follow the flow of network requests for Social Login on our newly built application, see if we can explain each request given the OAuth 2.0 protocol.

  1. Open up the Web Developer Tools of your browser (I am using Firefox), go to Network tab, enable "Persist Logs" so logs won't be cleared even between HTTP 302 redirects.
  2. head over to http://localhost:8080/login
  3. Clear existing logs on your Web Developer Tools's Network tab
  4. Click log in
  5. Log in with Auth0 by using username and password (so you don't get confused with another OAuth 2.0 flow)
    1. You can create user on Auth0 to test this
  6. Be redirected back to your application's home page http://localhost

Here's network requests I have captured (remember to filter only HTML requests), let check them one by one

Network requests

1. 302 GET

Request

GET http://localhost:8080/oauth2/authorization/auth0

This means we want the website to initiate a Social Login request, /oauth2/authorization/auth0 endpoint is generated by Java Spring Security OAuth2 Client.

Response

This is a HTTP 302 redirect response, we can see the new location is https://dev-klasxuv1hud7bzvw.us.auth0.com/authorize?response_type=code&client_id=YN6SfE9gY2VgWDGQjarX2rtaoVt4zeyX&scope=openid%20profile%20email&state=a0yS8sthWKEubutMly4LtOJb6zMXc115szp3LO7ocoA%3D&redirect_uri=http://localhost:8080/login/oauth2/code/auth0&nonce=_CGukeWyW594ab6f_2EfcWmqRite_2uaQnD9MASbOx0

Let's break the search parameters down

  1. Base URL is https://dev-klasxuv1hud7bzvw.us.auth0.com/authorize
  2. Search parameters are
    1. response_type=code
      1. Means OAuth2 Authorization Code grant type
    2. client_id=YN6SfE9gY2VgWDGQjarX2rtaoVt4zeyX
      1. The client id we get from Auth0, as specified in application.yml
    3. scope=openid%20profile%20email
      1. Scope we want access, as specified in application.yml
    4. state=a0yS8sthWKEubutMly4LtOJb6zMXc115szp3LO7ocoA%3D
      1. Ignore for now
    5. redirect_uri=http://localhost:8080/login/oauth2/code/auth0
      1. /login/oauth2/code/auth0 is another Java Spring Security OAuth2 Client generated endpoint. Remember earlier we set this value under the "Allowed Callback URLs" on Auth0. So Auth0 will only willing to send user back to this exact URL after authentication. No info will be leaked to other websites.
    6. nonce=_CGukeWyW594ab6f_2EfcWmqRite_2uaQnD9MASbOx0
      1. Ignore for now

2. 302 GET

Request #1 sends us to Auth0 website, since we are not logged in with Auth0, this request directs us to log in with Auth0. This is not part of OAuth2, as how Authorization Server authenticates user is out of scope for OAuth2. So let's move to next request.

3. 200 GET

Request #2 directs us to log in with Auth0, this request fetches the login page on Auth0. Also not part of OAuth2.

4. 302 POST

Request

This is a POST request, as user logs in with Auth0 by posting username and password. Still not part of OAuth2.

Response

This is HTTP 302 redirect response, new location <auth0.com>/authorize/resume?state=_es11V3TadvY1MMxs25YPY0Ka6Bg5Kdl

We can see this is because we have successfully logged in with Auth0, now they are directing us to resume the initial Social Login flow with our website.

5. 302 GET

Request

We can see this request is Auth0 trying to resume our initial login flow, the response is finally OAuth2 related.

Response

Another HTTP 302 redirect request, new location is

http://localhost:8080/login/oauth2/code/auth0?code=z-fTi4lz_mPpYGgwd-J7t6-CpSqpZDyd83qY97YePIM1f&state=a0yS8sthWKEubutMly4LtOJb6zMXc115szp3LO7ocoA%3D

Let's break it down

  1. Base URL http://localhost:8080/login/oauth2/code/auth0
    1. Hey, now Auth0 has successfully authenticated user, it directs user back to the callback URL we provided
  2. Search Parameters
    1. code=z-fTi4lz_mPpYGgwd-J7t6-CpSqpZDyd83qY97YePIM1f
      1. The Authorization Code granted from Authorization Server
    2. state=a0yS8sthWKEubutMly4LtOJb6zMXc115szp3LO7ocoA%3D
      1. Notice this is the same state value we see in Request #1

6. 302 GET

Request

The request provides the code (Authorization Code) to our website's server, now the server will use this code to exchange access token, and call Authorization Server again to fetch user profile/email etc. Once we have the user profile, Social Login is completed.

Response

Social Login complete, redirect user to home page. Set cookie as well so user remains logged in with our website.

7. 200 GET

This is just fetching the home page of our website.

Recap

Let's mark the requests from the flow earlier...

OIDC Request

Reference

[1] OAuth 2 Specification https://datatracker.ietf.org/doc/html/rfc6749
[2] OpenID Connect Specification https://openid.net/specs/openid-connect-core-1_0.html
[3] Auth0's handbook on OpenID Connect https://auth0.com/resources/ebooks/the-openid-connect-handbook
[4] Auth0's JWT Handbook https://auth0.com/resources/ebooks/jwt-handbook