Skip to content

Commit 7ec8fc4

Browse files
committed
improving headers builder during jwt building
1 parent 045cfe8 commit 7ec8fc4

File tree

2 files changed

+280
-2
lines changed

2 files changed

+280
-2
lines changed

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

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ import kotlin.uuid.ExperimentalUuidApi
4545
public class JwtBuilder {
4646
@PublishedApi
4747
internal val payloadBuilder: JwtPayload.Builder = JwtPayload.Builder()
48-
private val headerBuilder: JwtHeader.Builder = JwtHeader.Builder()
48+
49+
@PublishedApi
50+
internal val headerBuilder: JwtHeader.Builder = JwtHeader.Builder()
4951

5052
/**
5153
* Sets the issuer (`iss`) claim.
@@ -169,7 +171,7 @@ public class JwtBuilder {
169171
public inline fun <reified T> claim(
170172
name: String,
171173
value: T,
172-
): JwtBuilder = apply { payloadBuilder.claim(name, value) }
174+
): JwtBuilder = claim(name, kotlinx.serialization.serializer<T>(), value)
173175

174176
/**
175177
* Configures multiple claims at once using a DSL block applied to [JwtPayload.Builder].
@@ -179,6 +181,60 @@ public class JwtBuilder {
179181
*/
180182
public fun claims(block: JwtPayload.Builder.() -> Unit): JwtBuilder = apply { payloadBuilder.block() }
181183

184+
/**
185+
* Sets the token type (`typ`) header parameter.
186+
*
187+
* @param typ the token type; defaults to `"JWT"`
188+
* @return this builder for chaining
189+
*/
190+
public fun type(typ: String): JwtBuilder = apply { headerBuilder.type = typ }
191+
192+
/**
193+
* Sets the content type (`cty`) header parameter.
194+
*
195+
* @param cty the content type
196+
* @return this builder for chaining
197+
*/
198+
public fun contentType(cty: String): JwtBuilder = apply { headerBuilder.contentType = cty }
199+
200+
/**
201+
* Sets a raw extra header parameter using a pre-built [JsonElement].
202+
*
203+
* @param name the header parameter name
204+
* @param value the header value as a [JsonElement]
205+
* @return this builder for chaining
206+
*/
207+
public fun header(
208+
name: String,
209+
value: JsonElement,
210+
): JwtBuilder = apply { headerBuilder.extra(name, value) }
211+
212+
/**
213+
* Sets a typed extra header parameter using an explicit [SerializationStrategy].
214+
*
215+
* @param name the header parameter name
216+
* @param serializer the serialization strategy for [T]
217+
* @param value the header value, or `null` to remove the parameter
218+
* @return this builder for chaining
219+
*/
220+
public fun <T> header(
221+
name: String,
222+
serializer: SerializationStrategy<T>,
223+
value: T?,
224+
): JwtBuilder = apply { headerBuilder.extra(name, serializer, value) }
225+
226+
/**
227+
* Sets a typed extra header parameter, inferring the serializer from the reified type [T].
228+
*
229+
* @param name the header parameter name
230+
* @param value the header value
231+
* @return this builder for chaining
232+
*/
233+
public inline fun <reified T> header(
234+
name: String,
235+
value: T,
236+
): JwtBuilder = header(name, kotlinx.serialization.serializer<T>(), value)
237+
182238
/**
183239
* Configures JOSE header fields using a DSL block applied to [JwtHeader.Builder].
184240
*
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.contentType
4+
import co.touchlab.kjwt.ext.getHeader
5+
import co.touchlab.kjwt.ext.getHeaderOrNull
6+
import co.touchlab.kjwt.ext.type
7+
import co.touchlab.kjwt.model.algorithm.EncryptionAlgorithm
8+
import co.touchlab.kjwt.model.algorithm.EncryptionContentAlgorithm
9+
import co.touchlab.kjwt.model.algorithm.SigningAlgorithm
10+
import io.kotest.core.spec.style.FunSpec
11+
import kotlinx.serialization.builtins.serializer
12+
import kotlinx.serialization.json.JsonPrimitive
13+
import kotlin.test.assertEquals
14+
import kotlin.test.assertNull
15+
16+
class JwtBuilderHeaderTest :
17+
FunSpec({
18+
context("JWS") {
19+
test("type sets typ header") {
20+
val key = hs256Key()
21+
val token =
22+
Jwt
23+
.builder()
24+
.subject("user")
25+
.type("at+JWT")
26+
.signWith(SigningAlgorithm.HS256, key)
27+
.compact()
28+
29+
val jws =
30+
Jwt
31+
.parser()
32+
.verifyWith(SigningAlgorithm.HS256, key)
33+
.build()
34+
.parseSigned(token)
35+
36+
assertEquals("at+JWT", jws.header.type)
37+
}
38+
39+
test("contentType sets cty header") {
40+
val key = hs256Key()
41+
val token =
42+
Jwt
43+
.builder()
44+
.subject("user")
45+
.contentType("JWT")
46+
.signWith(SigningAlgorithm.HS256, key)
47+
.compact()
48+
49+
val jws =
50+
Jwt
51+
.parser()
52+
.verifyWith(SigningAlgorithm.HS256, key)
53+
.build()
54+
.parseSigned(token)
55+
56+
assertEquals("JWT", jws.header.contentType)
57+
}
58+
59+
test("header with JsonElement sets custom extra header") {
60+
val key = hs256Key()
61+
val token =
62+
Jwt
63+
.builder()
64+
.subject("user")
65+
.header("x-str", JsonPrimitive("hello"))
66+
.signWith(SigningAlgorithm.HS256, key)
67+
.compact()
68+
69+
val jws =
70+
Jwt
71+
.parser()
72+
.verifyWith(SigningAlgorithm.HS256, key)
73+
.build()
74+
.parseSigned(token)
75+
76+
assertEquals("hello", jws.header.getHeader("x-str"))
77+
}
78+
79+
test("header with explicit serializer sets custom extra header") {
80+
val key = hs256Key()
81+
val token =
82+
Jwt
83+
.builder()
84+
.subject("user")
85+
.header("x-num", Int.serializer(), 42)
86+
.signWith(SigningAlgorithm.HS256, key)
87+
.compact()
88+
89+
val jws =
90+
Jwt
91+
.parser()
92+
.verifyWith(SigningAlgorithm.HS256, key)
93+
.build()
94+
.parseSigned(token)
95+
96+
assertEquals(42, jws.header.getHeader("x-num"))
97+
}
98+
99+
test("header with explicit serializer and null value removes extra header") {
100+
val key = hs256Key()
101+
val token =
102+
Jwt
103+
.builder()
104+
.subject("user")
105+
.header("x-str", String.serializer(), null)
106+
.signWith(SigningAlgorithm.HS256, key)
107+
.compact()
108+
109+
val jws =
110+
Jwt
111+
.parser()
112+
.verifyWith(SigningAlgorithm.HS256, key)
113+
.build()
114+
.parseSigned(token)
115+
116+
assertNull(jws.header.getHeaderOrNull<String>("x-str"))
117+
}
118+
119+
test("header reified infers serializer for custom extra header") {
120+
val key = hs256Key()
121+
val token =
122+
Jwt
123+
.builder()
124+
.subject("user")
125+
.header("x-str", "world")
126+
.signWith(SigningAlgorithm.HS256, key)
127+
.compact()
128+
129+
val jws =
130+
Jwt
131+
.parser()
132+
.verifyWith(SigningAlgorithm.HS256, key)
133+
.build()
134+
.parseSigned(token)
135+
136+
assertEquals("world", jws.header.getHeader("x-str"))
137+
}
138+
}
139+
140+
context("JWE") {
141+
142+
test("type sets typ header") {
143+
val cek = aesSimpleKey(128)
144+
val token =
145+
Jwt
146+
.builder()
147+
.subject("user")
148+
.type("at+JWT")
149+
.encryptWith(cek, EncryptionAlgorithm.Dir, EncryptionContentAlgorithm.A128GCM)
150+
.compact()
151+
152+
val jwe =
153+
Jwt
154+
.parser()
155+
.decryptWith(EncryptionAlgorithm.Dir, cek)
156+
.build()
157+
.parseEncrypted(token)
158+
159+
assertEquals("at+JWT", jwe.header.type)
160+
}
161+
162+
test("contentType sets cty header") {
163+
val cek = aesSimpleKey(128)
164+
val token =
165+
Jwt
166+
.builder()
167+
.subject("user")
168+
.contentType("JWT")
169+
.encryptWith(cek, EncryptionAlgorithm.Dir, EncryptionContentAlgorithm.A128GCM)
170+
.compact()
171+
172+
val jwe =
173+
Jwt
174+
.parser()
175+
.decryptWith(EncryptionAlgorithm.Dir, cek)
176+
.build()
177+
.parseEncrypted(token)
178+
179+
assertEquals("JWT", jwe.header.contentType)
180+
}
181+
182+
test("header with JsonElement sets custom extra header") {
183+
val cek = aesSimpleKey(128)
184+
val token =
185+
Jwt
186+
.builder()
187+
.subject("user")
188+
.header("x-str", JsonPrimitive("hello"))
189+
.encryptWith(cek, EncryptionAlgorithm.Dir, EncryptionContentAlgorithm.A128GCM)
190+
.compact()
191+
192+
val jwe =
193+
Jwt
194+
.parser()
195+
.decryptWith(EncryptionAlgorithm.Dir, cek)
196+
.build()
197+
.parseEncrypted(token)
198+
199+
assertEquals("hello", jwe.header.getHeader("x-str"))
200+
}
201+
202+
test("header reified infers serializer for custom extra header") {
203+
val cek = aesSimpleKey(128)
204+
val token =
205+
Jwt
206+
.builder()
207+
.subject("user")
208+
.header("x-str", "world")
209+
.encryptWith(cek, EncryptionAlgorithm.Dir, EncryptionContentAlgorithm.A128GCM)
210+
.compact()
211+
212+
val jwe =
213+
Jwt
214+
.parser()
215+
.decryptWith(EncryptionAlgorithm.Dir, cek)
216+
.build()
217+
.parseEncrypted(token)
218+
219+
assertEquals("world", jwe.header.getHeader("x-str"))
220+
}
221+
}
222+
})

0 commit comments

Comments
 (0)