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: