Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ msal4j = "1.23.1"
sonarqube = "7.2.1.6560"
spotless = "8.1.0"
gradleWrapperUpgrade = "0.12"
springBoot = "3.5.7"
springBoot = "3.5.8"
kotlinLogging = "7.0.13"

[libraries]
Expand All @@ -25,6 +25,7 @@ kafkaClients = { group = "org.apache.kafka", name = "kafka-clients" }
mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" }
msal = { group = "com.microsoft.azure", name = "msal4j", version.ref = "msal4j" }
slf4jApi = { group = "org.slf4j", name = "slf4j-api" }
kotlinReflect = { group = "org.jetbrains.kotlin", name = "kotlin-reflect", version.ref = "kotlin" }
kotlinLoggingJvm = { group = "io.github.oshai", name = "kotlin-logging-jvm", version.ref = "kotlinLogging" }
springBootAutoconfigure = { group = "org.springframework.boot", name = "spring-boot-autoconfigure" }
springBootDependencies = { group = "org.springframework.boot", name = "spring-boot-dependencies", version.ref = "springBoot" }
Expand Down
10 changes: 5 additions & 5 deletions oauth-token-client/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
dependencies{
implementation(libs.springContext)
api(libs.msal)
implementation(libs.springBootAutoconfigure)
implementation(libs.kotlinReflect)
implementation(libs.msal)

testImplementation(libs.junitJupiterApi)
testImplementation(libs.junitJupiterEngine)

testImplementation(libs.springTest)

testImplementation(libs.springBootTest)
testImplementation(libs.assertJ)

testRuntimeOnly(libs.junitPlatformLauncher)
Expand All @@ -19,6 +18,7 @@ testing {
dependencies {
implementation(project())
implementation(libs.springBootStarterTest)
implementation(libs.msal)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// SPDX-FileCopyrightText: Copyright Contributors to the GXF project
//
// SPDX-License-Identifier: Apache-2.0
package com.gxf.utilities.spring.oauth

import com.gxf.utilities.spring.oauth.providers.TokenProvider
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.test.context.TestPropertySource
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig

@SpringJUnitConfig(OAuthTokenClientContext::class)
@TestPropertySource("classpath:oauth-file-msal.properties")
class FileAndMsalTokenProviderTest {

@Autowired lateinit var tokenProvider: TokenProvider

@Test
fun `should return token from file even if msal config is present`() {
assertThat(tokenProvider.getAccessToken()).hasValue("test-token-from-file")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,21 @@
// SPDX-License-Identifier: Apache-2.0
package com.gxf.utilities.spring.oauth

import com.gxf.utilities.spring.oauth.providers.OAuthTokenProvider
import com.gxf.utilities.spring.oauth.providers.TokenProvider
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.test.context.TestPropertySource
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig

@SpringJUnitConfig(OAuthTokenClientContext::class)
@TestPropertySource("classpath:oauth-enabled.properties")
class OAuthTokenProviderTest {
@TestPropertySource("classpath:oauth-file.properties")
class FileTokenProviderTest {

@Autowired lateinit var tokenProvider: TokenProvider

@Test
fun test() {
assert(tokenProvider is OAuthTokenProvider)
fun `should return token from file`() {
assertThat(tokenProvider.getAccessToken()).hasValue("test-token-from-file")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// SPDX-FileCopyrightText: Copyright Contributors to the GXF project
//
// SPDX-License-Identifier: Apache-2.0
package com.gxf.utilities.spring.oauth

import com.gxf.utilities.spring.oauth.providers.TokenProvider
import com.microsoft.aad.msal4j.ClientCredentialParameters
import com.microsoft.aad.msal4j.ConfidentialClientApplication
import com.microsoft.aad.msal4j.IAccount
import com.microsoft.aad.msal4j.IAuthenticationResult
import com.microsoft.aad.msal4j.ITenantProfile
import java.util.Date
import java.util.concurrent.CompletableFuture
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.mockito.ArgumentMatchers.any
import org.mockito.Mockito.`when`
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.test.context.TestPropertySource
import org.springframework.test.context.bean.override.mockito.MockitoBean
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig

@SpringJUnitConfig(OAuthTokenClientContext::class)
@TestPropertySource("classpath:oauth-msal.properties")
class MsalTokenProviderTest {

@Nested
inner class TestMsalConfiguration {
inner class TestAuthenticationResult(val accessToken: String) : IAuthenticationResult {
override fun accessToken(): String = accessToken

override fun idToken(): String? = null

override fun account(): IAccount? = null

override fun tenantProfile(): ITenantProfile? = null

override fun environment(): String? = null

override fun scopes(): String? = null

override fun expiresOnDate(): Date? = null
}

@MockitoBean lateinit var confidentialClientApplication: ConfidentialClientApplication

@Autowired lateinit var tokenProvider: TokenProvider

@Test
fun `should retrieve token from msal library`() {
val testToken = "test-token-value"
`when`(confidentialClientApplication.acquireToken(any<ClientCredentialParameters>()))
.thenReturn(CompletableFuture.supplyAsync { TestAuthenticationResult(testToken) })

assertThat(tokenProvider.getAccessToken()).contains(testToken)
}
}

@Nested
inner class TestMsalTokenProvider {

@Autowired lateinit var tokenProvider: TokenProvider

@Test
fun `msal token provider should bootstrap if confidential client application is not mocked`() {
// The msal4j library is very hard to mock properly
assertThat(tokenProvider).isNotNull()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
// SPDX-License-Identifier: Apache-2.0
package com.gxf.utilities.spring.oauth

import com.gxf.utilities.spring.oauth.providers.NoTokenProvider
import com.gxf.utilities.spring.oauth.providers.TokenProvider
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.test.context.TestPropertySource
Expand All @@ -17,7 +17,7 @@ class NoTokenProviderTest {
@Autowired lateinit var tokenProvider: TokenProvider

@Test
fun test() {
assert(tokenProvider is NoTokenProvider)
fun `should return empty`() {
assertThat(tokenProvider.getAccessToken()).isNotPresent()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

test-token-from-file
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
### OAUTH ###
oauth.client.enabled=false
oauth.client.token-location=classpath:keys/token-file
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
### OAUTH ###
oauth.client.enabled=true

oauth.client.token-location=classpath:keys/token-file

# When location is defined, the msal properties should be ignored
oauth.client.token-endpoint=https://localhost:56788/token
oauth.client.client-id=some-test-client-id
oauth.client.scope=some-test-scope

oauth.client.private-key=classpath:keys/private-key.key
oauth.client.certificate=classpath:keys/certificate.crt

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### OAUTH ###
oauth.client.enabled=true
oauth.client.token-location=classpath:keys/token-file
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
### OAUTH ###
oauth.client.enabled=true

oauth.client.token-location=

# MSAL
oauth.client.token-endpoint=https://localhost:56788/token
oauth.client.client-id=some-test-client-id
oauth.client.scope=some-test-scope
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
// SPDX-License-Identifier: Apache-2.0
package com.gxf.utilities.spring.oauth

import com.gxf.utilities.spring.oauth.config.OAuthClientConfig
import com.gxf.utilities.spring.oauth.config.MsalClientConfig
import com.gxf.utilities.spring.oauth.config.OAuthClientProperties
import com.gxf.utilities.spring.oauth.providers.FileTokenProvider
import com.gxf.utilities.spring.oauth.providers.MsalTokenProvider
import com.gxf.utilities.spring.oauth.providers.NoTokenProvider
import com.gxf.utilities.spring.oauth.providers.OAuthTokenProvider
import org.springframework.context.annotation.Configuration
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Import
import org.springframework.stereotype.Component

@Configuration
@Import(OAuthClientConfig::class, OAuthClientProperties::class, OAuthTokenProvider::class, NoTokenProvider::class)
@Component
@EnableConfigurationProperties(OAuthClientProperties::class)
@Import(MsalClientConfig::class, MsalTokenProvider::class, FileTokenProvider::class, NoTokenProvider::class)
class OAuthTokenClientContext
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// SPDX-License-Identifier: Apache-2.0
package com.gxf.utilities.spring.oauth.config

import com.gxf.utilities.spring.oauth.config.condition.OAuthEnabledCondition
import com.gxf.utilities.spring.oauth.config.condition.OAuthMsalEnabledCondition
import com.gxf.utilities.spring.oauth.exceptions.OAuthTokenException
import com.microsoft.aad.msal4j.ClientCredentialFactory
import com.microsoft.aad.msal4j.ClientCredentialParameters
Expand All @@ -14,45 +14,50 @@ import java.security.PrivateKey
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.security.spec.PKCS8EncodedKeySpec
import java.util.*
import java.util.Base64
import org.slf4j.LoggerFactory
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Conditional
import org.springframework.context.annotation.Configuration
import org.springframework.core.io.Resource

@Configuration
@Conditional(OAuthEnabledCondition::class)
class OAuthClientConfig {
@Conditional(OAuthMsalEnabledCondition::class)
class MsalClientConfig {

companion object {
private val LOGGER = LoggerFactory.getLogger(OAuthClientConfig::class.java)
private val LOGGER = LoggerFactory.getLogger(MsalClientConfig::class.java)
private val PEM_REMOVAL_PATTERN = Regex("-----[A-Z ]*-----")
}

@Bean
fun clientCredentialParameters(clientData: OAuthClientProperties): ClientCredentialParameters {
return ClientCredentialParameters.builder(setOf(clientData.scope)).build()
}
fun clientCredentialParameters(properties: OAuthClientProperties): ClientCredentialParameters =
ClientCredentialParameters.builder(setOf(properties.scope)).build()

@Bean
fun confidentialClientApplication(clientData: OAuthClientProperties): ConfidentialClientApplication {
fun confidentialClientApplication(properties: OAuthClientProperties): ConfidentialClientApplication {
val credential: IClientCredential =
ClientCredentialFactory.createFromCertificate(
getPrivateKey(Objects.requireNonNull(clientData.privateKey)),
getCertificate(Objects.requireNonNull(clientData.certificate)),
getPrivateKey(properties.privateKey),
getCertificate(properties.certificate),
)
return try {
ConfidentialClientApplication.builder(clientData.clientId, credential)
.authority(clientData.tokenEndpoint)
ConfidentialClientApplication.builder(properties.clientId, credential)
.authority(properties.tokenEndpoint)
.build()
} catch (e: Exception) {
throw OAuthTokenException("Error creating client credentials", e)
}
}

/** Reads a private key file and puts */
fun getPrivateKey(resource: Resource): PrivateKey {
fun getPrivateKey(resource: Resource?): PrivateKey {
if (resource == null) {
throw OAuthTokenException("No private key provided")
} else if (!resource.isReadable) {
throw OAuthTokenException("Private key ${resource.description} is not readable")
}

try {
LOGGER.info("Reading private key: ${resource.description}")
val privateKeyContent = readPEMFile(resource)
Expand All @@ -63,7 +68,13 @@ class OAuthClientConfig {
}
}

fun getCertificate(resource: Resource): X509Certificate {
fun getCertificate(resource: Resource?): X509Certificate {
if (resource == null) {
throw OAuthTokenException("No certificate provided")
} else if (!resource.isReadable) {
throw OAuthTokenException("Certificate ${resource.description} is not readable")
}

try {
LOGGER.info("Reading certificate: ${resource.description}")
val certificateContent = readPEMFile(resource)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,15 @@
// SPDX-License-Identifier: Apache-2.0
package com.gxf.utilities.spring.oauth.config

import com.gxf.utilities.spring.oauth.config.condition.OAuthEnabledCondition
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Conditional
import org.springframework.context.annotation.Configuration
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.core.io.Resource

@Configuration
@Conditional(OAuthEnabledCondition::class)
class OAuthClientProperties(
@Value("\${oauth.client.client-id}") val clientId: String,
@Value("\${oauth.client.scope}") val scope: String,
@Value("\${oauth.client.token-endpoint}") val tokenEndpoint: String,
@Value("\${oauth.client.private-key}") val privateKey: Resource,
@Value("\${oauth.client.certificate}") val certificate: Resource,
@ConfigurationProperties(prefix = "oauth.client")
data class OAuthClientProperties(
val tokenLocation: Resource?,
val clientId: String?,
val scope: String?,
val tokenEndpoint: String?,
val certificate: Resource?,
val privateKey: Resource?,
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,20 @@ package com.gxf.utilities.spring.oauth.config.condition

import org.springframework.context.annotation.Condition
import org.springframework.context.annotation.ConditionContext
import org.springframework.core.type.AnnotatedTypeMetadata

/** Condition to enable or disable the Oauth Client components */
abstract class OAuthCondition : Condition {
fun oAuthEnabled(context: ConditionContext) =
context.environment.getProperty("oauth.client.enabled").equals("true", ignoreCase = true)
class OAuthTokenFileEnabledCondition : Condition {
override fun matches(context: ConditionContext, metadata: AnnotatedTypeMetadata): Boolean =
oAuthEnabled(context) && tokenLocationPresent(context)
}

class OAuthMsalEnabledCondition : Condition {
override fun matches(context: ConditionContext, metadata: AnnotatedTypeMetadata): Boolean =
oAuthEnabled(context) && !tokenLocationPresent(context)
}

private fun oAuthEnabled(context: ConditionContext) =
context.environment.getProperty("oauth.client.enabled").equals("true", ignoreCase = true)

private fun tokenLocationPresent(context: ConditionContext) =
!context.environment.getProperty("oauth.client.token-location").isNullOrBlank()

This file was deleted.

Loading
Loading