Skip to content

Commit a6a49d3

Browse files
committed
allowing to override json and cryptography provider instance if needed
1 parent b051018 commit a6a49d3

File tree

11 files changed

+209
-57
lines changed

11 files changed

+209
-57
lines changed

docs/usage.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -767,6 +767,67 @@ val jwe = parser.parseEncrypted(token)
767767

768768
---
769769

770+
## Customising the `Json` Instance
771+
772+
By default, KJWT uses an internal `Json` configured with `ignoreUnknownKeys = true` and
773+
`explicitNulls = false`. This handles the most common use cases. If your application needs
774+
different serialization behaviour — for example, `encodeDefaults = true` or custom serializers
775+
registered via a `SerializersModule` — you can supply your own `Json` instance.
776+
777+
### Builder and parser
778+
779+
Pass a custom `Json` to `Jwt.builder()` or `Jwt.parser()`. The instance propagates automatically
780+
to every JSON operation performed by that builder or parser (claim serialization, payload and
781+
header encoding/decoding, etc.):
782+
783+
```kotlin
784+
val customJson = Json {
785+
ignoreUnknownKeys = true
786+
explicitNulls = false
787+
serializersModule = mySerializersModule
788+
}
789+
790+
// builder — affects claim/payload/header serialization
791+
val jws = Jwt.builder(customJson)
792+
.subject("user-123")
793+
.payload(UserClaims(role = "admin"))
794+
.signWith(signingKey)
795+
796+
// parser — affects payload/header deserialization
797+
val parser = Jwt.parser(customJson)
798+
.verifyWith(signingKey)
799+
.build()
800+
```
801+
802+
### Per-call overrides
803+
804+
Methods that directly perform JSON serialization also accept an optional `jsonInstance` parameter,
805+
so you can override the instance for a single call without rebuilding the whole builder or parser:
806+
807+
```kotlin
808+
// Deserialize the payload with a custom Json
809+
val claims: UserClaims = jws.getPayload<UserClaims>(jsonInstance = customJson)
810+
811+
// Deserialize the header with a custom Json
812+
val header: MyHeader = jws.getHeader<MyHeader>(jsonInstance = customJson)
813+
814+
// Read a custom claim with a custom Json
815+
val role: String = jws.payload.getClaim(String.serializer(), "role", jsonInstance = customJson)
816+
817+
// Set a header parameter using a custom Json (JwtHeader.Builder)
818+
headerBuilder.extra("x-meta", MyMeta.serializer(), meta, jsonInstance = customJson)
819+
headerBuilder.takeFrom(MyHeader.serializer(), myHeader, jsonInstance = customJson)
820+
821+
// Set a payload claim using a custom Json (JwtPayload.Builder)
822+
payloadBuilder.claim("meta", MyMeta.serializer(), meta, jsonInstance = customJson)
823+
payloadBuilder.takeFrom(UserClaims.serializer(), claims, jsonInstance = customJson)
824+
```
825+
826+
All `jsonInstance` parameters default to the library's built-in `JwtJson`, so existing code
827+
requires no changes.
828+
829+
---
830+
770831
## API Stability Annotations
771832

772833
KJWT uses two opt-in annotations to communicate the stability of its API surface.

lib/build.gradle.kts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ description = "Kotlin Multiplaftorm JWT"
88
kotlin {
99
sourceSets {
1010
commonMain.dependencies {
11-
implementation(libs.kotlinx.coroutines.core)
12-
implementation(libs.kotlinx.serialization.json)
13-
1411
api(libs.cryptography.core)
15-
implementation(libs.cryptography.bigint)
12+
api(libs.cryptography.bigint)
13+
api(libs.kotlinx.serialization.json)
14+
15+
implementation(libs.kotlinx.coroutines.core)
1616
implementation(libs.cryptography.serialization.asn1)
1717
implementation(libs.cryptography.serialization.asn1.modules)
1818
}

lib/src/commonMain/kotlin/co/touchlab/kjwt/Jwt.kt

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package co.touchlab.kjwt
22

33
import co.touchlab.kjwt.builder.JwtBuilder
4+
import co.touchlab.kjwt.internal.JwtJson
45
import co.touchlab.kjwt.parser.JwtParserBuilder
6+
import kotlinx.serialization.json.Json
57

68
/**
79
* Entry point for the KJWT library.
@@ -45,7 +47,27 @@ import co.touchlab.kjwt.parser.JwtParserBuilder
4547
* `cryptography-provider-optimal` to your app dependencies — it auto-registers on startup.
4648
*/
4749
public object Jwt {
48-
public fun builder(): JwtBuilder = JwtBuilder()
50+
/**
51+
* Creates a new [JwtBuilder] for constructing JWS or JWE tokens.
52+
*
53+
* @param jsonInstance the [Json] instance to use for all serialization within this builder;
54+
* defaults to the library's internal configuration (`ignoreUnknownKeys = true`,
55+
* `explicitNulls = false`)
56+
* @return a new [JwtBuilder]
57+
*/
58+
public fun builder(
59+
jsonInstance: Json = JwtJson,
60+
): JwtBuilder = JwtBuilder(jsonInstance)
4961

50-
public fun parser(): JwtParserBuilder = JwtParserBuilder()
62+
/**
63+
* Creates a new [JwtParserBuilder] for parsing and validating JWS or JWE tokens.
64+
*
65+
* @param jsonInstance the [Json] instance to use for all deserialization within this parser;
66+
* defaults to the library's internal configuration (`ignoreUnknownKeys = true`,
67+
* `explicitNulls = false`)
68+
* @return a new [JwtParserBuilder]
69+
*/
70+
public fun parser(
71+
jsonInstance: Json = JwtJson,
72+
): JwtParserBuilder = JwtParserBuilder(jsonInstance)
5173
}

lib/src/commonMain/kotlin/co/touchlab/kjwt/builder/JwtBuilder.kt

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package co.touchlab.kjwt.builder
22

33
import co.touchlab.kjwt.cryptography.SimpleKey
4-
import co.touchlab.kjwt.internal.JwtJson
54
import co.touchlab.kjwt.internal.encodeBase64Url
65
import co.touchlab.kjwt.internal.encodeToBase64Url
76
import co.touchlab.kjwt.model.JwtHeader
@@ -16,6 +15,7 @@ import co.touchlab.kjwt.model.registry.SigningKey
1615
import co.touchlab.kjwt.model.registry.SigningKey.Identifier
1716
import dev.whyoleg.cryptography.materials.key.Key
1817
import kotlinx.serialization.SerializationStrategy
18+
import kotlinx.serialization.json.Json
1919
import kotlinx.serialization.json.JsonElement
2020
import kotlin.time.Duration
2121
import kotlin.time.Instant
@@ -42,7 +42,9 @@ import kotlin.uuid.ExperimentalUuidApi
4242
* .encryptWith(encKey, EncryptionContentAlgorithm.A256GCM)
4343
* ```
4444
*/
45-
public class JwtBuilder {
45+
public class JwtBuilder(
46+
internal val jsonInstance: Json,
47+
) {
4648
@PublishedApi
4749
internal val payloadBuilder: JwtPayload.Builder = JwtPayload.Builder()
4850

@@ -159,7 +161,7 @@ public class JwtBuilder {
159161
name: String,
160162
serializer: SerializationStrategy<T>,
161163
value: T?,
162-
): JwtBuilder = apply { payloadBuilder.claim(name, serializer, value) }
164+
): JwtBuilder = apply { payloadBuilder.claim(name, serializer, value, jsonInstance) }
163165

164166
/**
165167
* Sets a typed claim, inferring the serializer from the reified type [T].
@@ -194,7 +196,7 @@ public class JwtBuilder {
194196
public fun <T> payload(
195197
serializer: SerializationStrategy<T>,
196198
value: T,
197-
): JwtBuilder = apply { payloadBuilder.takeFrom(serializer, value) }
199+
): JwtBuilder = apply { payloadBuilder.takeFrom(serializer, value, jsonInstance) }
198200

199201
/**
200202
* Merges all fields from [value] into the payload, inferring the serializer from the reified
@@ -249,7 +251,7 @@ public class JwtBuilder {
249251
name: String,
250252
serializer: SerializationStrategy<T>,
251253
value: T?,
252-
): JwtBuilder = apply { headerBuilder.extra(name, serializer, value) }
254+
): JwtBuilder = apply { headerBuilder.extra(name, serializer, value, jsonInstance) }
253255

254256
/**
255257
* Sets a typed extra header parameter, inferring the serializer from the reified type [T].
@@ -284,7 +286,7 @@ public class JwtBuilder {
284286
public fun <T> header(
285287
serializer: SerializationStrategy<T>,
286288
value: T,
287-
): JwtBuilder = apply { headerBuilder.takeFrom(serializer, value) }
289+
): JwtBuilder = apply { headerBuilder.takeFrom(serializer, value, jsonInstance) }
288290

289291
/**
290292
* Merges all fields from [value] into the JOSE header, inferring the serializer from the
@@ -492,9 +494,9 @@ public class JwtBuilder {
492494
val header = headerBuilder.build(key.identifier.algorithm, contentAlgorithm, keyId)
493495
val payload = payloadBuilder.build()
494496

495-
val headerB64 = JwtJson.encodeToBase64Url(header)
497+
val headerB64 = jsonInstance.encodeToBase64Url(header)
496498
val aad = headerB64.encodeToByteArray()
497-
val plaintext = JwtJson.encodeToString(payload).encodeToByteArray()
499+
val plaintext = jsonInstance.encodeToString(payload).encodeToByteArray()
498500

499501
val result = key.encrypt(contentAlgorithm, plaintext, aad)
500502

lib/src/commonMain/kotlin/co/touchlab/kjwt/ext/JwkExt.kt

Lines changed: 50 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,21 @@ import dev.whyoleg.cryptography.serialization.asn1.modules.RsaKeyAlgorithmIdenti
2727
import dev.whyoleg.cryptography.serialization.asn1.modules.RsaPrivateKey
2828
import dev.whyoleg.cryptography.serialization.asn1.modules.RsaPublicKey
2929
import dev.whyoleg.cryptography.serialization.asn1.modules.SubjectPublicKeyInfo
30+
import kotlinx.serialization.json.Json
3031

3132
/**
3233
* Computes the base64url-encoded SHA-256 hash of this JWK Thumbprint as defined by RFC 7638.
3334
*
3435
* @return The base64url-encoded SHA-256 digest of the canonical JSON representation of this thumbprint.
3536
*/
3637
@ExperimentalKJWTApi
37-
public suspend fun Jwk.Thumbprint.hashed(): String {
38-
val bytes = JwtJson.encodeToString(this).encodeToByteArray()
38+
public suspend fun Jwk.Thumbprint.hashed(
39+
jsonInstance: Json = JwtJson,
40+
cryptoProvider: CryptographyProvider = CryptographyProvider.Default,
41+
): String {
42+
val bytes = jsonInstance.encodeToString(this).encodeToByteArray()
3943
val hash =
40-
CryptographyProvider.Default
44+
cryptoProvider
4145
.get(SHA256)
4246
.hasher()
4347
.hash(bytes)
@@ -52,8 +56,11 @@ public suspend fun Jwk.Thumbprint.hashed(): String {
5256
* Converts this [Jwk.Oct] to an [HMAC.Key] for the given [digest].
5357
*/
5458
@ExperimentalKJWTApi
55-
public suspend fun Jwk.Oct.toHmacKey(digest: CryptographyAlgorithmId<Digest>): HMAC.Key =
56-
CryptographyProvider.Default
59+
public suspend fun Jwk.Oct.toHmacKey(
60+
digest: CryptographyAlgorithmId<Digest>,
61+
cryptoProvider: CryptographyProvider = CryptographyProvider.Default,
62+
): HMAC.Key =
63+
cryptoProvider
5764
.get(HMAC)
5865
.keyDecoder(digest)
5966
.decodeFromByteArray(HMAC.Key.Format.RAW, k.decodeBase64Url())
@@ -120,48 +127,66 @@ private fun Jwk.Rsa.toPkcs8Der(): ByteArray {
120127

121128
/** Converts to [RSA.PKCS1.PublicKey] for RS256/RS384/RS512 verification. */
122129
@ExperimentalKJWTApi
123-
public suspend fun Jwk.Rsa.toRsaPkcs1PublicKey(digest: CryptographyAlgorithmId<Digest>): RSA.PKCS1.PublicKey =
124-
CryptographyProvider.Default
130+
public suspend fun Jwk.Rsa.toRsaPkcs1PublicKey(
131+
digest: CryptographyAlgorithmId<Digest>,
132+
cryptoProvider: CryptographyProvider = CryptographyProvider.Default,
133+
): RSA.PKCS1.PublicKey =
134+
cryptoProvider
125135
.get(RSA.PKCS1)
126136
.publicKeyDecoder(digest)
127137
.decodeFromByteArray(RSA.PublicKey.Format.DER, toSpkiDer())
128138

129139
/** Converts to [RSA.PKCS1.PrivateKey] for RS256/RS384/RS512 signing. */
130140
@ExperimentalKJWTApi
131-
public suspend fun Jwk.Rsa.toRsaPkcs1PrivateKey(digest: CryptographyAlgorithmId<Digest>): RSA.PKCS1.PrivateKey =
132-
CryptographyProvider.Default
141+
public suspend fun Jwk.Rsa.toRsaPkcs1PrivateKey(
142+
digest: CryptographyAlgorithmId<Digest>,
143+
cryptoProvider: CryptographyProvider = CryptographyProvider.Default,
144+
): RSA.PKCS1.PrivateKey =
145+
cryptoProvider
133146
.get(RSA.PKCS1)
134147
.privateKeyDecoder(digest)
135148
.decodeFromByteArray(RSA.PrivateKey.Format.DER, toPkcs8Der())
136149

137150
/** Converts to [RSA.PSS.PublicKey] for PS256/PS384/PS512 verification. */
138151
@ExperimentalKJWTApi
139-
public suspend fun Jwk.Rsa.toRsaPssPublicKey(digest: CryptographyAlgorithmId<Digest>): RSA.PSS.PublicKey =
140-
CryptographyProvider.Default
152+
public suspend fun Jwk.Rsa.toRsaPssPublicKey(
153+
digest: CryptographyAlgorithmId<Digest>,
154+
cryptoProvider: CryptographyProvider = CryptographyProvider.Default,
155+
): RSA.PSS.PublicKey =
156+
cryptoProvider
141157
.get(RSA.PSS)
142158
.publicKeyDecoder(digest)
143159
.decodeFromByteArray(RSA.PublicKey.Format.DER, toSpkiDer())
144160

145161
/** Converts to [RSA.PSS.PrivateKey] for PS256/PS384/PS512 signing. */
146162
@ExperimentalKJWTApi
147-
public suspend fun Jwk.Rsa.toRsaPssPrivateKey(digest: CryptographyAlgorithmId<Digest>): RSA.PSS.PrivateKey =
148-
CryptographyProvider.Default
163+
public suspend fun Jwk.Rsa.toRsaPssPrivateKey(
164+
digest: CryptographyAlgorithmId<Digest>,
165+
cryptoProvider: CryptographyProvider = CryptographyProvider.Default,
166+
): RSA.PSS.PrivateKey =
167+
cryptoProvider
149168
.get(RSA.PSS)
150169
.privateKeyDecoder(digest)
151170
.decodeFromByteArray(RSA.PrivateKey.Format.DER, toPkcs8Der())
152171

153172
/** Converts to [RSA.OAEP.PublicKey] for RSA-OAEP / RSA-OAEP-256 key encryption. */
154173
@ExperimentalKJWTApi
155-
public suspend fun Jwk.Rsa.toRsaOaepPublicKey(digest: CryptographyAlgorithmId<Digest>): RSA.OAEP.PublicKey =
156-
CryptographyProvider.Default
174+
public suspend fun Jwk.Rsa.toRsaOaepPublicKey(
175+
digest: CryptographyAlgorithmId<Digest>,
176+
cryptoProvider: CryptographyProvider = CryptographyProvider.Default,
177+
): RSA.OAEP.PublicKey =
178+
cryptoProvider
157179
.get(RSA.OAEP)
158180
.publicKeyDecoder(digest)
159181
.decodeFromByteArray(RSA.PublicKey.Format.DER, toSpkiDer())
160182

161183
/** Converts to [RSA.OAEP.PrivateKey] for RSA-OAEP / RSA-OAEP-256 key decryption. */
162184
@ExperimentalKJWTApi
163-
public suspend fun Jwk.Rsa.toRsaOaepPrivateKey(digest: CryptographyAlgorithmId<Digest>): RSA.OAEP.PrivateKey =
164-
CryptographyProvider.Default
185+
public suspend fun Jwk.Rsa.toRsaOaepPrivateKey(
186+
digest: CryptographyAlgorithmId<Digest>,
187+
cryptoProvider: CryptographyProvider = CryptographyProvider.Default,
188+
): RSA.OAEP.PrivateKey =
189+
cryptoProvider
165190
.get(RSA.OAEP)
166191
.privateKeyDecoder(digest)
167192
.decodeFromByteArray(RSA.PrivateKey.Format.DER, toPkcs8Der())
@@ -239,16 +264,20 @@ private fun Jwk.Ec.toPkcs8Der(): ByteArray {
239264

240265
/** Converts to [ECDSA.PublicKey] for ES256/ES384/ES512 verification. */
241266
@ExperimentalKJWTApi
242-
public suspend fun Jwk.Ec.toEcdsaPublicKey(): ECDSA.PublicKey =
243-
CryptographyProvider.Default
267+
public suspend fun Jwk.Ec.toEcdsaPublicKey(
268+
cryptoProvider: CryptographyProvider = CryptographyProvider.Default,
269+
): ECDSA.PublicKey =
270+
cryptoProvider
244271
.get(ECDSA)
245272
.publicKeyDecoder(ecCurve(crv))
246273
.decodeFromByteArray(EC.PublicKey.Format.DER, toSpkiDer())
247274

248275
/** Converts to [ECDSA.PrivateKey] for ES256/ES384/ES512 signing. */
249276
@ExperimentalKJWTApi
250-
public suspend fun Jwk.Ec.toEcdsaPrivateKey(): ECDSA.PrivateKey =
251-
CryptographyProvider.Default
277+
public suspend fun Jwk.Ec.toEcdsaPrivateKey(
278+
cryptoProvider: CryptographyProvider = CryptographyProvider.Default,
279+
): ECDSA.PrivateKey =
280+
cryptoProvider
252281
.get(ECDSA)
253282
.privateKeyDecoder(ecCurve(crv))
254283
.decodeFromByteArray(EC.PrivateKey.Format.DER, toPkcs8Der())

lib/src/commonMain/kotlin/co/touchlab/kjwt/internal/JsonUtils.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package co.touchlab.kjwt.internal
33
import co.touchlab.kjwt.exception.MalformedJwtException
44
import kotlinx.serialization.DeserializationStrategy
55
import kotlinx.serialization.json.Json
6+
import kotlinx.serialization.json.JsonElement
7+
import kotlinx.serialization.json.encodeToJsonElement
68

79
@PublishedApi
810
internal val JwtJson: Json =
@@ -34,6 +36,9 @@ internal fun <T> Json.decodeBase64Url(
3436
}
3537

3638
internal inline fun <reified T> Json.encodeToBase64Url(value: T): String =
37-
encodeToString(value)
39+
encodeToJsonElement(value).encodeToBase64Url()
40+
41+
internal fun JsonElement.encodeToBase64Url(): String =
42+
toString()
3843
.encodeToByteArray()
3944
.encodeBase64Url()

0 commit comments

Comments
 (0)