-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
klient for den kule STS-tjenesten Gandalf
- Loading branch information
1 parent
63b3ace
commit f995bf8
Showing
14 changed files
with
300 additions
and
5 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,3 @@ | ||
val jacksonVersion = "2.16.1" | ||
|
||
dependencies { | ||
api(project(":azure-token-client")) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
Minimal STS Client | ||
================== | ||
|
||
Kontakter STS-tjenesten [Gandalf](https://github.com/navikt/gandalf), som er et bedre alternativ til den tradisjonelle | ||
SOAP-STS-tjenesten. | ||
|
||
Med denne klienten er du på god veg til å kvitte deg med CXF og sånt tull. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
val jacksonVersion: String by project | ||
val mockkVersion: String by project | ||
|
||
dependencies { | ||
api("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion") | ||
api("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jacksonVersion") | ||
|
||
testImplementation("io.mockk:mockk:$mockkVersion") | ||
testImplementation(project(":mock-http-client")) | ||
} |
20 changes: 20 additions & 0 deletions
20
minimal-sts-client/src/main/kotlin/com/github/navikt/tbd_libs/soap/InMemoryStsClient.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
package com.github.navikt.tbd_libs.soap | ||
|
||
import java.util.concurrent.ConcurrentHashMap | ||
|
||
class InMemoryStsClient(private val other: SamlTokenProvider) : SamlTokenProvider by (other) { | ||
private val cache = ConcurrentHashMap<String, SamlToken>() | ||
|
||
override fun samlToken(username: String, password: String): SamlToken { | ||
return retrieveFromCache(username) ?: storeInCache(username, password) | ||
} | ||
|
||
private fun retrieveFromCache(username: String) = | ||
cache[username]?.takeUnless(SamlToken::isExpired) | ||
|
||
private fun storeInCache(username: String, password: String): SamlToken { | ||
return other.samlToken(username, password).also { | ||
cache[username] = it | ||
} | ||
} | ||
} |
67 changes: 67 additions & 0 deletions
67
minimal-sts-client/src/main/kotlin/com/github/navikt/tbd_libs/soap/MinimalStsClient.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
package com.github.navikt.tbd_libs.soap | ||
|
||
import com.fasterxml.jackson.databind.JsonNode | ||
import com.fasterxml.jackson.databind.ObjectMapper | ||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper | ||
import java.net.URI | ||
import java.net.http.HttpClient | ||
import java.net.http.HttpRequest | ||
import java.net.http.HttpResponse | ||
import java.nio.charset.StandardCharsets | ||
import java.time.LocalDateTime | ||
import java.util.Base64 | ||
|
||
class MinimalStsClient( | ||
private val baseUrl: URI, | ||
private val httpClient: HttpClient = HttpClient.newHttpClient(), | ||
private val objectMapper: ObjectMapper = jacksonObjectMapper() | ||
) : SamlTokenProvider { | ||
override fun samlToken(username: String, password: String): SamlToken { | ||
val body = requestSamlToken(username, password) | ||
return decodeSamlTokenResponse(body) | ||
} | ||
|
||
private fun requestSamlToken(username: String, password: String): String { | ||
val encodedCredentials = Base64.getEncoder() | ||
.encodeToString("$username:$password".toByteArray(StandardCharsets.UTF_8)) | ||
val request = HttpRequest.newBuilder(URI("$baseUrl/rest/v1/sts/samltoken")) | ||
.header("Authorization", "Basic $encodedCredentials") | ||
.GET() | ||
.build() | ||
|
||
val response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()) | ||
return response.body() ?: throw StsClientException("Tom responskropp fra STS") | ||
} | ||
|
||
private fun decodeSamlTokenResponse(body: String): SamlToken { | ||
val node = try { | ||
objectMapper.readTree(body) | ||
} catch (err: Exception) { | ||
throw StsClientException("Kunne ikke tolke JSON fra responsen til STS: $body") | ||
} | ||
return extractSamlTokenFromResponse(node) ?: handleErrorResponse(node) | ||
} | ||
|
||
private fun extractSamlTokenFromResponse(node: JsonNode): SamlToken? { | ||
val accessToken = node.path("access_token").takeIf(JsonNode::isTextual)?.asText() ?: return null | ||
val issuedTokenType = node.path("issued_token_type").takeIf(JsonNode::isTextual)?.asText() ?: return null | ||
val expiresIn = node.path("expires_in").takeIf(JsonNode::isNumber)?.asLong() ?: return null | ||
if (issuedTokenType != "urn:ietf:params:oauth:token-type:saml2") throw StsClientException("Ukjent token type: $issuedTokenType") | ||
try { | ||
return SamlToken( | ||
Base64.getDecoder().decode(accessToken).decodeToString(), | ||
LocalDateTime.now().plusSeconds(expiresIn) | ||
) | ||
} catch (err: Exception) { | ||
throw StsClientException("Kunne ikke dekode Base64: ${err.message}", err) | ||
} | ||
} | ||
|
||
private fun handleErrorResponse(node: JsonNode): Nothing { | ||
val title = node.path("title").asText() | ||
val errorDetail = node.path("detail").takeIf(JsonNode::isTextual) ?: throw StsClientException("Ukjent respons fra STS: $node") | ||
throw StsClientException("Feil fra STS: $title - $errorDetail") | ||
} | ||
} | ||
|
||
class StsClientException(override val message: String, override val cause: Throwable? = null) : RuntimeException() |
16 changes: 16 additions & 0 deletions
16
minimal-sts-client/src/main/kotlin/com/github/navikt/tbd_libs/soap/SamlToken.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package com.github.navikt.tbd_libs.soap | ||
|
||
import java.time.Duration | ||
import java.time.LocalDateTime | ||
|
||
class SamlToken( | ||
val token: String, | ||
val expirationTime: LocalDateTime | ||
) { | ||
private companion object { | ||
private val EXPIRATION_MARGIN = Duration.ofSeconds(10) | ||
} | ||
val isExpired get() = expirationTime <= LocalDateTime.now().plus(EXPIRATION_MARGIN) | ||
} | ||
|
||
|
5 changes: 5 additions & 0 deletions
5
minimal-sts-client/src/main/kotlin/com/github/navikt/tbd_libs/soap/SamlTokenProvider.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package com.github.navikt.tbd_libs.soap | ||
|
||
interface SamlTokenProvider { | ||
fun samlToken(username: String, password: String): SamlToken | ||
} |
60 changes: 60 additions & 0 deletions
60
minimal-sts-client/src/test/kotlin/com/github/navikt/tbd_libs/soap/InMemoryStsClientTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
package com.github.navikt.tbd_libs.soap | ||
|
||
import io.mockk.every | ||
import io.mockk.mockk | ||
import io.mockk.verify | ||
import org.junit.jupiter.api.Assertions.* | ||
import org.junit.jupiter.api.Test | ||
import java.time.LocalDateTime | ||
|
||
class InMemoryStsClientTest { | ||
private companion object { | ||
private const val USERNAME = "foo" | ||
private const val PASSWORD = "bar" | ||
} | ||
|
||
@Test | ||
fun `henter fra kilde om cache er tom`() { | ||
val kilde = mockk<SamlTokenProvider>(relaxed = true) | ||
val token = SamlToken("<samltoken>", LocalDateTime.MAX) | ||
every { kilde.samlToken(eq(USERNAME), eq(PASSWORD)) } returns token | ||
|
||
val cachedClient = InMemoryStsClient(kilde) | ||
val result = cachedClient.samlToken(USERNAME, PASSWORD) | ||
|
||
assertSame(token, result) | ||
verify(exactly = 1) { kilde.samlToken(eq(USERNAME), eq(PASSWORD)) } | ||
} | ||
|
||
@Test | ||
fun `henter fra cache om verdi ikke er utgått`() { | ||
val kilde = mockk<SamlTokenProvider>(relaxed = true) | ||
val token = SamlToken("<samltoken>", LocalDateTime.MAX) | ||
every { kilde.samlToken(eq(USERNAME), eq(PASSWORD)) } returns token | ||
|
||
val cachedClient = InMemoryStsClient(kilde) | ||
val result1 = cachedClient.samlToken(USERNAME, PASSWORD) // første kall | ||
val result2 = cachedClient.samlToken(USERNAME, PASSWORD) // andre kall | ||
|
||
assertSame(token, result1) | ||
assertSame(result1, result2) | ||
verify(exactly = 1) { kilde.samlToken(eq(USERNAME), eq(PASSWORD)) } | ||
} | ||
|
||
@Test | ||
fun `henter fra kilde om verdi er utgått`() { | ||
val kilde = mockk<SamlTokenProvider>(relaxed = true) | ||
val token1 = SamlToken("<samltoken>", LocalDateTime.MIN) | ||
val token2 = SamlToken("<nytt samltoken>", LocalDateTime.MIN) | ||
every { kilde.samlToken(eq(USERNAME), eq(PASSWORD)) } returns token1 andThen token2 | ||
|
||
val cachedClient = InMemoryStsClient(kilde) | ||
val result1 = cachedClient.samlToken(USERNAME, PASSWORD) // første kall | ||
val result2 = cachedClient.samlToken(USERNAME, PASSWORD) // andre kall, oppfrisker | ||
|
||
assertSame(token1, result1) | ||
assertNotSame(result1, result2) | ||
assertSame(result2, result2) | ||
verify(exactly = 2) { kilde.samlToken(eq(USERNAME), eq(PASSWORD)) } | ||
} | ||
} |
105 changes: 105 additions & 0 deletions
105
minimal-sts-client/src/test/kotlin/com/github/navikt/tbd_libs/soap/MinimalStsClientTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
package com.github.navikt.tbd_libs.soap | ||
|
||
import com.github.navikt.tbd_libs.mock.MockHttpResponse | ||
import io.mockk.every | ||
import io.mockk.mockk | ||
import io.mockk.verify | ||
import java.net.URI | ||
import java.net.http.HttpClient | ||
import java.net.http.HttpRequest | ||
import org.intellij.lang.annotations.Language | ||
import org.junit.jupiter.api.Assertions.assertEquals | ||
import org.junit.jupiter.api.Test | ||
import org.junit.jupiter.api.assertThrows | ||
import java.nio.charset.StandardCharsets | ||
import java.util.Base64 | ||
import kotlin.jvm.optionals.getOrNull | ||
|
||
class MinimalStsClientTest { | ||
private companion object { | ||
private const val USERNAME = "username" | ||
private const val PASSWORD = "password" | ||
private const val TOKEN = "<saml token>" | ||
|
||
private fun String.encodeBase64() = Base64.getEncoder().encodeToString(toByteArray(StandardCharsets.UTF_8)) | ||
} | ||
@Test | ||
fun `dekoder token fra base64`() { | ||
val base64Token = TOKEN.encodeBase64() | ||
val (httpClient, stsClient) = mockClient(tokenResponse(base64Token)) | ||
|
||
val result = stsClient.samlToken(USERNAME, PASSWORD) | ||
|
||
assertEquals(TOKEN, result.token) | ||
verifiserRequest(httpClient) { | ||
it.uri() == URI("http://localhost/rest/v1/sts/samltoken") | ||
&& it.headers().firstValue("Authorization").getOrNull() == "Basic ${"$USERNAME:$PASSWORD".encodeBase64()}" | ||
} | ||
} | ||
|
||
@Test | ||
fun `håndterer at token ikke er saml`() { | ||
val base64Token = TOKEN.encodeBase64() | ||
val (_, stsClient) = mockClient(tokenResponse(base64Token, "jwt")) | ||
assertThrows<StsClientException> { stsClient.samlToken(USERNAME, PASSWORD) } | ||
} | ||
|
||
@Test | ||
fun `håndterer at token ikke er base 64`() { | ||
val (_, stsClient) = mockClient(tokenResponse(TOKEN)) | ||
assertThrows<StsClientException> { stsClient.samlToken(USERNAME, PASSWORD) } | ||
} | ||
|
||
@Test | ||
fun `håndterer feil fra server`() { | ||
val (_, stsClient) = mockClient(errorResponse()) | ||
assertThrows<StsClientException> { stsClient.samlToken(USERNAME, PASSWORD) } | ||
} | ||
|
||
@Test | ||
fun `håndterer ugyldig json i response`() { | ||
val (_, stsClient) = mockClient("Internal Server Error") | ||
assertThrows<StsClientException> { stsClient.samlToken(USERNAME, PASSWORD) } | ||
} | ||
|
||
private fun verifiserRequest(httpClient: HttpClient, sjekk: (HttpRequest) -> Boolean) { | ||
verify { | ||
httpClient.send<String>(match { | ||
sjekk(it) | ||
}, any()) | ||
} | ||
} | ||
|
||
private fun mockClient(response: String): Pair<HttpClient, MinimalStsClient> { | ||
val httpClient = mockk<HttpClient> { | ||
every { | ||
send<String>(any(), any()) | ||
} returns MockHttpResponse(response) | ||
} | ||
val stsClient = MinimalStsClient(URI("http://localhost"), httpClient) | ||
return httpClient to stsClient | ||
} | ||
|
||
private fun tokenResponse(token: String, tokenType: String = "urn:ietf:params:oauth:token-type:saml2"): String { | ||
@Language("JSON") | ||
val response = """{ | ||
"access_token": "$token", | ||
"issued_token_type": "$tokenType", | ||
"token_type": "Bearer", | ||
"expires_in": 3600 | ||
}""" | ||
return response | ||
} | ||
|
||
private fun errorResponse(): String { | ||
@Language("JSON") | ||
val response = """{ | ||
"type": "about:blank", | ||
"title": "Method Not Allowed", | ||
"status": 405, | ||
"detail": "Method 'POST' is not supported.", | ||
"instance": "/rest/v1/sts/samltoken" | ||
}""" | ||
return response | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,2 @@ | ||
rootProject.name = "tbd-libs" | ||
include("azure-token-client", "azure-token-client-default", "mock-http-client") | ||
include("azure-token-client", "azure-token-client-default", "mock-http-client", "minimal-sts-client") |