Skip to content

Commit e6ad817

Browse files
authored
feat: load some signing keys from file on startup (#89)
generation of keys on endpoint access can sometimes lead to read/connect timeout for clients depending on machine hardware/os. This PR will load some RSA keys from file on startup to remediate this. * move logic for key generation and caching into KeyProvider.kt * allow users to bring their own keys by creating a KeyProvider in the TokenProvider constructor * load up to 5 keys from file, new keys will be generated and cached on the fly if keys all keys are in use, i.e. if number of issuers > 5
1 parent 5450beb commit e6ad817

File tree

5 files changed

+219
-27
lines changed

5 files changed

+219
-27
lines changed

src/main/kotlin/no/nav/security/mock/oauth2/OAuth2Config.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,20 @@ import java.io.File
1919

2020
data class OAuth2Config @JvmOverloads constructor(
2121
val interactiveLogin: Boolean = false,
22+
@JsonDeserialize(using = OAuth2TokenProviderDeserializer::class)
2223
val tokenProvider: OAuth2TokenProvider = OAuth2TokenProvider(),
2324
@JsonDeserialize(contentAs = RequestMappingTokenCallback::class)
2425
val tokenCallbacks: Set<OAuth2TokenCallback> = emptySet(),
2526
@JsonDeserialize(using = OAuth2HttpServerDeserializer::class)
2627
val httpServer: OAuth2HttpServer = MockWebServerWrapper()
2728
) {
2829

30+
class OAuth2TokenProviderDeserializer : JsonDeserializer<OAuth2TokenProvider>() {
31+
override fun deserialize(p0: JsonParser?, p1: DeserializationContext?): OAuth2TokenProvider {
32+
return OAuth2TokenProvider()
33+
}
34+
}
35+
2936
class OAuth2HttpServerDeserializer : JsonDeserializer<OAuth2HttpServer>() {
3037
enum class ServerType {
3138
MockWebServerWrapper,
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package no.nav.security.mock.oauth2.token
2+
3+
import com.nimbusds.jose.jwk.JWKSet
4+
import com.nimbusds.jose.jwk.KeyUse
5+
import com.nimbusds.jose.jwk.RSAKey
6+
import java.security.KeyPairGenerator
7+
import java.security.interfaces.RSAPrivateKey
8+
import java.security.interfaces.RSAPublicKey
9+
import java.util.concurrent.ConcurrentHashMap
10+
import java.util.concurrent.LinkedBlockingDeque
11+
12+
open class KeyProvider @JvmOverloads constructor(
13+
private val initialKeys: List<RSAKey> = keysFromFile(INITIAL_KEYS_FILE)
14+
) {
15+
private val signingKeys: ConcurrentHashMap<String, RSAKey> = ConcurrentHashMap()
16+
17+
private val generator = KeyPairGenerator.getInstance("RSA").apply { this.initialize(2048) }
18+
19+
private val keyDeque = LinkedBlockingDeque<RSAKey>().apply {
20+
initialKeys.forEach {
21+
put(it)
22+
}
23+
}
24+
25+
fun signingKey(keyId: String): RSAKey = signingKeys.computeIfAbsent(keyId) { keyFromDequeOrNew(keyId) }
26+
27+
private fun keyFromDequeOrNew(keyId: String): RSAKey = keyDeque.poll()?.let {
28+
RSAKey.Builder(it).keyID(keyId).build()
29+
} ?: generator.generateRSAKey(keyId)
30+
31+
private fun KeyPairGenerator.generateRSAKey(keyId: String): RSAKey =
32+
generateKeyPair()
33+
.let {
34+
RSAKey.Builder(it.public as RSAPublicKey)
35+
.privateKey(it.private as RSAPrivateKey)
36+
.keyUse(KeyUse.SIGNATURE)
37+
.keyID(keyId)
38+
.build()
39+
}
40+
41+
companion object {
42+
const val INITIAL_KEYS_FILE = "/mock-oauth2-server-keys.json"
43+
44+
fun keysFromFile(filename: String): List<RSAKey> {
45+
val keysFromFile = KeyProvider::class.java.getResource(filename)
46+
if (keysFromFile != null) {
47+
return JWKSet.parse(keysFromFile.readText()).keys.map { it as RSAKey }
48+
}
49+
return emptyList()
50+
}
51+
}
52+
}

src/main/kotlin/no/nav/security/mock/oauth2/token/OAuth2TokenProvider.kt

Lines changed: 5 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,23 @@ import com.nimbusds.jose.JWSAlgorithm
55
import com.nimbusds.jose.JWSHeader
66
import com.nimbusds.jose.crypto.RSASSASigner
77
import com.nimbusds.jose.jwk.JWKSet
8-
import com.nimbusds.jose.jwk.KeyUse
9-
import com.nimbusds.jose.jwk.RSAKey
108
import com.nimbusds.jwt.JWTClaimsSet
119
import com.nimbusds.jwt.SignedJWT
1210
import com.nimbusds.oauth2.sdk.TokenRequest
1311
import no.nav.security.mock.oauth2.extensions.clientIdAsString
1412
import no.nav.security.mock.oauth2.extensions.issuerId
1513
import okhttp3.HttpUrl
16-
import java.security.KeyPairGenerator
17-
import java.security.interfaces.RSAPrivateKey
18-
import java.security.interfaces.RSAPublicKey
1914
import java.time.Duration
2015
import java.time.Instant
2116
import java.util.Date
2217
import java.util.UUID
23-
import java.util.concurrent.ConcurrentHashMap
24-
25-
class OAuth2TokenProvider {
26-
private val signingKeys: ConcurrentHashMap<String, RSAKey> = ConcurrentHashMap()
2718

19+
class OAuth2TokenProvider @JvmOverloads constructor(
20+
private val keyProvider: KeyProvider = KeyProvider()
21+
) {
2822
@JvmOverloads
2923
fun publicJwkSet(issuerId: String = "default"): JWKSet {
30-
return JWKSet(rsaKey(issuerId)).toPublicJWKSet()
24+
return JWKSet(keyProvider.signingKey(issuerId)).toPublicJWKSet()
3125
}
3226

3327
fun idToken(
@@ -88,10 +82,8 @@ class OAuth2TokenProvider {
8882
builder.build()
8983
}.sign(issuerId, JOSEObjectType.JWT.type)
9084

91-
private fun rsaKey(issuerId: String): RSAKey = signingKeys.computeIfAbsent(issuerId) { generateRSAKey(issuerId) }
92-
9385
private fun JWTClaimsSet.sign(issuerId: String, type: String): SignedJWT {
94-
val key = rsaKey(issuerId)
86+
val key = keyProvider.signingKey(issuerId)
9587
return SignedJWT(
9688
JWSHeader.Builder(JWSAlgorithm.RS256)
9789
.keyID(key.keyID)
@@ -127,18 +119,4 @@ class OAuth2TokenProvider {
127119
builder.addClaims(additionalClaims)
128120
builder.build()
129121
}
130-
131-
companion object {
132-
private fun generateRSAKey(keyId: String): RSAKey =
133-
KeyPairGenerator.getInstance("RSA").let {
134-
it.initialize(2048)
135-
it.generateKeyPair()
136-
}.let {
137-
RSAKey.Builder(it.public as RSAPublicKey)
138-
.privateKey(it.private as RSAPrivateKey)
139-
.keyUse(KeyUse.SIGNATURE)
140-
.keyID(keyId)
141-
.build()
142-
}
143-
}
144122
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
{
2+
"keys": [
3+
{
4+
"p": "5riKsMcpImce_BGr13BQv6kA2RuRfW8aiqad7eu3WrdUPBZBGeI7LdISOYtikNij1GSuAnqLVyjPtKL1WDmNiKTo8ArvtZmlCNlEqQzzbUUIQFxUAJ9cMR_mgmoo-jfmia_0wPIVdMpev9Xzg30mAObNT0pj3F968e5ThSRave8",
5+
"kty": "RSA",
6+
"q": "tEM-X38gszZOAMwVZXQa_GEon3f9sngC6K1lX_TDbTdPoIHdberli70W2-505FEviEvtWf0VZGUbsxVIdWtN7rV9p-Q3__KnFYvj5dtIbZR1-Njgnleya9rmv_7pP3c_p66S0-jeEaXnLHorK3HOsvyzfuRM389BKwapKNdOGbs",
7+
"d": "V19sUxBYchl1hbCrlYse7zLds_-lUCPI0xVaMhrw4OEmJhOqJqjNZtkDAcb7DsSyDW-L_Ai5XdXvOZoj8ZdqRO5eFEJtPpyRAmL7_4ZO2DT0ztyOCJu7xc2nBAFiTw25fk9aP2JxFEZTvCPoFMBFThsljS6A7_nvleVa8pD55s3LxBRojqmJZAPeO8he0ju2O2rAWsT8YJX7cgMUDBfE3eTR4riwQrIPh7fihcn0zVbf7DXDSMLaCeedc9EI17A00dwrPQx9eiqByatWMRTodzykTjAJGF06gtivBuVzJTTXUto3n9rBRp9pjxwp4o-u8-aZxJaZLIrDLJLgGv60aQ",
8+
"e": "AQAB",
9+
"use": "sig",
10+
"kid": "initialkey-1",
11+
"qi": "HqUVnJoG53wI1I3a922NDUWmVYxBa4fAA0nketVgSDbF5XkZTQW3JsC1K0oPqD-HtSz8bQu0JuOaG3ya-DVEGjeC8lpY_WX9rqoyBe-1AtqXOciU98iQwDIs5gARlTVj6q-jr867keNUdF8f9-GCFWqXWo0QFOeyXnOIwNRNJBo",
12+
"dp": "V03XdUM3poP87odFTjV66Ltrzbun1x4WngakViZO8G3U2xPUuJWprRIDwXCj8Il5nOIoEloKpgWUaxcK3cKRcvZsCoEo6b_k-i800v9KkbuAftIxGzcyLIiUsh63uM80Vj-VzvkL83CsX8z243eUzNyJLgrJNNeQb0Guqv_xslM",
13+
"dq": "pKUijDDeWjHIjzvYcyDYIRpQ806yftbUuVbe2AUElnXFmfHjoKjC3p5oCZtEUuHAS3omIWJirp8W7LwMwYqv1M8aJUXyzCkC6VAraN-fyM6n7hGRH68z_QUka8rVmi0-C9cMrtbsNixU-K_hTD4XsC3VeJnniSiQ-k6MJQx6fk8",
14+
"n": "onZcB1ryWS1keTIcbgsLKJ1UBwL1Wbzse5P2HjkrNwbG3Jy2lefUEcTVJxN8bpLeW460Luz3ScZd3d9p8IoHjmhZ2cyO49E41aBRIlBRzWNpebK5xeC95rSKenYHpOPlLzPgybg2qxallzQUOcKCheiF0fsErlapaA9YmKwzP3DwvzYW4JqSrHhDGWPwUCcsR4dpetwKXP_9tRFso06ryr4um3qiq7giyZEyZVG3fHMplD-5e-2-RrzBiGFW_zvs-XVRGPIf9Y5YNjeQJRuS4vF82V8mNZxEZddtUY5plSz-vgX3GSvANLDH-LZJ76Zmx3a8dEZbI7VxgsBQAqcUlQ"
15+
},
16+
{
17+
"p": "8X4E3UUItsFp4JSzLVBvDz7VKZAvQTbngQfvIRokECLdsP--1YGkniit-e9KHR8UtVI3h-cqMmHvffHoqwOdKoxi5BViuwmqCdIfjBvpfQB3PZxOEAFojiRG0apxnXtmxUKG3PIIsFGXG8YhHwLIqwTvwaKr1BF3g6qW2pSzFFs",
18+
"kty": "RSA",
19+
"q": "3YFMc2Ux4sFcBHqT440beIBmXbEeUZ3jc-728PFXGZRVeTNn9HZSascgO6nw9iECz-jRQwTX4P9RIAnWm782AV5QIGXmz3t-4ssimLHtP9jkyhqTDMmuUUv1QKhdD2p4q_uzbrkx4A3OMphgGDbLe4UKBA-9X10BozoRFXawNds",
20+
"d": "EFVtvj1zJ5ixPHVH-4bzDNQK_WJyi8RkxkbVFXmfMdtylh3tWfaNRnuKjiwlCHWT7x0Asof4SYY9YAPvoaq8g79zp4sRL_XmNawLk9QRi0hHHXowwlVK39qQHZ15Q8Hs3s3VDXx6NlVqWBFjTBz2eX-2vwesvPjPQUd2iCXRz8UXtsKRuToE_ddAFLC_Gcz_yPSQCFfyptPhBCPVsI6opsdK_SELuGspvI3ux2Gbq5aWGaByWo5-2NpPfXdiEPcY0mREKfe3kQU0Wa5-vqYA43ekpFaCKSsj-vmmhkP0FwQxBtVs_CVxRfgzyn1_mF2oZsPuvUeJXbHFF-PdjBmz7Q",
21+
"e": "AQAB",
22+
"use": "sig",
23+
"kid": "initialkey-2",
24+
"qi": "5OHgZtRG6CecHV-2AUlRZtHraN_G3nftrMAGuczh97RdjlEUxia_LkQqc_OUJf8M_57I5WZ4z2H7u1JVzscM3D7nFKJLq2JoW5kc2zffYhZm83mcTWyvQSf6WPvmqxqX2TZr_JFBLn0_33DQUZwOj4tAmvpXjYFCqWXbNjZxEs4",
25+
"dp": "669a3fzXAU4IoCdgK5R5n35qGbNfex0zmYl9x2e01I7CoFEpFUT-vWDkUq5IPd2snz4LdjaUxzEvxFJJCkZvqCv1A7cfcX2AFy-cnGhNWzMOLPIUeah2O2uKNmxLkC_0YAaKiq4o7rPibzfR8WsNH2Ok_u1dF46ofrcJnXBMyks",
26+
"dq": "FhPBDu9THYqwJTIic1epGUWS7lus7e2SsgdrTXCAgegq7L2W6uKwLDxUlh3GCoIXyakm0ks1SROpfkv8u-E-_LvtuIzviFaCuxAMDrQNNYPkqdAkP-4KFchAVYVyYQr3pAyeQbbrpa06lAhj64XqmhEUgnsfINYgR6iN81m1Dmk",
27+
"n": "0PPC0byb9f-Kueq8B733sZnANXDHyy2M5qUr1vWW733l_lOf2dFKDu6csaGEALro_39EFjhoad1D7Ebw1srj5APElaX9QMQxjK4pEdJlNU9SygwBObgAqCxfWmBjNBQ4NBrG8wi61MG4aoYwi-5W-N0VLL6tOxe_V6JyA4P4e-EzrlRJm9_dHT6ev3c6KyaRcGVnOBuArj3uhOh15-tbjuux8P30kR6RytxRWRZzAQqpkekpBFYYvoFyP3N_WGU5ruOEUYF8KloDmFqANSpqXvUyI3kl-McTtqzH0BuZG6fG8bH3ZZdH1fM2BJ8z0fO7n24HhNn3lAIPo8q9OPlA2Q"
28+
},
29+
{
30+
"p": "xMkiP2irzMOFmQCYOWbnBOHg5wFPxFypGBAk8jmGCxe7njvjZPZ4k5xQf6XMBZkFANCro920ApwvdC9DweMyMOblFa7wRWBkOEgvxV9sFXRBOnvYQeVXB-tWp6p3DUqXC1APRrRkGnJAOEWhgtQrDLTDrxXz7m0NBC7xDeruSy0",
31+
"kty": "RSA",
32+
"q": "sqPpmS2vApCSSua3rdAAaNzyqGP3wGvigungzZpX40_JlSlfIiYE6KLPuCZnuFIjCcG2dxZ7FnZhbupDpb4f720bYdGoFQs1j4z8Pm6VHJlZJ2anq4T2ZS9gOiUd51eQWT-G1vmvgs-8XBb80gePsE_MEvZbbaGuts3UC_nWpPk",
33+
"d": "MhekjcELeJ9CayS_MZhD9uN751nmSJ3jgV_Us-QHV-1DornrAkhptQGQn4qfs-92uj6_4LY6M3xLl8FZHO9z-OPSfxK0tgOBECpgiWwm_qSl825S68OYYnRPE84NAKNklvmv5ihFTZRtgIJNvFsIdKS5ZUY31zhNNjs9P4txxYZ0G_Q--l7koie4CXI8Boy7ZxbiCxzh04K40QWW1Ui378PyScQo65aqv9oDibrCpgQHW3eWq0rG0e3PyngBW8j6xDqYHTa5cto5oLtizV-0sRK--cTyIyD7DJbFwCOKyis1Lqq8OTTMvqyaSXPEJY3u8vEVdzM8rjqg61EeMs8j4Q",
34+
"e": "AQAB",
35+
"use": "sig",
36+
"kid": "initialkey-3",
37+
"qi": "fLmBa9vAltNdT1Tdo9r24tStgsosNln8b0XQtNA-v5aSSZrGYQGUst5iC70J_qT0sLVALnJJoS0JeSkjBiC_PnnmtZbTOGkzgKsuimhNsIWUILrFiDgaR7ujEiNuDc-pucVPRQ2xLstvCmPR1ocSYpACR0dKZEgOfh3ghPL61zo",
38+
"dp": "Y7WiU8y-mD9N10vU1ekND41APuyMNWvaBiZQAighgkdhOnkP7F1ylSC0LSmeKgvx3ArfnWU9y8DFzrIQPBLZoKut0gHVHuILhfUVt4V1J53DW1XbKvCA27NkMgqOzj5IMGQ9iU7oFfpkDd9CSh8lPQfuyy1tbxb0bHU4kRvD6HU",
39+
"dq": "F3rf61hL1oR2Fg45OklKpH3WDzgEinAjt51SBPQydRg5oLdtX6mrn4A22TeDDoENRe0GNKTpzMwGhnOYLKLOw8ONg8_wzcNJaPLY_MPAKaAmTb16cFrrn-UYOsxCH_Qsbu6gpITxArqXQWtsE5cW1c_HPP7QiZpkwnZPVruh8NE",
40+
"n": "iVHZcbSmKmmCCJQ52WFtyB5w6nE-34Ykds-GZSg47JI52Gr4wLKnpAfQH_zob_SikXQ9B98ivrI8-QoAAMbsrOnGJIcXgxoxZ2cYUOsR7Ft_M_22VCvyqH1YP0ahA-mnjfYHg_VfKW1PTdBqYcYT2XaX4nD7qDzIvarzQUYOYXlMMNaNAUj4Whq4IV_qvDJU7sUYRyrPOFKvLiH5d-EHPeV9Nut01Gt6Ux2OSOQheE7UizKNQx5cvrTXlKur5zS0xxCbkLsIh81xINYAfkmwwqwKZ5R0NdrqPvsjt_k3IMRjkE2OZ3eeBermYwtAJvSDpyIZn1ntYgvOwQpKxo3yxQ"
41+
},
42+
{
43+
"p": "4xriuf47Gk2xcOlFKftdFlcnpZ-NwETqqhW2pQpOLvYhaInGBKiK5WLjGy29YoAh2kWOBj1zW4Ug19ohYGmh3yu3Q3a3Lem6uJrN0NOkt0w0zfwta6UKaPXJ2jDMCHOyShBmeBSJcrCIy6DAA9bhuU7z31zd2K4MgMv59kyOo3E",
44+
"kty": "RSA",
45+
"q": "v9iN0HnKObqTLZbY3S9fM9u4lLG5Q4zCmYvleTreZ8LFb38xDAAEzYNu09nYPPGxUDSD9vHUbYoU9zKTRiWXlf-qJw9fiEsTpWFZPRc6AYqosdVSRjoClAgczoOUY_2U8PCyUIpTzn4mquC8h5D6ZXczPaukAudB0VM2EQEqf0U",
46+
"d": "KKGAiF0lQJnAPD5O7-l2BnVHJR_95m2QbSxmyIalA8jVwxtXMMlZdymla1K5b8NflgYlJKB1MeHtGZMtdg_U8MCfuQXAaQc3IQg8oUQ3dwHBJrFYXsoS2P7S6XQ-geHAhzlMtYJDKc28SudRnGJP-ZkiTLWVQpSbRoAGnCz36mK8j1MlD6d_14UhyU_hqFCCEcMhQzFm4FZRN8U2sj0d0Dppm2pko_D6xswZxcRL9kjUNAKR-sdWcLGdCqutrvwmtO9r9wNJa3STVcn7Ya_--rrX2JPYFKEKwWDH6r0ZX4NIeI8Gdiu9j5iztppVMsqmg-lpcU-Tc4rgJKhhalEEgQ",
47+
"e": "AQAB",
48+
"use": "sig",
49+
"kid": "initialkey-4",
50+
"qi": "1vc5qZgIg3ptsg8WmyUHmpVCtY_PG8rnZSd0dIuC9u_c841FtRNMbUCNiTujthYmtlcaf6qIQiqmHBPWuEI00ywLcyUX-WDEzEkHUKqpuLGVM65MxlGV0LOO8hgdeGXfyrUYJ2ACG0yD5G6uj9I9Sl_aAyf0MQFfEaRTfE7CtLQ",
51+
"dp": "bukEbR0RtCjZTXE-y9_seCqcPDCNw6ZkjCgKiNNdl2WwryMJx-Cf5KLEktNluCMnZTeuwrFkEwATKBdpUXKFET6CQ7pIf220OM-xUBjsSnA3IZnUfMufJ99RcvN90WrfWXhk8qPk9FPumrOo0rcwiZVbWGw8E8P8azIyouyEhKE",
52+
"dq": "e9kNE_zLtCDiSpgLQB8I0q2Rp0xkUVtZdU5-wZhjY5C1bJkrzJdmglXLAjCsDAvrb9-3IYBUprJxfnPD55D1HvyBl92wyofNEwKZXXrVE5Gz_bm892ETsQTbs-X1sedOc4yvUJc8Kx39UGrsyoepXj9pcPKRWt53-u5BBRE_ohE",
53+
"n": "qjErptJgXjqW9K-27k2FrSGiQJVFYbzlovJJtayk-ANMNpoJbTV5pMA6_ArxN3r_unUB6R6Psl0_LpIhM9LCUyUExZybnY9d0uVbbDtwVC-mFOs0dXQqEV7_DjicpuWQ0ds3lB_zG5nesokwAFNzvk7tvMhvIilkvh14Q5nWTI78rmMY_rjYDsNp0nIL4eUBOoUscqJQ6Z4bDhB1sygS3dFwjxFxCUEsRPjmpm2Qbw5nhVcJWO1KawB_PYqr54LaRY_VX_P3RhWWaDqTuHb_b9jcpZxfL3jWJ79Yjjyw2IAjJVjGnHqN-Yu_7Vh6vB8pDv7Jb2iCVJVLRl3opxEcdQ"
54+
},
55+
{
56+
"p": "3rAPTm2gdVCEvFG9lPT2CEZhwjpESbuSbmIuL1sMazR9Y8imX3JPE4vFiBRxE63Ol0R_p_sQ4W9oO7HSdj-027GxARVdIwVx_0UboTY_ljfuMq2y5yKyXXym-DmppMAh1KS-xRLqblbGX6QmN8PmhSH2r62kLHcrL0HTZNhqchM",
57+
"kty": "RSA",
58+
"q": "y2HAJ4E60bF-S-eh-oFEWtLRiVK9YtBjtz4Syfa0svcRjgXTaH7ebqBO1qG3qHU43Kn9ps4TJTZx8qt7OXQTbQalfVX6Dd7HdeYOal6hcNFKIKsvd1epFZcmlKvilItTgu_m_GvfxXj7zLqVk_wHY14E2V0E6cFD9n6rqX7WP8M",
59+
"d": "eU3pdM0zBkvLf8NCmZebg7q15564pHpeF85MOwNk302g7uIeY8WxfOiL9Mj8Z6qTFuOIFD7E9UWor5fOG2z2Pz54PTRMabb1dXO90rCt6JMkPB86e7WGx_VSjV_ibKfpjZ6y3LgCprSzZnnr3FUMWgcZlBJ92cRheZEcEi0XNFJNTF2M0YdoHfhoayzrTJaF6qgwfjhqJTUFvaP_cSrPAdhNFJ7hM_yzoOlpq33V6GbylvJ2C2GpAWd8khu5DyOmySNt79HYCzSdhCJY-oN2GFewZhRUk8vT8AsGZNONFCJtHtaqs6kIhrtQhJNE6i3pfJHLwOq5CJHVun2RmHbrIQ",
60+
"e": "AQAB",
61+
"use": "sig",
62+
"kid": "initialkey-5",
63+
"qi": "DY7KQvVnUgQJqIkTSObp6P4DMfHeiom_U4vgwKoFDNzYgMvLh_VvfPdhqzSS6IlPD0_Gyc7hwK5SpitLp-WmtNnsqfFKy0VQMFkiBfdJEPWIDMMUrZZ6nsl9KjV7MiDrkYonw71eSFNh2NEX30DWKTTk9KojpcU0HvM_912rH8o",
64+
"dp": "fmNWjKDTzUGh1HBgNUbCzPeFTINddquq9FNs-xul9MKZ2CRtqQZrsyBFQHK5qv2en2QVP_XTIt_kPN00IkEOGRLE72R8s__HL6a9g8YSWOPtoX3MaDrdGQpCiefQTN1vVg0a6SdPPsipVmcH-eaJ003vgM4Au-v26p9lp3rdD1k",
65+
"dq": "dOYRkWNZEJApnK1dz-OfC2kjYP_6tSI8PmXiXM19nWQfZfd5RRWu-f0Qc5NuQdhmv4bBsa-_F2OM6UOhRyutwrvQQRM679_924lI_eC4gGT7a32ZgcoT-MHxPgDx8hmG_bqwlKPYceORL2KLeQyinn264cjyev1H-BVky76InQs",
66+
"n": "sOqj_2Zc0AB3St_KkSwChpNbNfFTd76_-mtPCT_xP68O-YAB20n_7HWi6zMwRMStA7q3_R9FnhJ-AHVj92dUhZB5BmgPHrEadYwS0F1XyVCbC4zprz1QVgZwxSzkIiZRtB4VNlfGkSfYaaE3Iw2V5Ns9owCx0OMfkoqVO_wK8kBLfe7HMI43xUMaSuekNRxGkxruEMWPuqwOq7-0NciX4meykz_jHybl1UlAtHhRhlI25LgsumsDP0N-3FNDv8uwwGrcalYwAj1tScpdawUgiN2WjxnZn_WtTMRVy2XzbNEvTHoOqygRLCmsBxErCNJDtiU9N__3M7XqzSO-wQmReQ"
67+
}
68+
]
69+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package no.nav.security.mock.oauth2.token
2+
3+
import com.nimbusds.jose.jwk.JWKSet
4+
import com.nimbusds.jose.jwk.KeyUse
5+
import com.nimbusds.jose.jwk.RSAKey
6+
import io.kotest.assertions.asClue
7+
import io.kotest.matchers.collections.shouldBeIn
8+
import io.kotest.matchers.collections.shouldNotBeIn
9+
import io.kotest.matchers.shouldBe
10+
import no.nav.security.mock.oauth2.token.KeyProvider.Companion.INITIAL_KEYS_FILE
11+
import org.junit.jupiter.api.Test
12+
import java.io.File
13+
import java.security.KeyPairGenerator
14+
import java.security.interfaces.RSAPrivateKey
15+
import java.security.interfaces.RSAPublicKey
16+
17+
internal class KeyProviderTest {
18+
19+
private val initialKeysFile = File("src/main/resources$INITIAL_KEYS_FILE")
20+
21+
@Test
22+
fun `signingKey should return a key from initial keys file until deque is empty`() {
23+
val provider = KeyProvider()
24+
val initialPublicKeys = initialPublicKeys()
25+
26+
for (i in initialPublicKeys.indices) {
27+
provider.signingKey("issuer$i").asClue {
28+
it.toRSAPublicKey() shouldBeIn initialPublicKeys
29+
it.keyID shouldBe "issuer$i"
30+
}
31+
}
32+
33+
provider.signingKey("shouldBeGeneratedOnTheFly").asClue {
34+
it.toRSAPublicKey() shouldNotBeIn initialPublicKeys
35+
it.keyID shouldBe "shouldBeGeneratedOnTheFly"
36+
}
37+
}
38+
39+
@Test
40+
fun `signingKey should return a key from provided constructor arg until deque is empty`() {
41+
val initialKeys = generateKeys(2)
42+
val provider = KeyProvider(initialKeys)
43+
val initialPublicKeys = initialKeys.map { it.toRSAPublicKey() }
44+
45+
for (i in initialPublicKeys.indices) {
46+
provider.signingKey("issuer$i").asClue {
47+
it.toRSAPublicKey() shouldBeIn initialPublicKeys
48+
it.keyID shouldBe "issuer$i"
49+
}
50+
}
51+
52+
provider.signingKey("shouldBeGeneratedOnTheFly").asClue {
53+
it.toRSAPublicKey() shouldNotBeIn initialPublicKeys
54+
it.keyID shouldBe "shouldBeGeneratedOnTheFly"
55+
}
56+
}
57+
58+
private fun initialPublicKeys(): List<RSAPublicKey> =
59+
initialKeysFile.readText().let {
60+
JWKSet.parse(it).keys
61+
}.map {
62+
it.toRSAKey().toRSAPublicKey()
63+
}
64+
65+
private fun writeInitialKeysFile() {
66+
val list = generateKeys(5)
67+
initialKeysFile.writeText(JWKSet(list).toString(false))
68+
}
69+
70+
private fun generateKeys(numKeys: Int): List<RSAKey> {
71+
val list = mutableListOf<RSAKey>()
72+
for (i in 1..numKeys) {
73+
val key = KeyPairGenerator.getInstance("RSA").apply { this.initialize(2048) }
74+
.generateKeyPair()
75+
.let {
76+
RSAKey.Builder(it.public as RSAPublicKey)
77+
.privateKey(it.private as RSAPrivateKey)
78+
.keyUse(KeyUse.SIGNATURE)
79+
.keyID("initialkey-$i")
80+
.build()
81+
}
82+
list.add(key)
83+
}
84+
return list
85+
}
86+
}

0 commit comments

Comments
 (0)