How to create an OAuth2 Client using Spring Boot

Overview

 

This guide shows how to create an OAuth2 client and consume an endpoint protected by OAuth2 authorization server using Spring Boot.

Introduction

Let’s say you are part of a Bank backend development team and you need to create an internal endpoint, which will give real time status of international payment transaction. The internal endpoint when invoked will call an external swift endpoint, which gives detailed status history of payment went through swift network. The response returned from swift needs to be transformed into customised simple status response. The swift endpoint is protected by OAuth2 Authorization server using grant type urn:ietf:params:oauth:grant-type:jwt-bearer. This grant type enables an OAuth client to request an access token using JSON Web Tokens (JWT). The subsequent part of this guide will show key components involved in creating a Spring boot application, which exposes a rest end point and also acts as an OAuth2 Client to external Swift server.

Proposed System Architecture

 

Rest Controller

@RestController
class TransactionController(val swiftTransactionService: SwiftTransactionService) {
@GetMapping("/transactions/{uetr}")
  fun getTransactionStatus(@PathVariable uetr: String): ResponseEntity<TransactionStatus> {
    val status = swiftTransactionService.getTransactionStatus(uetr)
    return ResponseEntity.ok(TransactionStatus(status))
  }
}

Typical Spring Boot Rest controller to create an endpoint , it uses swiftTransactionService which in turns call external swift resource end point, protected by Swift Authorization server

Service

@Service
@EnableConfigurationProperties(SwiftApiProperties::class)
class SwiftTransactionService(
  private val webClient: WebClient,
  private val swiftApiProperties: SwiftApiProperties
) {

  fun getTransactionStatus(uetr: String): String {
  val swiftResponse = webClient
    .get()
    .uri(URI(swiftApiProperties.resourceUrl.format(uetr)))
    .retrieve()
    .bodyToMono<Map<String, Any>>()
    .block() ?: emptyMap()
  val status = swiftResponse["transaction_status"] as String
  return status
  }
}

Service layer uses WebClient to call external resource end point https://sandbox.swift.com/swift-apitracker/v5/payments/{uetr}/transactions, which is protected by authorization server. WebClient is configured to automtically send client credentials like clientId, clientSecret, JWT to Authorization server to get valid access token, which is then used to access resource end point.

JwtTokenUtil

@Component
@EnableConfigurationProperties(SwiftApiProperties::class)
class JwtTokenUtil(val swiftApiProperties: SwiftApiProperties) {

  fun createJwtToken(): String {
    val certFactory = CertificateFactory.getInstance("X.509")
    val certificateInputStream = loadPublicCertificateAsStream(swiftApiProperties.certPath)
    val certificate: X509Certificate = certFactory.generateCertificate(certificateInputStream) as X509Certificate
    val base64EncodedCert = com.nimbusds.jose.util.Base64.encode(certificate.encoded)
    val header = JWSHeader.Builder(JWSAlgorithm.RS256)
     .type(JOSEObjectType.JWT)
     .x509CertChain(listOf(base64EncodedCert))
     .build()

    val jti = (1..12)
      .map { "abcdefghijklmnopqrstuvwxyz0123456789".random() }
      .joinToString("")
    val currentTimeMillis = System.currentTimeMillis()
    val issuedAt = currentTimeMillis / 1000
    val expiration = issuedAt + 900 // 15 minutes from issuance
    val claims = JWTClaimsSet.Builder()
      .issuer(swiftApiProperties.issuer)
      .audience(swiftApiProperties.audience)
      .subject(swiftApiProperties.subject)
      .jwtID(jti)
      .expirationTime(Date(expiration * 1000))
      .issueTime(Date(issuedAt * 1000))
      .build()

    val signedJWT = SignedJWT(
      header,
      claims
      )
    val privateKey = loadPrivateKey(swiftApiProperties.privateKeyPath)
      signedJWT.sign(RSASSASigner(privateKey))
      return signedJWT.serialize()
  }

  fun loadPrivateKey(privateKeyPath: String): PrivateKey {
    Security.addProvider(BouncyCastleProvider())
    // Load the private key from the PEM file
    val resource = ClassPathResource(privateKeyPath)
    val keyContentPKCS1 = resource.inputStream.bufferedReader(StandardCharsets.UTF_8).use { it.readText() }
     .replace("-----BEGIN RSA PRIVATE KEY-----", "")
     .replace("-----END RSA PRIVATE KEY-----", "")
     .replace("\\s+".toRegex(), "")

    val keyBytes: ByteArray = Base64.getDecoder().decode(keyContentPKCS1)
    val asn1Parser = ASN1StreamParser(keyBytes)
    val asn1Object = asn1Parser.readObject()
    val rsaKey = RSAPrivateKey.getInstance(asn1Object.toASN1Primitive())
    val keySpec = RSAPrivateCrtKeySpec(
     rsaKey.modulus,
     rsaKey.publicExponent,
     rsaKey.privateExponent,
     rsaKey.prime1,
     rsaKey.prime2,
     rsaKey.exponent1,
     rsaKey.exponent2,
     rsaKey.coefficient
     )
  val keyFactory = KeyFactory.getInstance("RSA")
  val privateKey = keyFactory.generatePrivate(keySpec)
  return privateKey
}
  fun loadPublicCertificateAsStream(filePath: String): InputStream {
    val resource = ClassPathResource(filePath)
    return resource.inputStream
  }
}

This utlity is used to create JWT dynamically. JWT header is constructed using clients public certificate, kept as cert.pem in resources folder.JWT payload is constructed using statically configured: issuer, audience, subject, and dynamically generated: JTI, issued Time and expiration Time. The consolidated JWT is then signed with clients private Key , kept as privateKey.pem in resources folder.

P.S. Sensitive information like private Key and public certificate should not be kept along side the code for security reason. Ideally they should be kept in secure vault like AWS secret manager and access dynamically. For the simplicity of this demo, it is kept along side the code.

OAuth2ClientConfig

@Configuration
@EnableConfigurationProperties(SwiftApiProperties::class)
class OAuth2ClientConfig(
  val clientRegistrationRepository: ClientRegistrationRepository,
  val authorizedClientRepository: OAuth2AuthorizedClientRepository,
  val jwtTokenUtil: JwtTokenUtil,
  val swiftApiProperties: SwiftApiProperties
) {
  @Bean
  fun authorizedClientManager(): OAuth2AuthorizedClientManager {
    val authorizedClientManager = DefaultOAuth2AuthorizedClientManager(
      clientRegistrationRepository,
      authorizedClientRepository
     )
    authorizedClientManager.setAuthorizedClientProvider { context ->
      val clientRegistration = context.clientRegistration
      val jwtBearerToken = jwtTokenUtil.createJwtToken()
      val formData: MultiValueMap<String, String> = LinkedMultiValueMap()
      formData.add("grant_type", swiftApiProperties.grantType)
      formData.add("assertion", jwtBearerToken)
      formData.add("scope", swiftApiProperties.scope)
      val body = BodyInserters
       .fromFormData(formData)
      WebClient.builder()
       .defaultHeader("Content-Type", "application/x-www-form-urlencoded")
       .defaultHeaders { headers ->
          headers.setBasicAuth(
            swiftApiProperties.consumerKey,
            swiftApiProperties.consumerSecret
          )
        }
        .build()
        .post()
        .uri(URI(clientRegistration.providerDetails.tokenUri))
        .body(body)
        .retrieve()
        .bodyToMono<Map<String, Any>>()
        .map { responseMap ->
          OAuth2AuthorizedClient(
            clientRegistration, context.principal.name, OAuth2AccessToken(
            OAuth2AccessToken.TokenType.BEARER,
            responseMap?.let {
              it["access_token"] as String
            },
            Instant.now(),
            Instant.now().plusSeconds(1799)
          )
          )
        }
        .block()

    }
    return authorizedClientManager
  }
  @Bean
  fun webClient(authorizedClientManager: OAuth2AuthorizedClientManager): WebClient {
    val oauth2Filter = ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager)
    oauth2Filter.setDefaultClientRegistrationId("swift-oauth-client")
    return WebClient.builder()
      .apply(oauth2Filter.oauth2Configuration())
      .build()
  }

}

Spring Bean OAuth2AuthorizedClientManager is configured to call Swift token endpoint automatically and get access token, when ever new token is required or when old token is expired. The bean is injected into Spring Bean WebClient and is subsequently used in Service layer to call swift resource endpoint

Summary

  • Implementing OAuth2 Client with Spring Boot and WebClient reduces complexity and boilerplate code.
  • Bean type OAuth2AuthorizedClientManager automates retrieval of access token required for accessing protected endpoint and hides low level plumbing
  • Code can be accessed from here
  • Useful references:

https://oauth.net/2/

https://auth0.com/intro-to-iam/what-is-oauth-2

https://en.wikipedia.org/wiki/JSON_Web_Token

Kotlin for Java Developers in 10 minutes

Kotlin is a statically typed programming language that runs on Java virtual machine and also can be compiled to JavaScript source code. It is designed to interoperate with Java code, meaning Kotlin code can be called from Java and Java code from Kotlin.

Variables

Java

final int x;
final int y = 1;
int z = 2;

Kotlin

val x: Int
val y = 1
var z = 2

Nullable Property

Java

final String name = null;
String lastName;
lastName = null

Kotlin

val name: String? = null
var lastName: String?
lastName = null
var firstName: String
// Compilation error!!
firstName = null

Null Safety

Java

if (name != null) { 
int length = name.length
}

Kotlin

val length = name?.length

Multi Line String

Java

String address = "FirstLine\n" +
"Second Line\n" +
"Third Line";

Kotlin

val address = """
|FirstLine
|SecondLine
|ThirdLine
""".trimMargin()

Ternary Operator

Java

String result = x > 5 ? "x > 5" : "x<=5";

Kotlin

val result = if (x > 5)
"x > 5"
else "x <=5"

Switch/When

Java

final int x = 3;
final String result;
switch (x) {
case 0:
case 11:
result = "0 or 11"
break;
case 1:
case 2:
//...
case 10:
result = " from 1 to 10";
break;
default
result = " not within range 0-11"

Kotlin

val x = 3
val result = when (x) {
0,11 -> "0 or 11"
in 1..10 -> " from 1 to 10"
else -> "not within range 0-11"
}

Collections

Java

final List<Integer> numbers = Arrays.asList(1, 2, 3);
final Map<Integer, String> map = new HashMap<Integer, String>();
map.put(1, "One");
map.put(2, "Two);
map.put(3, "Three);

Kotlin

val numbers = listof(1, 2, 3)
val map = mapOf(1 to "One",
2 to "Two",
3 to "Three")

Functions

Vararg

Java

public int sum(int... numbers ) {}

Kotlin

fun sum(vararg x: It) {}

Main

Java

public class MyClass { 
public static void main(String[] args) {
}
}

Kotlin

fun main(args: Array<String>){
}

Optional Arguments

Java

openFile("file.txt", true);
public static File openFile(String filename, boolean readonly);

Kotlin

openFile("file.txt")
fun openFile(filename: String, readOnly: Boolean = true)

Classes

Constructor Call

Java

final User user = new User("Tom");

Kotlin

val user = User("Tom")

Final Class

Java

public final class User {
}

Kotlin

class User

Open Class

Java

public class User {
}

Kotlin

open class User

Final Attributes

Java

final class User {
private final String name;
public User(String name) {
this.name = name;
}
public String getName() {
return name;
}
}

Kotlin

class User(val name: String)

Primary Constructor

Java

final class User {
private String name;
public User(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

Kotlin

class User(var name: String)

Optional arguments in Constructors

Java

final class User {
private String name;
private String lastName;

public User(String name) {
this(name, "");
}
public User(String name, String lastName) {
this.name = name;
this.lastName = lastName;
}
// Getter and setters
}

Kotlin

class User(var name: String, var lastName: String = "")

Properties

Java

public class User {
private String id = "00x";

public String getId() {
return id;
}
public void setId(String id) {
if(id !=null && !id.isEmpty()){
this.id = id;
}
}

}

Kotlin

class User {
var id: String = "00x"
set(value) {
if(value.isNotEmpty()) field = value
}
}

Abstract Class

Java

public abstract class Animal {
public abstract void sound();
}
public class Dog extends Animal {
@override
public void sound() {
System.out.println("Woof");
}
}

Kotlin

abstract class Animal {
abstract fun sound(): Unit
}
class Dog : Animal() {
override fun sound(): Unit {
println("Woof")
}
}

Singleton

Java

public class Animal {
private static final Animal instance = new Animal();

public static Animal getInstance(){
return instance;
}
}

Kotlin

object Animal {
}

Extensions

Java

public class ByteArrayUtils { 
public static String toHexString(byte[] data) {
}

}
final byte[] dummyData = new byte[10];
final String hexValue = ByteArrayUtils.toHexString(dummyData);

Kotlin

fun ByteArray.toHex() : String {
}
val dummyData = byteArrayOf()
val hexValue = dummyData.toHex()

Interface

Java

public interface Animal {
void eat();
}
public class Dog implements Animal {
@Override
public void eat() {
}
}

Kotlin

interface Animal {
fun eat()
}
class Dog : Animal {
override fun eat() {
}
}