In this post we will see how we can implement OAuth2 authentication in a Java Spring Boot application.
We will create a working website that can allow a user to sign in using Github authentication.
If you just want to see the code, you can view the full example on Github
How OAuth2 Works
Let’s take a brief look at the OAuth protocol before we jump into implementation.
If you’ve ever seen a dialog like this, then you’ve probably used OAuth before:
Here, we are trying to login to Gitlab using Github to authenticate.
There are three parties in any OAuth mechanism:
- The client - The person, or user who is trying to log in
- The consumer - The application that the client wants to log into (which is Gitlab in this example)
- The service provider - The external application that authenticates the users identity. (which is Github in this example). OAuth service providers normally have a portal in which you can register your consumer. On registration, you will receive a client ID (which we are using here as
myclientid123
), and a client secret (which we will use later on). For Github, the portal to register new applications can be found on https://github.com/settings/applications/new.
In this post, we’ll create an HTTP server (consumer) using the Spring Boot framework, that uses Github’s OAuth2 API (service provider) to authenticate the user (client).
Let’s look at an overview of how this would work in practice.
Creating the Spring Boot Application
Now that we’ve seen an overview of the OAuth2 mechanism, let’s implement it in a new Spring Boot server.
First, we need to add our dependencies to our pom.xml
file:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
You can see the full
pom.xml
file on Github
Creating the HTTP Server
Let’s start by initializing the HTTP server with Spring boot:
// Declare the main application class. We will define our HTTP routes here
@SpringBootApplication
@RestController
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
// We will implement this later
@RequestMapping("/welcome")
public String simpleRequest(HttpServletResponse response) {
return ""
}
}
Currently, we have a single /welcome
route - ideally, we want to send a personalized message to the logged in user, like "welcome <username>"
.
Let’s see what we need to do to achieve this:
- The user needs to authenticate via Github.
- We have to redirect the user to the
/welcome
page after successful authentication. - We should retrieve information about the user (like their username) from Github and display a welcome message.
We can configure our application to perform all these steps using the Spring security library.
Setting the Security Configuration
Let’s create a new class - SecurityConfiguration.java
, that will define the code configuration for our OAuth2 application:
/**
* Defines the spring security configuration for our application via the `getSecurityFilterChain`
* method
*/
@Configuration
@EnableWebSecurity
class SecurityConfiguration {
/**
* @param httpSecurity is injected by spring security
*/
@Bean
@Autowired
SecurityFilterChain getSecurityFilterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// Disable CSRF (not required for this demo)
.csrf().disable();
// authorize all requests coming through, and ensure that they are
// authenticated
httpSecurity.authorizeHttpRequests().anyRequest().authenticated();
// enable oauth2 login, and forward successful logins to the `/welcome` route
// Here `true` ensures that the user is forwarded to `/welcome` irrespective of
// the original route that they entered through our application
httpSecurity.oauth2Login()
.defaultSuccessUrl("/welcome", true);
return httpSecurity.build();
}
// enabling oauth2 login will not work without defining a
// `ClientRegistrationRepository` bean. FOr simplicity, we're defining it in the same class
@Bean
public ClientRegistrationRepository clientRegistrationRepository() {
// Create a new client registration
return new InMemoryClientRegistrationRepository(
// spring security provides us pre-populated builders for popular
// oauth2 providers, like Github, Google, or Facebook.
// The `CommonOAuth2Provider` contains the definitions for these
CommonOAuth2Provider.GITHUB.getBuilder("github")
// In this case, most of the common configuration, like the token and user endpoints
// are pre-populated. We only need to supply our client ID and secret
.clientId("a6f4d49ed380c40aa0")
.clientSecret("0a3a74b5d6b55aba18fb618693d371c86c07")
// ^ these credentials are fake, so you'll have to use your own :)
.build());
}
}
Let’s summarize what we just did:
- The
getSecurityFilterChain
method defines the core Spring security configuration, and configures our application to enable OAuth2 login. If we opt for OAuth2 login, we need to define aClientRegistrationRepository
bean, otherwise we would get a runtime error. - The
clientRegistrationRepository
method exposes aClientRegistrationRepository
instance as a bean, which is used by spring security to get information about the OAuth2 provider.
Using a Custom OAuth2 Provider
In the above example, we used the CommonOAuth2Provider
class, which gave us a pre-populated client registration for Github OAuth2 login, among others.
If we want to use a provider other than the pre-populated options, we can define our own client registration as well.
For example, we can define the OAuth2 configuration for our Github client using our own ClientRegistration
builder:
@Bean
public ClientRegistrationRepository clientRegistrationRepository() {
ClientRegistration.Builder builder = ClientRegistration.withRegistrationId("github");
builder.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
builder.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE);
builder.redirectUri("{baseUrl}/{action}/oauth2/code/{registrationId}");
builder.scope("read:user");
builder.authorizationUri("https://github.com/login/oauth/authorize");
builder.tokenUri("https://github.com/login/oauth/access_token");
builder.userInfoUri("https://api.github.com/user");
builder.userNameAttributeName("id");
builder.clientName("GitHub");
builder.clientId("a6f4d49ed380c40aa0");
builder.clientSecret("0a3a74b5d6b55aba18fb618693d371c86c07");
// Create a new client registration
return new InMemoryClientRegistrationRepository(
builder.build());
}
The result is the same as using the CommonOAuth2Provider.GITHUB
instance in this case, but it’s useful if your OAuth2 provider is not covered in the predefined settings.
Retrieving User Information
Once the user is logged in, their information is stored in the SecurityContext
of the current execution thread, which we can get by calling the SecurityContextHolder.getContext()
method.
Let’s define the /welcome
route in our Application
class to fetch and display the information of the authenticated user:
@SpringBootApplication
@RestController
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
// All requests to '/welcome' will be handled by this method
@RequestMapping("/welcome")
public String simpleRequest(HttpServletResponse response) {
// Get the username of the currently logged in user
// this method is defined below
Optional<String> username = getUsernameFromSecurityContext();
if (username.isEmpty()) {
// if user information cannot be obtained, return
// a 403 status
response.setStatus(HttpStatus.FORBIDDEN.value());
return "error";
}
// show the welcome messages with the users Github username
return "welcome " + username.get();
}
private Optional<String> getUsernameFromSecurityContext() {
// Get the security context for this request thread
// and get the principal object from the context
SecurityContext context = SecurityContextHolder.getContext();
Object principal = context.getAuthentication().getPrincipal();
// If the user is authenticated, the principal should be an
// instance of DefaultOAuth2User
if (!(principal instanceof DefaultOAuth2User)) {
// if not, return an empty value
return Optional.empty();
}
// Get the username from the DefaultOAuth2User and return
// the welcome message
String username =
((DefaultOAuth2User) principal).getAttributes().get("login").toString();
return Optional.of(username);
}
}
Running Our Application
We can run our application with Maven:
mvn -DMAIN_CLASS=com.sohamkamani.oauth2.Application clean compile package && java -jar ./target/spring-security-examples-0.0.1-SNAPSHOT.jar
Once our application has started, we can navigate to http://localhost:8080/ and go through the authentication flow:
You can view the complete example code, along with instructions on how to run the application on Github
Additional Security Measures
Although this post demonstrated the basics of OAuth2, there is a lot more that can be done to further secure your application.
Session Tokens
In this example, we passed the access token to the client so that it can make requests as the authorized user. To make your app more secure, the access token should not be passed directly to the user. Instead, create a session token that is sent to the user as a cookie.
The app will maintain a mapping of the session tokens to access tokens on the server side (most likely a database).
Instead of making requests to github, the user will make requests to the node server (with the session token), which will in turn use the provided session token to look up the access token and make the request to github on the server side.
I have written more about sessions and cookies here.
OAuth Query State
While sending the user to the authorization URL, there is a provision to provide a value for a query parameter called state
. The value of this should be a random un-guessable string provided by the application.
When github calls the redirect url, it will attach this state variable to the request params. The new URL would now look like:
https://github.com/login/oauth/authorize?client_id=myclientid123&redirect_uri=http://localhost:8080/oauth/redirect&state=somerandomstring
The application can now compare this value with the value it originally generated. If they are not the same, that means the request came from some third party, and should be rejected.
This helps protect our application against cross site request forgery attacks.