Skip to content

Commit

Permalink
make JWT more usable
Browse files Browse the repository at this point in the history
  • Loading branch information
angryziber committed Jan 30, 2024
1 parent c72dba6 commit 6ba6106
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 6 deletions.
39 changes: 35 additions & 4 deletions oauth/src/JWT.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,41 @@
package klite.oauth

import klite.base64Decode
import klite.Email
import klite.base64UrlDecode
import klite.json.*
import java.time.Instant
import java.util.*

data class JWT(val token: String) {
private val parts = token.split(".").map { it.base64Decode().decodeToString() }
val header get() = parts[0]
val payload get() = parts[1]
companion object {
private val jsonMapper = JsonMapper()
}

private val parts = token.split(".").map { it.base64UrlDecode().decodeToString() }
val headerJson get() = parts[0]
val payloadJson get() = parts[1]
val signature get() = parts[2]

val header by lazy { Header(jsonMapper.parse<JsonNode>(headerJson)) }
val payload by lazy { Payload(jsonMapper.parse<JsonNode>(payloadJson)) }

data class Header(val fields: JsonNode): JsonNode by fields {
val alg by fields
val typ by fields
}

/** https://www.iana.org/assignments/jwt/jwt.xhtml#claims */
data class Payload(val claims: JsonNode): JsonNode by claims {
val subject get() = getString("sub")
val audience get() = getString("aud")
val issuedAt get() = getOrNull<Number>("iat")?.let { Instant.ofEpochSecond(it.toLong()) }
val issuer = getOrNull<String>("iss")
val expiresAt get() = getOrNull<Number>("exp")?.let { Instant.ofEpochSecond(it.toLong()) }
val name get() = getOrNull<String>("name")
val email get() = getOrNull<String>("email")?.let { Email(it) }
val emailVerified get() = getOrNull<Boolean>("email_verified")
val locale get() = getOrNull<String>("locale")?.let { Locale.forLanguageTag(it) }
}

data class Signature(val fields: JsonNode): JsonNode by fields
}
4 changes: 2 additions & 2 deletions oauth/src/OAuthClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,9 @@ class AppleOAuthClient(httpClient: HttpClient): OAuthClient(
httpClient
) {
override suspend fun profile(token: OAuthTokenResponse, exchange: HttpExchange): UserProfile {
val email = http.json.parse<JsonNode>(token.idToken!!.payload).getString("email")
val email = token.idToken!!.payload.email!!
val user = exchange.bodyParams["user"]?.let { http.json.parse<AppleUserProfile>(it.toString()) }
return UserProfile(provider, email, Email(email), user?.name?.firstName ?: email.substringBefore("@").capitalize(), user?.name?.lastName ?: "")
return UserProfile(provider, token.idToken.payload.subject, email, user?.name?.firstName ?: email.value.substringBefore("@").capitalize(), user?.name?.lastName ?: "")
}

data class AppleUserProfile(val name: AppleUserName, val email: Email)
Expand Down
18 changes: 18 additions & 0 deletions oauth/test/JWTTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import ch.tutteli.atrium.api.fluent.en_GB.toEqual
import ch.tutteli.atrium.api.verbs.expect
import klite.base64UrlDecode
import klite.oauth.JWT
import org.junit.jupiter.api.Test
import java.time.Instant

class JWTTest {
@Test fun parse() {
val jwt = JWT("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c")
expect(jwt.header.alg).toEqual("HS256")
expect(jwt.header.typ).toEqual("JWT")
expect(jwt.payload.subject).toEqual("1234567890")
expect(jwt.payload.name).toEqual("John Doe")
expect(jwt.payload.issuedAt).toEqual(Instant.ofEpochSecond(1516239022))
expect(jwt.signature).toEqual("SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c".base64UrlDecode().decodeToString())
}
}
2 changes: 2 additions & 0 deletions server/src/klite/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ fun String.urlEncode() = URLEncoder.encode(this, Charsets.UTF_8)!!

fun ByteArray.base64Encode() = Base64.getEncoder().encodeToString(this)
fun String.base64Encode() = toByteArray().base64Encode()
fun String.base64UrlEncode() = base64Encode().replace('+', '-').replace('/', '_').trimEnd('=')
fun String.base64Decode() = Base64.getDecoder().decode(this)
fun String.base64UrlDecode() = replace('-', '+').replace('_', '/').base64Decode()

typealias Params = Map<String, String?>
val URI.queryParams: Params get() = urlDecodeParams(rawQuery)
Expand Down
10 changes: 10 additions & 0 deletions server/test/klite/UtilsTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,14 @@ class UtilsTest {
expect(urlEncodeParams(params)).toEqual("Hello=W%C3%B6rld&1=2")
expect(urlDecodeParams("Hello=W%C3%B6rld&1=2")).toEqual(params - "null")
}

@Test fun base64() {
expect("hellöu".base64Encode()).toEqual("aGVsbMO2dQ==")
expect("aGVsbMO2dQ==".base64Decode().decodeToString()).toEqual("hellöu")
}

@Test fun base64Url() {
expect("hellöu".base64UrlEncode()).toEqual("aGVsbMO2dQ")
expect("aGVsbMO2dQ".base64UrlDecode().decodeToString()).toEqual("hellöu")
}
}

0 comments on commit 6ba6106

Please sign in to comment.