Hi Guys,

In modern web applications, securing APIs with OAuth 2.0 is a common practice. But before we proceed further, let’s see what is meant by OAuth.

What is OAuth?
OAuth which stands for “Open Authorization”, is a standard designed to allow a website or application to access resources hosted by other web apps on behalf of a user. It replaced OAuth 1.0 in 2012 and is now the de facto industry standard for online authorization. OAuth 2.0 provides consented access and restricts actions of what the client app can perform on resources on behalf of the user, without ever sharing the user’s credentials.

auth0.com

What is authentication?

The process of verify an identity based on attached credentials with it is called authentication.

What is authorization?

The process of granting access to a associated and protected resource to an authenticated identity is called authorization.

Now, what is Bearer Authentication & Authorization?

First we need to know what does “bearer” means. The term “Bearer” signifies that whoever possesses the authority (credentials) can use it to access the protected resources, similar to how a person who bears a physical key can use it to unlock a door. It’s important to handle bearer tokens securely, as they can provide unrestricted access to the associated resources. Now, simply replace “identity” with “bearer” in above definitions of authentication and authorization, that’s what bearer authentication & authorization means. Bearer authentication & authorization are also called token authentication & authorization, because a token is used in this process which is nothing but a long alphanumeric string – be it opaque or jwt string.

Please check other blogs on this topic as well. Many of them provide detailed explanations that can enhance your understanding.

Now, let’s jump into the code. We are going to use Spring Boot version 3.2.4

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.2.4</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>
  <groupId>com.example</groupId>
  <artifactId>bearer-token-auth</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>bearer-token-auth</name>
  <description>Demo project for Spring Boot</description>
  <properties>
    <java.version>17</java.version>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>

    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>io.projectreactor</groupId>
      <artifactId>reactor-test</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
          <excludes>
            <exclude>
              <groupId>org.projectlombok</groupId>
              <artifactId>lombok</artifactId>
            </exclude>
          </excludes>
        </configuration>
      </plugin>
    </plugins>
  </build>

</project>

application.properties

spring.application.name=bearer-token-auth
server.port=8080

For bearer authentication, we need following as part of required credentials –

  1. The endpoint
  2. Client ID
  3. Client Secret
  4. Grant Type
  5. Scope

These are the minimum requirement. For your specific requirement, you might need more details.

Create a constant class called BearerTokenAuthConstantto hold these values.

public class BearerTokenAuthConstant {

  private BearerTokenAuthConstant() {}
  
  public final static String CLIENT_ID = "client_id_value";
  
  public final static String CLIENT_SECRET = "client_secret_value";
  
  public final static String GRANT_TYPE = "grant_type_value";	
  
  public final static String SCOPE = "scope_value";
  
  public final static String OAUTH2_BASE_URL = "https://www.example.com";
  
  public final static String OAUTH2_URI = "/oauth/token";
}

We are using WebFlux so we will need WebClient.So, let create a @Configuration class which will declare WebClient bean.

import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.List;
import javax.net.ssl.SSLException;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.http.HttpHeaders;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;
import io.netty.channel.ChannelOption;
import io.netty.channel.unix.UnixChannelOption;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
import io.netty.resolver.DefaultAddressResolverGroup;
import reactor.netty.http.client.HttpClient;
import reactor.netty.resources.ConnectionProvider;

@Configuration
public class WebClientConfiguration {

  private static final String CONNECTION_PROVIDER_NAME = "customConnectionProvider";
  private static final int MAX_CONNECTIONS = 10;
  private static final int ACQUIRE_TIMEOUT = 5;
  private static final int CONNECT_TIMEOUT_MILLIS = 6000;
  private static final int READ_WRITE_TIMEOUT_SECONDS = 5;
  private static final int TIMEOUT_SECONDS = 5;
  private static final int MAX_PENDING_ACQUIRES = 5;
  
  private static final Iterable<String> allowedCiphers = List.of("TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384");
  
  @Primary
  @Bean(name = "WebClientWithTimeout")
  WebClient.Builder webClientBuilderStage() throws SSLException {
      ConnectionProvider connectionProvider = buildConnectionProvider();
      SslContext sslContext = buildSslContext();
      HttpClient httpClient = buildHttpClient(connectionProvider, sslContext);

      return buildWebClient(httpClient);
  }

  private ConnectionProvider buildConnectionProvider() {
      return ConnectionProvider.builder(CONNECTION_PROVIDER_NAME)
      		.maxConnections(MAX_CONNECTIONS)
              .maxLifeTime(Duration.ofSeconds(TIMEOUT_SECONDS))
              .pendingAcquireTimeout(Duration.ofMillis(ACQUIRE_TIMEOUT))
                .pendingAcquireMaxCount(MAX_PENDING_ACQUIRES)
              .maxIdleTime(Duration.ofSeconds(ACQUIRE_TIMEOUT))
              .lifo()
              .build();
  }

  private SslContext buildSslContext() throws SSLException {
    return SslContextBuilder.forClient()
        .protocols("SSLv3","TLSv1","TLSv1.1","TLSv1.2")
            .ciphers(allowedCiphers)
            .trustManager(InsecureTrustManagerFactory.INSTANCE).build();
  }

  private HttpClient buildHttpClient(ConnectionProvider connectionProvider, SslContext sslContext) {
      return HttpClient.create(connectionProvider)
      		.compress(true) 
      		.followRedirect(true)
      		.resolver(DefaultAddressResolverGroup.INSTANCE)
      		.secure(t -> t.sslContext(sslContext).handshakeTimeout(Duration.ofSeconds(TIMEOUT_SECONDS)))
      		.keepAlive(true)
      		.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, CONNECT_TIMEOUT_MILLIS)
                .doOnConnected(connection -> {
                    connection.addHandlerLast(new ReadTimeoutHandler(READ_WRITE_TIMEOUT_SECONDS));
                    connection.addHandlerLast(new WriteTimeoutHandler(READ_WRITE_TIMEOUT_SECONDS));
                })
                .option(UnixChannelOption.SO_KEEPALIVE, true);
  }

  private WebClient.Builder buildWebClient(HttpClient httpClient) {
      return WebClient.builder()
      		.defaultHeader(HttpHeaders.ACCEPT_CHARSET, StandardCharsets.UTF_8.toString())
              .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(16 * 1024 * 1024))
              .clientConnector(new ReactorClientHttpConnector(httpClient));
  }
}

Please note: SSL check is disabled in above class. If you want to do SSL check, please modify the code.

Since bearer authentication is an industry standard, there are certain values like access_token and expires_in are returned along with other details in the response. So, let create the response object called BearerTokenResponseDTO

import lombok.Data;

@Data
public class BearerTokenResponseDTO {

  private String access_token;
  
  private String expires_in;
  
  // ... put other field as needed
}

Now, lets make the call to OAuth 2.0 server. Create interface called BearerTokenAuthenticationService

import org.springframework.stereotype.Service;
import com.example.bearer.token.auth.response.BearerTokenResponseDTO;
import reactor.core.publisher.Mono;

@Service
public interface BearerTokenAuthenticationService {
  
  public Mono<BearerTokenResponseDTO> bearerToken();

}

Create the implementation of above interface called BearerTokenAuthenticationServiceImpl

import java.util.Base64;
import java.util.Collections;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import com.example.bearer.token.auth.constant.BearerTokenAuthConstant;
import com.example.bearer.token.auth.response.BearerTokenResponseDTO;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import reactor.core.publisher.Mono;

@Log4j2
@RequiredArgsConstructor
@Service
public class BearerTokenAuthenticationServiceImpl implements BearerTokenAuthenticationService {
  
  private final WebClient.Builder builder;

  @Override
  public Mono<BearerTokenResponseDTO> bearerToken() {
    HttpHeaders headers = new HttpHeaders();
      headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
      headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
      
      String authString = BearerTokenAuthConstant.CLIENT_ID + ":" + BearerTokenAuthConstant.CLIENT_SECRET;
      headers.set("Authorization", Base64.getEncoder().encodeToString(authString.getBytes()));

      MultiValueMap<String, String> bodyParamMap = new LinkedMultiValueMap<>();
      bodyParamMap.add("grant_type", BearerTokenAuthConstant.GRANT_TYPE);
      bodyParamMap.add("scope", BearerTokenAuthConstant.SCOPE);

      return builder.baseUrl(BearerTokenAuthConstant.OAUTH2_BASE_URL).build()
            .post()
            .uri(BearerTokenAuthConstant.OAUTH2_URI)
            .headers(httpHeaders -> httpHeaders.addAll(headers))
            .bodyValue(bodyParamMap)
            .retrieve()
            .bodyToMono(BearerTokenResponseDTO.class)
            .doOnSuccess(response -> log.info("Status code 200, Response {}", response))
            .onErrorResume(WebClientResponseException.class, ex -> {
                log.error("Error while fetching token: {}", ex.getStatusCode(), ex);
                return Mono.empty();
            });
  }
}

In order to test this service, let create a @RestControllercalled BearerTokenController

import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.example.bearer.token.auth.response.BearerTokenResponseDTO;
import com.example.bearer.token.auth.service.BearerTokenAuthenticationService;
import lombok.RequiredArgsConstructor;
import reactor.core.publisher.Mono;

@RequiredArgsConstructor
@RestController
@RequestMapping("example")
public class BearerTokenController {
  
  private final BearerTokenAuthenticationService service;

  @PostMapping(value = "/toke/", produces = MediaType.APPLICATION_JSON_VALUE)
  public Mono<BearerTokenResponseDTO> getBearerToken() {
    return service.bearerToken();
  }
}

Use tools like Postman to test. You should see a JSON response something like this –

{
    "access_token": "fdd98915-4c7e-4678-9dd6-ca3c956eb22d",
    "token_type": "bearer",
    "expires_in": 13598,
     // other fields ......
}

You can download the full source code from GitHub – https://github.com/niteshapte/oauth-2.0-bearer-token-authentication-and-authorization-using-spring-boot-webflux

Direct Download – https://github.com/niteshapte/oauth-2.0-bearer-token-authentication-and-authorization-using-spring-boot-webflux/archive/refs/heads/main.zip

 

That’s it guys.

Hope you like it.

Have a great day ahead!

 

Loading