Skip to content

Commit 871c1fc

Browse files
committed
added helper functions to construct jwt header and payload from serializable type
1 parent 7ec8fc4 commit 871c1fc

File tree

4 files changed

+338
-0
lines changed

4 files changed

+338
-0
lines changed

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

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,34 @@ public class JwtBuilder {
181181
*/
182182
public fun claims(block: JwtPayload.Builder.() -> Unit): JwtBuilder = apply { payloadBuilder.block() }
183183

184+
/**
185+
* Merges all fields from [value] into the payload, encoded using [serializer].
186+
*
187+
* The object is serialized to a JSON object and each key-value pair is added to the payload,
188+
* overwriting any existing claim with the same name.
189+
*
190+
* @param serializer the serialization strategy for [T]
191+
* @param value the object whose fields should be merged into the payload
192+
* @return this builder for chaining
193+
*/
194+
public fun <T> payload(
195+
serializer: SerializationStrategy<T>,
196+
value: T,
197+
): JwtBuilder = apply { payloadBuilder.takeFrom(serializer, value) }
198+
199+
/**
200+
* Merges all fields from [value] into the payload, inferring the serializer from the reified
201+
* type [T].
202+
*
203+
* The object is serialized to a JSON object and each key-value pair is added to the payload,
204+
* overwriting any existing claim with the same name.
205+
*
206+
* @param value the object whose fields should be merged into the payload
207+
* @return this builder for chaining
208+
*/
209+
public inline fun <reified T> payload(value: T): JwtBuilder =
210+
payload(kotlinx.serialization.serializer<T>(), value)
211+
184212
/**
185213
* Sets the token type (`typ`) header parameter.
186214
*
@@ -243,6 +271,34 @@ public class JwtBuilder {
243271
*/
244272
public fun header(block: JwtHeader.Builder.() -> Unit): JwtBuilder = apply { headerBuilder.block() }
245273

274+
/**
275+
* Merges all fields from [value] into the JOSE header, encoded using [serializer].
276+
*
277+
* The object is serialized to a JSON object and each key-value pair is added to the header,
278+
* overwriting any existing parameter with the same name.
279+
*
280+
* @param serializer the serialization strategy for [T]
281+
* @param value the object whose fields should be merged into the header
282+
* @return this builder for chaining
283+
*/
284+
public fun <T> header(
285+
serializer: SerializationStrategy<T>,
286+
value: T,
287+
): JwtBuilder = apply { headerBuilder.takeFrom(serializer, value) }
288+
289+
/**
290+
* Merges all fields from [value] into the JOSE header, inferring the serializer from the
291+
* reified type [T].
292+
*
293+
* The object is serialized to a JSON object and each key-value pair is added to the header,
294+
* overwriting any existing parameter with the same name.
295+
*
296+
* @param value the object whose fields should be merged into the header
297+
* @return this builder for chaining
298+
*/
299+
public inline fun <reified T> header(value: T): JwtBuilder =
300+
header(kotlinx.serialization.serializer<T>(), value)
301+
246302
/**
247303
* Builds and returns a JWS compact serialization: `header.payload.signature`.
248304
*

lib/src/commonMain/kotlin/co/touchlab/kjwt/model/JwtHeader.kt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,36 @@ public class JwtHeader internal constructor(
162162
extra(name, kotlinx.serialization.serializer<T>(), value)
163163
}
164164

165+
/**
166+
* Merges all fields from [value] into this builder, encoded using [serializer].
167+
*
168+
* The object is serialized to a [JsonObject] and each key-value pair is added to the
169+
* header, overwriting any existing parameter with the same name.
170+
*
171+
* @param serializer the serialization strategy for [T]
172+
* @param value the object whose fields should be merged into the header
173+
*/
174+
public fun <T> takeFrom(
175+
serializer: SerializationStrategy<T>,
176+
value: T,
177+
) {
178+
val jsonObject = JwtJson.encodeToJsonElement(serializer, value) as JsonObject
179+
jsonObject.forEach { (key, element) -> content[key] = element }
180+
}
181+
182+
/**
183+
* Merges all fields from [value] into this builder, inferring the serializer from the
184+
* reified type [T].
185+
*
186+
* The object is serialized to a [JsonObject] and each key-value pair is added to the
187+
* header, overwriting any existing parameter with the same name.
188+
*
189+
* @param value the object whose fields should be merged into the header
190+
*/
191+
public inline fun <reified T> takeFrom(value: T) {
192+
takeFrom(kotlinx.serialization.serializer<T>(), value)
193+
}
194+
165195
internal fun build(
166196
algorithm: SigningAlgorithm<*, *>,
167197
keyId: String?,

lib/src/commonMain/kotlin/co/touchlab/kjwt/model/JwtPayload.kt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,36 @@ public class JwtPayload internal constructor(
191191
claim(name, kotlinx.serialization.serializer<T>(), value)
192192
}
193193

194+
/**
195+
* Merges all fields from [value] into this builder, encoded using [serializer].
196+
*
197+
* The object is serialized to a [JsonObject] and each key-value pair is added to the
198+
* payload, overwriting any existing claim with the same name.
199+
*
200+
* @param serializer the serialization strategy for [T]
201+
* @param value the object whose fields should be merged into the payload
202+
*/
203+
public fun <T> takeFrom(
204+
serializer: SerializationStrategy<T>,
205+
value: T,
206+
) {
207+
val jsonObject = JwtJson.encodeToJsonElement(serializer, value) as JsonObject
208+
jsonObject.forEach { (key, element) -> content[key] = element }
209+
}
210+
211+
/**
212+
* Merges all fields from [value] into this builder, inferring the serializer from the
213+
* reified type [T].
214+
*
215+
* The object is serialized to a [JsonObject] and each key-value pair is added to the
216+
* payload, overwriting any existing claim with the same name.
217+
*
218+
* @param value the object whose fields should be merged into the payload
219+
*/
220+
public inline fun <reified T> takeFrom(value: T) {
221+
takeFrom(kotlinx.serialization.serializer<T>(), value)
222+
}
223+
194224
/**
195225
* Sets the expiration time (`exp`) claim relative to the current time.
196226
*
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
package co.touchlab.kjwt
2+
3+
import co.touchlab.kjwt.ext.audience
4+
import co.touchlab.kjwt.ext.getClaim
5+
import co.touchlab.kjwt.ext.getClaimOrNull
6+
import co.touchlab.kjwt.ext.getHeader
7+
import co.touchlab.kjwt.ext.subject
8+
import co.touchlab.kjwt.ext.type
9+
import co.touchlab.kjwt.model.JwtHeader
10+
import co.touchlab.kjwt.model.JwtPayload
11+
import co.touchlab.kjwt.model.algorithm.EncryptionAlgorithm
12+
import co.touchlab.kjwt.model.algorithm.EncryptionContentAlgorithm
13+
import co.touchlab.kjwt.model.algorithm.SigningAlgorithm
14+
import io.kotest.core.spec.style.FunSpec
15+
import kotlinx.serialization.SerialName
16+
import kotlinx.serialization.Serializable
17+
import kotlinx.serialization.serializer
18+
import kotlin.test.assertEquals
19+
20+
@Serializable private data class TakeFromPayload(
21+
@SerialName(JwtPayload.SUB) val userId: String,
22+
val role: String,
23+
)
24+
25+
@Serializable private data class TakeFromHeader(
26+
@SerialName(JwtHeader.TYP) val type: String,
27+
val version: Int,
28+
)
29+
30+
class JwtBuilderTakeFromTest :
31+
FunSpec({
32+
context("JwtBuilder.payload()") {
33+
test("explicit serializer merges payload fields (JWS)") {
34+
val key = hs256Key()
35+
val token =
36+
Jwt.builder()
37+
.payload(serializer<TakeFromPayload>(), TakeFromPayload("u1", "admin"))
38+
.signWith(SigningAlgorithm.HS256, key)
39+
.compact()
40+
41+
val jws = Jwt.parser().verifyWith(SigningAlgorithm.HS256, key).build().parseSigned(token)
42+
43+
assertEquals("u1", jws.payload.subject)
44+
assertEquals("admin", jws.payload.getClaim("role"))
45+
}
46+
47+
test("reified generic merges payload fields (JWS)") {
48+
val key = hs256Key()
49+
val token =
50+
Jwt.builder()
51+
.payload(TakeFromPayload("u2", "viewer"))
52+
.signWith(SigningAlgorithm.HS256, key)
53+
.compact()
54+
55+
val jws = Jwt.parser().verifyWith(SigningAlgorithm.HS256, key).build().parseSigned(token)
56+
57+
assertEquals("u2", jws.payload.subject)
58+
assertEquals("viewer", jws.payload.getClaim("role"))
59+
}
60+
61+
test("payload() is additive — previously set claims survive") {
62+
val key = hs256Key()
63+
val token =
64+
Jwt.builder()
65+
.audience("prior-aud")
66+
.payload(TakeFromPayload("u3", "editor"))
67+
.signWith(SigningAlgorithm.HS256, key)
68+
.compact()
69+
70+
val jws = Jwt.parser().verifyWith(SigningAlgorithm.HS256, key).build().parseSigned(token)
71+
72+
assertEquals("prior-aud", jws.payload.audience.first())
73+
assertEquals("u3", jws.payload.subject)
74+
assertEquals("editor", jws.payload.getClaimOrNull("role"))
75+
}
76+
77+
test("payload() is additive — existing keys are replaced") {
78+
val key = hs256Key()
79+
val token =
80+
Jwt.builder()
81+
.subject("sub")
82+
.payload(TakeFromPayload("u3", "editor"))
83+
.signWith(SigningAlgorithm.HS256, key)
84+
.compact()
85+
86+
val jws = Jwt.parser().verifyWith(SigningAlgorithm.HS256, key).build().parseSigned(token)
87+
88+
assertEquals("u3", jws.payload.subject)
89+
assertEquals("editor", jws.payload.getClaimOrNull("role"))
90+
}
91+
92+
test("values taken from another type can be replaced") {
93+
val key = hs256Key()
94+
val token =
95+
Jwt.builder()
96+
.payload(TakeFromPayload("u4", "editor"))
97+
.subject("sub")
98+
.signWith(SigningAlgorithm.HS256, key)
99+
.compact()
100+
101+
val jws = Jwt.parser().verifyWith(SigningAlgorithm.HS256, key).build().parseSigned(token)
102+
103+
assertEquals("sub", jws.payload.subject)
104+
assertEquals("editor", jws.payload.getClaimOrNull("role"))
105+
}
106+
107+
test("explicit serializer merges payload fields (JWE)") {
108+
val cek = aesSimpleKey(256)
109+
val token =
110+
Jwt.builder()
111+
.payload(serializer<TakeFromPayload>(), TakeFromPayload("u4", "guest"))
112+
.encryptWith(cek, EncryptionAlgorithm.Dir, EncryptionContentAlgorithm.A256GCM)
113+
.compact()
114+
115+
val jwe = Jwt.parser().decryptWith(EncryptionAlgorithm.Dir, cek).build().parseEncrypted(token)
116+
117+
assertEquals("u4", jwe.payload.subject)
118+
assertEquals("guest", jwe.payload.getClaim("role"))
119+
}
120+
}
121+
122+
context("JwtBuilder.header()") {
123+
test("explicit serializer merges header fields (JWS)") {
124+
val key = hs256Key()
125+
val token =
126+
Jwt.builder()
127+
.subject("user")
128+
.header(serializer<TakeFromHeader>(), TakeFromHeader("at+JWT", 2))
129+
.signWith(SigningAlgorithm.HS256, key)
130+
.compact()
131+
132+
val jws = Jwt.parser().verifyWith(SigningAlgorithm.HS256, key).build().parseSigned(token)
133+
134+
assertEquals("at+JWT", jws.header.type)
135+
assertEquals(2, jws.header.getHeader<Int>("version"))
136+
}
137+
138+
test("reified generic merges header fields (JWS)") {
139+
val key = hs256Key()
140+
val token =
141+
Jwt.builder()
142+
.subject("user")
143+
.header(TakeFromHeader("at+JWT", 3))
144+
.signWith(SigningAlgorithm.HS256, key)
145+
.compact()
146+
147+
val jws = Jwt.parser().verifyWith(SigningAlgorithm.HS256, key).build().parseSigned(token)
148+
149+
assertEquals("at+JWT", jws.header.type)
150+
assertEquals(3, jws.header.getHeader<Int>("version"))
151+
}
152+
153+
test("header() is additive — pre-existing parameters survive") {
154+
val key = hs256Key()
155+
val token =
156+
Jwt.builder()
157+
.subject("user")
158+
.contentType("JWT")
159+
.header(TakeFromHeader("at+JWT", 1))
160+
.signWith(SigningAlgorithm.HS256, key)
161+
.compact()
162+
163+
val jws = Jwt.parser().verifyWith(SigningAlgorithm.HS256, key).build().parseSigned(token)
164+
165+
assertEquals("JWT", jws.header.getHeader("cty"))
166+
assertEquals("at+JWT", jws.header.type)
167+
assertEquals(1, jws.header.getHeader<Int>("version"))
168+
}
169+
}
170+
171+
context("JwtPayload.Builder.takeFrom()") {
172+
test("explicit serializer merges claims via DSL block") {
173+
val key = hs256Key()
174+
val token = Jwt.builder().claims {
175+
takeFrom(serializer<TakeFromPayload>(), TakeFromPayload("u5", "ops"))
176+
}.signWith(SigningAlgorithm.HS256, key).compact()
177+
178+
val jws = Jwt.parser().verifyWith(SigningAlgorithm.HS256, key).build().parseSigned(token)
179+
180+
assertEquals("u5", jws.payload.subject)
181+
assertEquals("ops", jws.payload.getClaim("role"))
182+
}
183+
184+
test("reified generic merges claims via DSL block") {
185+
val key = hs256Key()
186+
val token = Jwt.builder().claims {
187+
takeFrom(TakeFromPayload("u6", "dev"))
188+
}.signWith(SigningAlgorithm.HS256, key).compact()
189+
190+
val jws = Jwt.parser().verifyWith(SigningAlgorithm.HS256, key).build().parseSigned(token)
191+
192+
assertEquals("u6", jws.payload.subject)
193+
assertEquals("dev", jws.payload.getClaim("role"))
194+
}
195+
}
196+
197+
context("JwtHeader.Builder.takeFrom()") {
198+
test("explicit serializer merges header params via DSL block") {
199+
val key = hs256Key()
200+
val token = Jwt.builder().subject("user").header {
201+
takeFrom(serializer<TakeFromHeader>(), TakeFromHeader("at+JWT", 4))
202+
}.signWith(SigningAlgorithm.HS256, key).compact()
203+
204+
val jws = Jwt.parser().verifyWith(SigningAlgorithm.HS256, key).build().parseSigned(token)
205+
206+
assertEquals("at+JWT", jws.header.type)
207+
assertEquals(4, jws.header.getHeader<Int>("version"))
208+
}
209+
210+
test("reified generic merges header params via DSL block") {
211+
val key = hs256Key()
212+
val token = Jwt.builder().subject("user").header {
213+
takeFrom(TakeFromHeader("at+JWT", 5))
214+
}.signWith(SigningAlgorithm.HS256, key).compact()
215+
216+
val jws = Jwt.parser().verifyWith(SigningAlgorithm.HS256, key).build().parseSigned(token)
217+
218+
assertEquals("at+JWT", jws.header.type)
219+
assertEquals(5, jws.header.getHeader<Int>("version"))
220+
}
221+
}
222+
})

0 commit comments

Comments
 (0)