Skip to content

Commit 045cfe8

Browse files
committed
support parsing header as custom type like we support for payload
1 parent 3ea21b7 commit 045cfe8

File tree

3 files changed

+130
-5
lines changed

3 files changed

+130
-5
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import kotlinx.serialization.json.put
2222
@Serializable(with = JwtHeaderSerializer::class)
2323
public class JwtHeader internal constructor(
2424
internal val base64Encoded: String,
25-
internal val jsonData: JsonObject,
25+
@PublishedApi internal val jsonData: JsonObject,
2626
) {
2727
internal constructor(jsonData: JsonObject) : this(
2828
base64Encoded = JwtJson.encodeToBase64Url(jsonData),

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,16 @@ public sealed class JwtInstance {
2020
payload.jsonData
2121
)
2222

23+
/**
24+
* Deserializes the token header as type [T].
25+
*
26+
* @return the header deserialized into an instance of [T]
27+
*/
28+
public inline fun <reified T> getHeader(): T = JwtJson.decodeFromJsonElement(
29+
kotlinx.serialization.serializer<T>(),
30+
header.jsonData
31+
)
32+
2333
/** Represents a JWE (encrypted) token with five compact-serialization parts. */
2434
public class Jwe internal constructor(
2535
override val header: JwtHeader,

lib/src/commonTest/kotlin/co/touchlab/kjwt/CustomPayloadTest.kt renamed to lib/src/commonTest/kotlin/co/touchlab/kjwt/CustomTypeParsing.kt

Lines changed: 119 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,38 @@
22

33
package co.touchlab.kjwt
44

5+
import co.touchlab.kjwt.model.JwtHeader
56
import co.touchlab.kjwt.model.JwtPayload
67
import co.touchlab.kjwt.model.algorithm.EncryptionAlgorithm
78
import co.touchlab.kjwt.model.algorithm.EncryptionContentAlgorithm
89
import co.touchlab.kjwt.model.algorithm.SigningAlgorithm
910
import io.kotest.core.spec.style.FunSpec
1011
import kotlinx.serialization.SerialName
1112
import kotlinx.serialization.Serializable
12-
import kotlinx.serialization.json.JsonObject
1313
import kotlin.test.assertEquals
1414
import kotlin.test.assertNull
1515
import kotlin.time.Clock
1616
import kotlin.time.Duration.Companion.hours
1717

18-
// ---- Custom payload type used in all tests in this file ----
18+
// ---- Custom types used in all tests in this file ----
19+
20+
@Serializable
21+
data class CustomHeader(
22+
@SerialName(JwtHeader.ALG) val algorithm: String? = null,
23+
@SerialName(JwtHeader.TYP) val type: String? = null,
24+
@SerialName("kid") val keyId: String? = null,
25+
@SerialName("x-custom") val custom: String? = null,
26+
)
1927

2028
@Serializable
2129
data class UserClaims(
2230
@SerialName(JwtPayload.SUB) val subject: String? = null,
2331
@SerialName("role") val role: String? = null,
2432
@SerialName("level") val level: Int? = null,
2533
@SerialName("exp") val expSeconds: Long? = null,
26-
private val jsonData: JsonObject = JsonObject(emptyMap()),
2734
)
2835

29-
class CustomPayloadTest :
36+
class CustomTypeParsing :
3037
FunSpec({
3138

3239
context("JWS") {
@@ -79,6 +86,114 @@ class CustomPayloadTest :
7986
}
8087
}
8188

89+
context("getHeader") {
90+
91+
test("JWS - deserializes standard header fields into custom type") {
92+
val key = hs256Key()
93+
val token =
94+
Jwt
95+
.builder()
96+
.subject("user-1")
97+
.signWith(SigningAlgorithm.HS256, key)
98+
.compact()
99+
100+
val jws =
101+
Jwt
102+
.parser()
103+
.verifyWith(SigningAlgorithm.HS256, key)
104+
.build()
105+
.parseSigned(token)
106+
107+
val header = jws.getHeader<CustomHeader>()
108+
assertEquals("HS256", header.algorithm)
109+
assertEquals("JWT", header.type)
110+
}
111+
112+
test("JWS - custom extra header field is deserialized") {
113+
val key = hs256Key()
114+
val token =
115+
Jwt
116+
.builder()
117+
.subject("user-2")
118+
.header { extra("x-custom", "hello") }
119+
.signWith(SigningAlgorithm.HS256, key)
120+
.compact()
121+
122+
val jws =
123+
Jwt
124+
.parser()
125+
.verifyWith(SigningAlgorithm.HS256, key)
126+
.build()
127+
.parseSigned(token)
128+
129+
val header = jws.getHeader<CustomHeader>()
130+
assertEquals("hello", header.custom)
131+
}
132+
133+
test("JWS - absent optional header field is null") {
134+
val key = hs256Key()
135+
val token =
136+
Jwt
137+
.builder()
138+
.subject("user-3")
139+
.signWith(SigningAlgorithm.HS256, key)
140+
.compact()
141+
142+
val jws =
143+
Jwt
144+
.parser()
145+
.verifyWith(SigningAlgorithm.HS256, key)
146+
.build()
147+
.parseSigned(token)
148+
149+
val header = jws.getHeader<CustomHeader>()
150+
assertNull(header.keyId)
151+
assertNull(header.custom)
152+
}
153+
154+
test("JWE - deserializes standard header fields into custom type") {
155+
val cek = aesSimpleKey(128)
156+
val token =
157+
Jwt
158+
.builder()
159+
.subject("enc-user-1")
160+
.encryptWith(cek, EncryptionAlgorithm.Dir, EncryptionContentAlgorithm.A128GCM)
161+
.compact()
162+
163+
val jwe =
164+
Jwt
165+
.parser()
166+
.decryptWith(EncryptionAlgorithm.Dir, cek)
167+
.build()
168+
.parseEncrypted(token)
169+
170+
val header = jwe.getHeader<CustomHeader>()
171+
assertEquals("dir", header.algorithm)
172+
assertEquals("JWT", header.type)
173+
}
174+
175+
test("JWE - custom extra header field is deserialized") {
176+
val cek = aesSimpleKey(128)
177+
val token =
178+
Jwt
179+
.builder()
180+
.subject("enc-user-2")
181+
.header { extra("x-custom", "world") }
182+
.encryptWith(cek, EncryptionAlgorithm.Dir, EncryptionContentAlgorithm.A128GCM)
183+
.compact()
184+
185+
val jwe =
186+
Jwt
187+
.parser()
188+
.decryptWith(EncryptionAlgorithm.Dir, cek)
189+
.build()
190+
.parseEncrypted(token)
191+
192+
val header = jwe.getHeader<CustomHeader>()
193+
assertEquals("world", header.custom)
194+
}
195+
}
196+
82197
context("JWE") {
83198

84199
test("parse encrypted Jwt custom type direct property access") {

0 commit comments

Comments
 (0)