Skip to content

Commit a3b17f5

Browse files
committedMar 25, 2025··
Texas-klient
1 parent 2e1aa71 commit a3b17f5

File tree

7 files changed

+264
-74
lines changed

7 files changed

+264
-74
lines changed
 

‎texas-klient/src/main/kotlin/no/nav/dagpenger/texas/Config.kt

+6-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import com.natpryce.konfig.stringType
99

1010
object Config {
1111
val configuration: Configuration = ConfigurationProperties.systemProperties() overriding EnvironmentVariables()
12-
val tokenEndpoint = configuration[Key("nais.token.endpoint", stringType)]
13-
val tokenExchangeEndpoint = configuration[Key("nais.token.exhange.endpoint", stringType)]
12+
val tokenEndpoint = configuration.tokenEndpoint()
13+
val tokenExchangeEndpoint = configuration.tokenExchangeEndpoint()
14+
15+
fun Configuration.tokenEndpoint() = this[Key("nais.token.endpoint", stringType)]
16+
17+
fun Configuration.tokenExchangeEndpoint() = this[Key("nais.token.endpoint", stringType)]
1418
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package no.nav.dagpenger.texas
2+
3+
import io.ktor.client.HttpClient
4+
5+
class EntraKlient(
6+
tokenEndpoint: String,
7+
tokenExchangeEndpoint: String,
8+
introspectEndpoint: String,
9+
httpClient: HttpClient = defaultHttpClient(),
10+
) {
11+
companion object {
12+
val IDENTITY_PROVIDER = IdentityProvider.ENTRA_ID
13+
}
14+
15+
private val texasKlient: TexasKlient =
16+
TexasKlient(
17+
tokenEndpoint = tokenEndpoint,
18+
tokenExchangeEndpoint = tokenExchangeEndpoint,
19+
introspectEndpoint = introspectEndpoint,
20+
httpClient = httpClient,
21+
)
22+
23+
suspend fun accessToken(
24+
target: String,
25+
resource: String? = null,
26+
skipCache: Boolean = true,
27+
): TokenResponse =
28+
texasKlient.accessToken(
29+
target = target,
30+
identityProvider = IDENTITY_PROVIDER,
31+
resource = resource,
32+
skipCache = skipCache,
33+
)
34+
35+
suspend fun exchangeToken(
36+
target: String,
37+
token: String,
38+
skipCache: Boolean = false,
39+
): TokenResponse =
40+
texasKlient.exchangeToken(
41+
target = target,
42+
token = token,
43+
identityProvider = IDENTITY_PROVIDER,
44+
skipCache = skipCache,
45+
)
46+
}

‎texas-klient/src/main/kotlin/no/nav/dagpenger/texas/HttpClient.kt

+11-7
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package no.nav.dagpenger.texas
22

33
import com.fasterxml.jackson.annotation.JsonInclude
44
import com.fasterxml.jackson.databind.DeserializationFeature
5+
import com.fasterxml.jackson.databind.module.SimpleModule
56
import io.ktor.client.HttpClient
67
import io.ktor.client.HttpClientConfig
78
import io.ktor.client.call.body
@@ -18,23 +19,27 @@ import io.ktor.serialization.jackson.jackson
1819

1920
fun defaultHttpClient(
2021
httpClientEngine: HttpClientEngine = CIO.create {},
21-
configure: List<HttpClientConfig<*>.() -> Unit> = listOf(defaultRetryConfig()),
22+
configure: List<HttpClientConfig<*>.() -> Unit> = listOf(defaultPlugins()),
2223
) = HttpClient(httpClientEngine) {
2324
expectSuccess = true
2425

2526
install(ContentNegotiation) {
2627
jackson {
2728
configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
2829
setSerializationInclusion(JsonInclude.Include.NON_NULL)
30+
registerModules(
31+
SimpleModule().also {
32+
it.addDeserializer(IntrospectResponse::class.java, IntrospectResponseDeserializer)
33+
},
34+
)
2935
}
3036
}
3137

3238
HttpResponseValidator {
33-
handleResponseException { cause, request ->
39+
handleResponseException { cause, _ ->
3440
when (cause) {
3541
is ClientRequestException -> {
36-
val statusCode = cause.response.status
37-
when (statusCode) {
42+
when (val statusCode = cause.response.status) {
3843
HttpStatusCode.BadRequest -> {
3944
val errorResponse = cause.response.body<ErrorResponse>()
4045
throw BadRequestException(statusCode, errorResponse)
@@ -43,8 +48,7 @@ fun defaultHttpClient(
4348
}
4449

4550
is ServerResponseException -> {
46-
val statusCode = cause.response.status
47-
when (statusCode) {
51+
when (val statusCode = cause.response.status) {
4852
HttpStatusCode.InternalServerError -> {
4953
val errorResponse = cause.response.body<ErrorResponse>()
5054
throw ServerError(statusCode, errorResponse)
@@ -57,7 +61,7 @@ fun defaultHttpClient(
5761
configure.forEach { it() }
5862
}
5963

60-
fun defaultRetryConfig(): HttpClientConfig<*>.() -> Unit =
64+
fun defaultPlugins(): HttpClientConfig<*>.() -> Unit =
6165
{
6266
install(HttpRequestRetry) {
6367
retryOnException(maxRetries = 3, retryOnTimeout = true)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package no.nav.dagpenger.texas
2+
3+
import com.fasterxml.jackson.core.JsonParser
4+
import com.fasterxml.jackson.core.type.TypeReference
5+
import com.fasterxml.jackson.databind.DeserializationContext
6+
import com.fasterxml.jackson.databind.JsonDeserializer
7+
8+
object IntrospectResponseDeserializer : JsonDeserializer<IntrospectResponse>() {
9+
override fun deserialize(
10+
p: JsonParser,
11+
ctxt: DeserializationContext,
12+
): IntrospectResponse {
13+
return kotlin.runCatching {
14+
val map = p.codec.readValue(p, object : TypeReference<Map<String, Any>>() {})
15+
when (map["active"] as Boolean) {
16+
true -> {
17+
IntrospectResponse.Valid(
18+
map.filter {
19+
it.key != "active"
20+
},
21+
)
22+
}
23+
24+
false -> {
25+
IntrospectResponse.Invalid(map["error"] as String)
26+
}
27+
}
28+
}.getOrElse { t ->
29+
throw ParseException("Failed to parse IntrospectResponse:", t)
30+
}
31+
}
32+
}
33+
34+
class ParseException(message: String, cause: Throwable) : RuntimeException(message, cause)
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,40 @@
11
package no.nav.dagpenger.texas
22

3+
import com.fasterxml.jackson.annotation.JsonValue
34
import io.ktor.client.HttpClient
45
import io.ktor.client.call.body
56
import io.ktor.client.request.header
67
import io.ktor.client.request.post
78
import io.ktor.client.request.setBody
89
import io.ktor.http.HttpStatusCode
910

11+
data class IntrospectRequest(
12+
val identity_provider: IdentityProvider,
13+
val token: String,
14+
)
15+
16+
sealed class IntrospectResponse(val active: Boolean) {
17+
data class Valid(val claims: Map<String, Any>) : IntrospectResponse(active = true)
18+
19+
data class Invalid(val error: String) : IntrospectResponse(active = false)
20+
}
21+
22+
enum class IdentityProvider(
23+
@JsonValue
24+
val value: String,
25+
) {
26+
ENTRA_ID("azuread"),
27+
}
28+
1029
data class TokenRequest(
11-
val identity_provider: String,
30+
val identity_provider: IdentityProvider,
1231
val target: String,
1332
val resource: String? = null,
1433
val skip_cache: Boolean = false,
1534
)
1635

1736
data class TokenExchangeRequest(
18-
val identity_provider: String,
37+
val identity_provider: IdentityProvider,
1938
val target: String,
2039
val user_token: String,
2140
val skip_cache: Boolean = false,
@@ -43,55 +62,15 @@ class BadRequestException(httpStatusCode: HttpStatusCode, errorResponse: ErrorRe
4362
class ServerError(httpStatusCode: HttpStatusCode, errorResponse: ErrorResponse) :
4463
RequestError(httpStatusCode, errorResponse)
4564

46-
class EntraKlient(
47-
tokenEndpoint: String,
48-
tokenExchangeEndpoint: String,
49-
httpClient: HttpClient = defaultHttpClient(),
50-
) {
51-
companion object {
52-
const val IDENTITY_PROVIDER = "azuread"
53-
}
54-
55-
private val texasKlient: TexasKlient =
56-
TexasKlient(
57-
tokenEndpoint = tokenEndpoint,
58-
tokenExchangeEndpoint = tokenExchangeEndpoint,
59-
httpClient = httpClient,
60-
)
61-
62-
suspend fun accessToken(
63-
target: String,
64-
resource: String? = null,
65-
skipCache: Boolean = true,
66-
): TokenResponse =
67-
texasKlient.accessToken(
68-
target = target,
69-
identityProvider = IDENTITY_PROVIDER,
70-
resource = resource,
71-
skipCache = skipCache,
72-
)
73-
74-
suspend fun exchangeToken(
75-
target: String,
76-
token: String,
77-
skipCache: Boolean = false,
78-
): TokenResponse =
79-
texasKlient.exchangeToken(
80-
target = target,
81-
token = token,
82-
identityProvider = IDENTITY_PROVIDER,
83-
skipCache = skipCache,
84-
)
85-
}
86-
8765
class TexasKlient(
8866
private val tokenEndpoint: String,
8967
private val tokenExchangeEndpoint: String,
68+
private val introspectEndpoint: String,
9069
private val httpClient: HttpClient = defaultHttpClient(),
9170
) {
9271
suspend fun accessToken(
9372
target: String,
94-
identityProvider: String,
73+
identityProvider: IdentityProvider,
9574
resource: String? = null,
9675
skipCache: Boolean,
9776
): TokenResponse {
@@ -107,19 +86,40 @@ class TexasKlient(
10786
suspend fun exchangeToken(
10887
target: String,
10988
token: String,
110-
identityProvider: String,
89+
identityProvider: IdentityProvider,
11190
skipCache: Boolean,
11291
): TokenResponse {
113-
return httpClient.post(tokenExchangeEndpoint) {
114-
header("Content-Type", "application/json")
115-
setBody(
116-
TokenExchangeRequest(
117-
identity_provider = identityProvider,
118-
target = target,
119-
user_token = token,
120-
skip_cache = skipCache,
121-
),
122-
)
123-
}.body<TokenResponse>()
92+
return kotlin.runCatching {
93+
httpClient.post(tokenExchangeEndpoint) {
94+
header("Content-Type", "application/json")
95+
setBody(
96+
TokenExchangeRequest(
97+
identity_provider = identityProvider,
98+
target = target,
99+
user_token = token,
100+
skip_cache = skipCache,
101+
),
102+
)
103+
}.body<TokenResponse>()
104+
}.onFailure {
105+
}.getOrThrow()
106+
}
107+
108+
suspend fun introspect(
109+
identityProvider: IdentityProvider,
110+
token: String,
111+
): IntrospectResponse {
112+
return kotlin.runCatching {
113+
httpClient.post(introspectEndpoint) {
114+
header("Content-Type", "application/json")
115+
setBody(
116+
IntrospectRequest(
117+
identity_provider = identityProvider,
118+
token = token,
119+
),
120+
)
121+
}.body<IntrospectResponse>()
122+
}.onFailure {
123+
}.getOrThrow()
124124
}
125125
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package no.nav.dagpenger.texas
2+
3+
import com.fasterxml.jackson.databind.module.SimpleModule
4+
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
5+
import io.kotest.matchers.shouldBe
6+
import org.junit.jupiter.api.Test
7+
8+
class IntrospectResponseDeserializerTest {
9+
private val mapper =
10+
jacksonObjectMapper().also {
11+
val simpleModule =
12+
SimpleModule().also {
13+
it.addDeserializer(IntrospectResponse::class.java, IntrospectResponseDeserializer)
14+
}
15+
it.registerModule(simpleModule)
16+
}
17+
18+
@Test
19+
fun `deserialize valid response`() {
20+
//language=json
21+
val json =
22+
"""
23+
{
24+
"active": true,
25+
"sub": "1234567890",
26+
"name": "John Doe",
27+
"int": 12223,
28+
"admin": true
29+
}
30+
""".trimIndent()
31+
32+
mapper.readValue(json, IntrospectResponse::class.java) shouldBe
33+
IntrospectResponse.Valid(
34+
mapOf(
35+
"sub" to "1234567890",
36+
"int" to 12223,
37+
"name" to "John Doe",
38+
"admin" to true,
39+
),
40+
)
41+
}
42+
43+
@Test
44+
fun `deserialize invalid response`() {
45+
//language=json
46+
val json =
47+
"""
48+
{
49+
"active": false,
50+
"error": "This is an error"
51+
}
52+
""".trimIndent()
53+
54+
mapper.readValue(json, IntrospectResponse::class.java) shouldBe
55+
IntrospectResponse.Invalid(
56+
"This is an error",
57+
)
58+
}
59+
}

‎texas-klient/src/test/kotlin/no/nav/dagpenger/texas/TexasKlientTest.kt

+52-9
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@ import org.junit.jupiter.api.Test
1919

2020
class TexasKlientTest {
2121
@Test
22-
fun `riktig http verb,headers og body`() {
22+
fun `riktig http verb,headers, body og response ved kall til Texas`() {
2323
var tokenRequest: HttpRequestData? = null
2424
var tokenExchangeRequest: HttpRequestData? = null
25+
var introspectRequest: HttpRequestData? = null
2526
runBlocking {
2627
val mockEngine =
2728
MockEngine { request ->
@@ -46,35 +47,76 @@ class TexasKlientTest {
4647
)
4748
}
4849

50+
"introspect" -> {
51+
introspectRequest = request
52+
respond(
53+
// language=json
54+
content = """{ "active": true, "sub": "1234567890", "name": "John Doe", "int": 12223, "admin": true }""",
55+
status = HttpStatusCode.OK,
56+
headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()),
57+
)
58+
}
59+
4960
else -> error("bad url")
5061
}
5162
}
5263
TexasKlient(
5364
tokenEndpoint = "http://localhost/token",
5465
tokenExchangeEndpoint = "http://localhost/tokenexchange",
66+
introspectEndpoint = "http://localhost/introspect",
5567
httpClient = defaultHttpClient(mockEngine),
5668
).let { client ->
5769
client.accessToken(
5870
target = "target",
59-
identityProvider = "test",
71+
identityProvider = IdentityProvider.ENTRA_ID,
6072
skipCache = true,
61-
)
73+
) shouldBe
74+
TokenResponse(
75+
access_token = "token",
76+
token_type = "Bearer",
77+
expires_in = 9999,
78+
)
6279
requireNotNull(tokenRequest).let { it ->
6380
//language=json
64-
String(it.body.toByteArray()) shouldEqualJson """{"identity_provider":"test","target":"target", "skip_cache":true}"""
81+
String(it.body.toByteArray()) shouldEqualJson """{"identity_provider":"azuread","target":"target", "skip_cache":true}"""
6582
it.headers[HttpHeaders.Accept] shouldBe ContentType.Application.Json.toString()
6683
it.method shouldBe HttpMethod.Post
6784
}
6885

6986
client.exchangeToken(
7087
target = "target",
7188
token = "user_token",
72-
identityProvider = "test",
89+
identityProvider = IdentityProvider.ENTRA_ID,
7390
skipCache = true,
74-
)
91+
) shouldBe
92+
TokenResponse(
93+
access_token = "obo_token",
94+
token_type = "Bearer",
95+
expires_in = 9999,
96+
)
7597
requireNotNull(tokenExchangeRequest).let {
7698
//language=json
77-
String(it.body.toByteArray()) shouldEqualJson """{"identity_provider":"test","target":"target","user_token":"user_token", "skip_cache":true}"""
99+
String(it.body.toByteArray()) shouldEqualJson """{"identity_provider":"azuread","target":"target","user_token":"user_token", "skip_cache":true}"""
100+
it.headers[HttpHeaders.Accept] shouldBe ContentType.Application.Json.toString()
101+
it.method shouldBe HttpMethod.Post
102+
}
103+
104+
client.introspect(
105+
identityProvider = IdentityProvider.ENTRA_ID,
106+
token = "introspect_token",
107+
) shouldBe
108+
IntrospectResponse.Valid(
109+
claims =
110+
mapOf(
111+
"sub" to "1234567890",
112+
"name" to "John Doe",
113+
"int" to 12223,
114+
"admin" to true,
115+
),
116+
)
117+
requireNotNull(introspectRequest).let {
118+
//language=json
119+
String(it.body.toByteArray()) shouldEqualJson """{"identity_provider":"azuread","token":"introspect_token"}"""
78120
it.headers[HttpHeaders.Accept] shouldBe ContentType.Application.Json.toString()
79121
it.method shouldBe HttpMethod.Post
80122
}
@@ -111,12 +153,13 @@ class TexasKlientTest {
111153
TexasKlient(
112154
tokenEndpoint = "http://localhost/token",
113155
tokenExchangeEndpoint = "http://localhost/tokenexchange",
156+
introspectEndpoint = "http://localhost/introspect",
114157
httpClient = defaultHttpClient(mockEngine),
115158
).let { client ->
116159
shouldThrow<BadRequestException> {
117160
client.accessToken(
118161
target = "target",
119-
identityProvider = "test",
162+
identityProvider = IdentityProvider.ENTRA_ID,
120163
skipCache = true,
121164
)
122165
}.errorResponse shouldBe ErrorResponse("badrequest", "badrequest description")
@@ -125,7 +168,7 @@ class TexasKlientTest {
125168
client.exchangeToken(
126169
target = "target",
127170
token = "user_token",
128-
identityProvider = "test",
171+
identityProvider = IdentityProvider.ENTRA_ID,
129172
skipCache = true,
130173
)
131174
}.errorResponse shouldBe

0 commit comments

Comments
 (0)
Please sign in to comment.