Java Authenticating a generic client with Spring Security OAuth2 Client

Hello everyone đź‘‹,

I recently worked on a small side project written in Java with the Spring Framework and I had difficulties authenticating to an external OAuth2 client using spring security. The solution to my problem was clear after I perused the code and documentation of the Spring OAuth Client package.

I thought that I'll have to write custom classes to configure it for my specific need but it turned out that my issues was strictly configuration related.

The issues was that I had a Confidential OAuth2 client and the default configuration of Spring Security assumes that the client is Public.

The RFC describes the client types as follows:
  • The Confidential client type is for applications that can keep the client secret safe, for example a back-end web application.
  • The Public client type is for applications that cannot keep their client secrets safe, for example a mobile applications or front end application. The user can always browse the source code and obtain the secrets.
In my Spring project, I've configured the authentication using the applications.properties file. Notice that the configuration of the client and provider is under the nuculabs key.

Code:
# OAuth2 Client Configuration
spring.security.oauth2.client.registration.nuculabs.client-id=xxx
spring.security.oauth2.client.registration.nuculabs.client-secret=xxx
spring.security.oauth2.client.registration.nuculabs.scope=user:read,read:user
spring.security.oauth2.client.registration.nuculabs.authorizationGrantType=authorization_code
spring.security.oauth2.client.registration.nuculabs.redirectUri=http://localhost:8080/login/oauth2/code/nuculabs
spring.security.oauth2.client.registration.nuculabs.client-authentication-method=client_secret_post

spring.security.oauth2.client.provider.nuculabs.authorization-uri=https://www.example.com/oauth2/authorize
spring.security.oauth2.client.provider.nuculabs.token-uri=https://www.example.com/api/oauth2/token
spring.security.oauth2.client.provider.nuculabs.user-info-uri=https://www.example.com/api/me
spring.security.oauth2.client.provider.nuculabs.user-name-attribute=me

And the configuration that I was missing was client_secret_post.

Once that was completed I only had to add the following SecurityConfig class:

Java:
package dev.nuculabs.xenchat.xenchat.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.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    // OAuth2AuthorizationCodeGrantRequestEntityConverter

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(authorize -> authorize
                        .requestMatchers("/error", "/webjars/**", "/login").permitAll()
                        .requestMatchers("/").authenticated()
                        .anyRequest().authenticated()
                )
                .oauth2Login(oauth2 -> oauth2
                        .loginPage("/login")
                        .defaultSuccessUrl("/", true)
                )
                .logout(logout -> logout
                        .logoutSuccessUrl("/login")
                        .permitAll()
                );

        return http.build();
    }
}

And when I want to authenticate users using my external OAuth client, I only have to redirect them to an internal link in order to trigger the authentication flow implemented by Spring Security:

Code:
/oauth2/authorization/nuculabs

The final step is to configure the external OAuth client with the following URL:

Code:
http://localhost:8080/login/oauth2/code/nuculabs

This will technically complete the authentication flow. What will happen next is that Spring will try to grab the user data from the user-info-uri, in my case the API is secured by the access token and it will respond with a "me" object that looks like this:

JavaScript:
{

    "me": {
    "about": "I’m Denis, a Software Engineer living in Romania. I’m passionate about cloud computing and software development ✨.",
    "activity_visible": false,
    "alert_optout": [],
    "allow_post_profile": "members",
    "allow_receive_news_feed": "members",
    "allow_send_personal_conversation": "members",
    "allow_view_identities": "members",
    "allow_view_profile": "members",
    "name": "Denis",
    ...
}

What I had to do was to specify me as the user-name attribute as follows:

Code:
spring.security.oauth2.client.provider.nuculabs.user-name-attribute=me

Then I can access all those fields from a controller like so:

Java:
package dev.nuculabs.xenchat.xenchat.controller;

import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.LinkedHashMap;

@Controller
public class HomeController {

@GetMapping("/")
public String home(@AuthenticationPrincipal OAuth2User principal, Model model) {
LinkedHashMap<String, Object> userData = principal.getAttribute("me");
        // Add user attributes to the model
        model.addAttribute("name", userData.get("username"));
        model.addAttribute("email", userData.get("email"));
        model.addAttribute("title", userData.get("custom_title"));
        model.addAttribute("profileUrl", userData.get("view_url"));
        model.addAttribute("isModerator", userData.get("is_moderator"));

        // Add all attributes for debugging
        model.addAttribute("attributes", principal.getAttributes());

return "home";
    }

@GetMapping("/login")
public String login() {
return "login";
    }
}

That's about it, I hope this article helped! :D

Thanks for reading!
 
Last edited:
Back
Top