Guide to Social Login using OAuth 2.0 in Java Spring Boot
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.
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.
- 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?"
- 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:
- Authentication: In a airport, you show your passport to prove who you are.
- 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:
- On OneDrive.com, user clicks "Import from Google Drive"
- User is directed to GoogleDrive.com and asked to log in with their Google account
- After login, user is asked to confirm if they want to share Google Drive access with OneDrive.com
- 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:
- 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.
Again, these are abstract words. let's replace it with "real world" example above.
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.
Real world example
Since this is a popular way to achieve Social Login, you can find examples from literally anywhere. Reddit for example:
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
- Open https://start.spring.io/
- Select "Maven" under project (Gradle is fine but I prefer Maven personally)
- Select "Java" under language
- Select latest Spring Boot version (mine is 3.2.2 at the time of writing)
- Give the project a name you like
- Leave default value for Packaging and Java version
On the right hand side panel, add following dependencies
- Spring Web
- Spring Security
- OAuth2 Client
- Thymeleaf
The final result should look like this
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:
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.
In the following section I will be using Auth0 as the social login provider.
Register Client (Your App actually) with Auth0
- Click "Create Application" under side panel "Applications"
- Select "Regular Web Applications"
- Give it a name
- 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.
Now head over to "Application URIs" section on this setting page
- add
http://localhost:8080/login/oauth2/code/auth0
to "Allowed Callback URLs" - 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 usingapplication.properties
, rename it toapplication.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?
- We register a client with Java Spring Security called
auth0
- This client will use Authorization Code type
- This client will request access to openid, user profile and email
- We register a provider (think it as Authorization Server) also called
auth0
- We can reach this provider at
issuer-uri
- We can reach this provider at
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
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.
- 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.
- head over to http://localhost:8080/login
- Clear existing logs on your Web Developer Tools's Network tab
- Click log in
- Log in with Auth0 by using username and password (so you don't get confused with another OAuth 2.0 flow)
- You can create user on Auth0 to test this
- 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
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
- Base URL is
https://dev-klasxuv1hud7bzvw.us.auth0.com/authorize
- Search parameters are
response_type=code
- Means OAuth2 Authorization Code grant type
client_id=YN6SfE9gY2VgWDGQjarX2rtaoVt4zeyX
- The client id we get from Auth0, as specified in
application.yml
- The client id we get from Auth0, as specified in
scope=openid%20profile%20email
- Scope we want access, as specified in
application.yml
- Scope we want access, as specified in
state=a0yS8sthWKEubutMly4LtOJb6zMXc115szp3LO7ocoA%3D
- Ignore for now
redirect_uri=http://localhost:8080/login/oauth2/code/auth0
/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.
nonce=_CGukeWyW594ab6f_2EfcWmqRite_2uaQnD9MASbOx0
- 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
- Base URL
http://localhost:8080/login/oauth2/code/auth0
- Hey, now Auth0 has successfully authenticated user, it directs user back to the callback URL we provided
- Search Parameters
code=z-fTi4lz_mPpYGgwd-J7t6-CpSqpZDyd83qY97YePIM1f
- The Authorization Code granted from Authorization Server
state=a0yS8sthWKEubutMly4LtOJb6zMXc115szp3LO7ocoA%3D
- Notice this is the same
state
value we see in Request #1
- Notice this is the same
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...
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