From 5ae1de69a990720248403f86e9ca2897a8efff46 Mon Sep 17 00:00:00 2001 From: Volodymyr Nevmerzhytskyi Date: Mon, 11 Aug 2025 10:17:21 +0300 Subject: [PATCH 1/3] initial release: JWTDecode --- app-android/build.gradle.kts | 4 ++-- .../{template => jwt/decode}/android/MainActivity.kt | 4 ++-- app-shared/build.gradle.kts | 2 +- .../kotlin/dev/sdkforge/{template => jwt/decode}/app/App.kt | 4 ++-- .../{template => jwt/decode}/app/ApplicationTheme.kt | 2 +- .../decode}/app/ComposeAppViewController.kt | 2 +- gradle.properties | 4 ++-- settings.gradle.kts | 2 +- shared-core/api/shared-core.api | 6 +++--- shared-core/build.gradle.kts | 2 +- .../{template => jwt/decode}/core/Platform.android.kt | 2 +- .../{template => jwt/decode}/core/PlatformTest.android.kt | 2 +- .../dev/sdkforge/{template => jwt/decode}/core/Platform.kt | 2 +- .../sdkforge/{template => jwt/decode}/core/PlatformTest.kt | 2 +- .../sdkforge/{template => jwt/decode}/core/Platform.ios.kt | 2 +- .../{template => jwt/decode}/core/PlatformTest.ios.kt | 2 +- shared/api/shared.api | 4 ++-- shared/build.gradle.kts | 2 +- .../kotlin/dev/sdkforge/{template => jwt/decode}/Library.kt | 2 +- 19 files changed, 26 insertions(+), 26 deletions(-) rename app-android/src/main/java/dev/sdkforge/{template => jwt/decode}/android/MainActivity.kt (87%) rename app-shared/src/commonMain/kotlin/dev/sdkforge/{template => jwt/decode}/app/App.kt (92%) rename app-shared/src/commonMain/kotlin/dev/sdkforge/{template => jwt/decode}/app/ApplicationTheme.kt (97%) rename app-shared/src/iosMain/kotlin/dev/sdkforge/{template => jwt/decode}/app/ComposeAppViewController.kt (93%) rename shared-core/src/androidMain/kotlin/dev/sdkforge/{template => jwt/decode}/core/Platform.android.kt (84%) rename shared-core/src/androidUnitTest/kotlin/dev/sdkforge/{template => jwt/decode}/core/PlatformTest.android.kt (88%) rename shared-core/src/commonMain/kotlin/dev/sdkforge/{template => jwt/decode}/core/Platform.kt (74%) rename shared-core/src/commonTest/kotlin/dev/sdkforge/{template => jwt/decode}/core/PlatformTest.kt (90%) rename shared-core/src/iosMain/kotlin/dev/sdkforge/{template => jwt/decode}/core/Platform.ios.kt (86%) rename shared-core/src/iosTest/kotlin/dev/sdkforge/{template => jwt/decode}/core/PlatformTest.ios.kt (88%) rename shared/src/commonMain/kotlin/dev/sdkforge/{template => jwt/decode}/Library.kt (64%) diff --git a/app-android/build.gradle.kts b/app-android/build.gradle.kts index 374f254..3a50e3d 100644 --- a/app-android/build.gradle.kts +++ b/app-android/build.gradle.kts @@ -8,10 +8,10 @@ plugins { } android { - namespace = "dev.sdkforge.template.android" + namespace = "dev.sdkforge.jwt.decode.android" compileSdk = 36 defaultConfig { - applicationId = "dev.sdkforge.template.android" + applicationId = "dev.sdkforge.jwt.decode.android" minSdk = 21 targetSdk = 36 versionCode = 1 diff --git a/app-android/src/main/java/dev/sdkforge/template/android/MainActivity.kt b/app-android/src/main/java/dev/sdkforge/jwt/decode/android/MainActivity.kt similarity index 87% rename from app-android/src/main/java/dev/sdkforge/template/android/MainActivity.kt rename to app-android/src/main/java/dev/sdkforge/jwt/decode/android/MainActivity.kt index 1c9f5d2..0c12152 100644 --- a/app-android/src/main/java/dev/sdkforge/template/android/MainActivity.kt +++ b/app-android/src/main/java/dev/sdkforge/jwt/decode/android/MainActivity.kt @@ -1,4 +1,4 @@ -package dev.sdkforge.template.android +package dev.sdkforge.jwt.decode.android import android.os.Bundle import androidx.activity.ComponentActivity @@ -6,7 +6,7 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.ui.Modifier -import dev.sdkforge.template.app.App +import dev.sdkforge.jwt.decode.app.App class MainActivity : ComponentActivity() { override fun onCreate( diff --git a/app-shared/build.gradle.kts b/app-shared/build.gradle.kts index 506ff0b..2bc0917 100644 --- a/app-shared/build.gradle.kts +++ b/app-shared/build.gradle.kts @@ -21,5 +21,5 @@ kotlin { } android { - namespace = "dev.sdkforge.template.app" + namespace = "dev.sdkforge.jwt.decode.app" } diff --git a/app-shared/src/commonMain/kotlin/dev/sdkforge/template/app/App.kt b/app-shared/src/commonMain/kotlin/dev/sdkforge/jwt/decode/app/App.kt similarity index 92% rename from app-shared/src/commonMain/kotlin/dev/sdkforge/template/app/App.kt rename to app-shared/src/commonMain/kotlin/dev/sdkforge/jwt/decode/app/App.kt index 0811cf7..5e94485 100644 --- a/app-shared/src/commonMain/kotlin/dev/sdkforge/template/app/App.kt +++ b/app-shared/src/commonMain/kotlin/dev/sdkforge/jwt/decode/app/App.kt @@ -1,4 +1,4 @@ -package dev.sdkforge.template.app +package dev.sdkforge.jwt.decode.app import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -9,7 +9,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import dev.sdkforge.template.core.currentPlatform +import dev.sdkforge.jwt.decode.core.currentPlatform @Composable fun App( diff --git a/app-shared/src/commonMain/kotlin/dev/sdkforge/template/app/ApplicationTheme.kt b/app-shared/src/commonMain/kotlin/dev/sdkforge/jwt/decode/app/ApplicationTheme.kt similarity index 97% rename from app-shared/src/commonMain/kotlin/dev/sdkforge/template/app/ApplicationTheme.kt rename to app-shared/src/commonMain/kotlin/dev/sdkforge/jwt/decode/app/ApplicationTheme.kt index 2ab7425..4661296 100644 --- a/app-shared/src/commonMain/kotlin/dev/sdkforge/template/app/ApplicationTheme.kt +++ b/app-shared/src/commonMain/kotlin/dev/sdkforge/jwt/decode/app/ApplicationTheme.kt @@ -1,4 +1,4 @@ -package dev.sdkforge.template.app +package dev.sdkforge.jwt.decode.app import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.shape.RoundedCornerShape diff --git a/app-shared/src/iosMain/kotlin/dev/sdkforge/template/app/ComposeAppViewController.kt b/app-shared/src/iosMain/kotlin/dev/sdkforge/jwt/decode/app/ComposeAppViewController.kt similarity index 93% rename from app-shared/src/iosMain/kotlin/dev/sdkforge/template/app/ComposeAppViewController.kt rename to app-shared/src/iosMain/kotlin/dev/sdkforge/jwt/decode/app/ComposeAppViewController.kt index 437287b..fd45fd1 100644 --- a/app-shared/src/iosMain/kotlin/dev/sdkforge/template/app/ComposeAppViewController.kt +++ b/app-shared/src/iosMain/kotlin/dev/sdkforge/jwt/decode/app/ComposeAppViewController.kt @@ -1,4 +1,4 @@ -package dev.sdkforge.template.app +package dev.sdkforge.jwt.decode.app import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.ui.Modifier diff --git a/gradle.properties b/gradle.properties index 5cc4358..5eb7354 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,7 +12,7 @@ android.nonTransitiveRClass=true org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true #Publishing -publishing.group=dev.sdkforge.template +publishing.group=dev.sdkforge.jwt.decode publishing.version=0.0.1 publishing.owner=SDKForge -publishing.repository=template-sdk \ No newline at end of file +publishing.repository=JWTDecode \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 6db4227..bd2ef92 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -33,7 +33,7 @@ dependencyResolutionManagement { } } -rootProject.name = "SDKForgeTemplate" +rootProject.name = "SDKForge-JWTDecode" include(":app-android") include(":app-shared") diff --git a/shared-core/api/shared-core.api b/shared-core/api/shared-core.api index 9193b11..f1592e5 100644 --- a/shared-core/api/shared-core.api +++ b/shared-core/api/shared-core.api @@ -1,9 +1,9 @@ -public abstract interface class dev/sdkforge/template/core/Platform { +public abstract interface class dev/sdkforge/jwt/decode/core/Platform { public abstract fun getName ()Ljava/lang/String; public abstract fun getVersion ()Ljava/lang/String; } -public final class dev/sdkforge/template/core/Platform_androidKt { - public static final fun getCurrentPlatform ()Ldev/sdkforge/template/core/Platform; +public final class dev/sdkforge/jwt/decode/core/Platform_androidKt { + public static final fun getCurrentPlatform ()Ldev/sdkforge/jwt/decode/core/Platform; } diff --git a/shared-core/build.gradle.kts b/shared-core/build.gradle.kts index 745b438..ccad629 100644 --- a/shared-core/build.gradle.kts +++ b/shared-core/build.gradle.kts @@ -24,5 +24,5 @@ kotlin { } android { - namespace = "dev.sdkforge.template.core" + namespace = "dev.sdkforge.jwt.decode.core" } diff --git a/shared-core/src/androidMain/kotlin/dev/sdkforge/template/core/Platform.android.kt b/shared-core/src/androidMain/kotlin/dev/sdkforge/jwt/decode/core/Platform.android.kt similarity index 84% rename from shared-core/src/androidMain/kotlin/dev/sdkforge/template/core/Platform.android.kt rename to shared-core/src/androidMain/kotlin/dev/sdkforge/jwt/decode/core/Platform.android.kt index 2271a96..31a76f4 100644 --- a/shared-core/src/androidMain/kotlin/dev/sdkforge/template/core/Platform.android.kt +++ b/shared-core/src/androidMain/kotlin/dev/sdkforge/jwt/decode/core/Platform.android.kt @@ -1,4 +1,4 @@ -package dev.sdkforge.template.core +package dev.sdkforge.jwt.decode.core actual val currentPlatform: Platform = object : Platform { override val name: String get() = "Android" diff --git a/shared-core/src/androidUnitTest/kotlin/dev/sdkforge/template/core/PlatformTest.android.kt b/shared-core/src/androidUnitTest/kotlin/dev/sdkforge/jwt/decode/core/PlatformTest.android.kt similarity index 88% rename from shared-core/src/androidUnitTest/kotlin/dev/sdkforge/template/core/PlatformTest.android.kt rename to shared-core/src/androidUnitTest/kotlin/dev/sdkforge/jwt/decode/core/PlatformTest.android.kt index fe15c20..f6cf2bf 100644 --- a/shared-core/src/androidUnitTest/kotlin/dev/sdkforge/template/core/PlatformTest.android.kt +++ b/shared-core/src/androidUnitTest/kotlin/dev/sdkforge/jwt/decode/core/PlatformTest.android.kt @@ -1,6 +1,6 @@ @file:Suppress("ktlint:standard:filename") -package dev.sdkforge.template.core +package dev.sdkforge.jwt.decode.core import org.junit.Assert.assertTrue import org.junit.Test diff --git a/shared-core/src/commonMain/kotlin/dev/sdkforge/template/core/Platform.kt b/shared-core/src/commonMain/kotlin/dev/sdkforge/jwt/decode/core/Platform.kt similarity index 74% rename from shared-core/src/commonMain/kotlin/dev/sdkforge/template/core/Platform.kt rename to shared-core/src/commonMain/kotlin/dev/sdkforge/jwt/decode/core/Platform.kt index 14f599a..26c39a3 100644 --- a/shared-core/src/commonMain/kotlin/dev/sdkforge/template/core/Platform.kt +++ b/shared-core/src/commonMain/kotlin/dev/sdkforge/jwt/decode/core/Platform.kt @@ -1,4 +1,4 @@ -package dev.sdkforge.template.core +package dev.sdkforge.jwt.decode.core interface Platform { val name: String diff --git a/shared-core/src/commonTest/kotlin/dev/sdkforge/template/core/PlatformTest.kt b/shared-core/src/commonTest/kotlin/dev/sdkforge/jwt/decode/core/PlatformTest.kt similarity index 90% rename from shared-core/src/commonTest/kotlin/dev/sdkforge/template/core/PlatformTest.kt rename to shared-core/src/commonTest/kotlin/dev/sdkforge/jwt/decode/core/PlatformTest.kt index 3a9bfee..68231a3 100644 --- a/shared-core/src/commonTest/kotlin/dev/sdkforge/template/core/PlatformTest.kt +++ b/shared-core/src/commonTest/kotlin/dev/sdkforge/jwt/decode/core/PlatformTest.kt @@ -1,6 +1,6 @@ @file:Suppress("ktlint:standard:filename") -package dev.sdkforge.template.core +package dev.sdkforge.jwt.decode.core import kotlin.test.Test import kotlin.test.assertTrue diff --git a/shared-core/src/iosMain/kotlin/dev/sdkforge/template/core/Platform.ios.kt b/shared-core/src/iosMain/kotlin/dev/sdkforge/jwt/decode/core/Platform.ios.kt similarity index 86% rename from shared-core/src/iosMain/kotlin/dev/sdkforge/template/core/Platform.ios.kt rename to shared-core/src/iosMain/kotlin/dev/sdkforge/jwt/decode/core/Platform.ios.kt index 6d8ce7c..ee141ba 100644 --- a/shared-core/src/iosMain/kotlin/dev/sdkforge/template/core/Platform.ios.kt +++ b/shared-core/src/iosMain/kotlin/dev/sdkforge/jwt/decode/core/Platform.ios.kt @@ -1,4 +1,4 @@ -package dev.sdkforge.template.core +package dev.sdkforge.jwt.decode.core import platform.UIKit.UIDevice diff --git a/shared-core/src/iosTest/kotlin/dev/sdkforge/template/core/PlatformTest.ios.kt b/shared-core/src/iosTest/kotlin/dev/sdkforge/jwt/decode/core/PlatformTest.ios.kt similarity index 88% rename from shared-core/src/iosTest/kotlin/dev/sdkforge/template/core/PlatformTest.ios.kt rename to shared-core/src/iosTest/kotlin/dev/sdkforge/jwt/decode/core/PlatformTest.ios.kt index 0053846..7323517 100644 --- a/shared-core/src/iosTest/kotlin/dev/sdkforge/template/core/PlatformTest.ios.kt +++ b/shared-core/src/iosTest/kotlin/dev/sdkforge/jwt/decode/core/PlatformTest.ios.kt @@ -1,6 +1,6 @@ @file:Suppress("ktlint:standard:filename") -package dev.sdkforge.template.core +package dev.sdkforge.jwt.decode.core import kotlin.test.Test import kotlin.test.assertTrue diff --git a/shared/api/shared.api b/shared/api/shared.api index c1fd5a0..d896552 100644 --- a/shared/api/shared.api +++ b/shared/api/shared.api @@ -1,5 +1,5 @@ -public final class dev/sdkforge/template/Library { - public static final field INSTANCE Ldev/sdkforge/template/Library; +public final class dev/sdkforge/jwt/decode/Library { + public static final field INSTANCE Ldev/sdkforge/jwt/decode/Library; public static final field VERSION Ljava/lang/String; public fun equals (Ljava/lang/Object;)Z public fun hashCode ()I diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 0ad5432..03b8c7c 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -47,5 +47,5 @@ kotlin { } android { - namespace = "dev.sdkforge.template" + namespace = "dev.sdkforge.jwt.decode" } diff --git a/shared/src/commonMain/kotlin/dev/sdkforge/template/Library.kt b/shared/src/commonMain/kotlin/dev/sdkforge/jwt/decode/Library.kt similarity index 64% rename from shared/src/commonMain/kotlin/dev/sdkforge/template/Library.kt rename to shared/src/commonMain/kotlin/dev/sdkforge/jwt/decode/Library.kt index c0641f5..5619665 100644 --- a/shared/src/commonMain/kotlin/dev/sdkforge/template/Library.kt +++ b/shared/src/commonMain/kotlin/dev/sdkforge/jwt/decode/Library.kt @@ -1,4 +1,4 @@ -package dev.sdkforge.template +package dev.sdkforge.jwt.decode data object Library { const val VERSION = "0.0.1" From 813ee71827e462be0e70e93653faaacb79e2b9db Mon Sep 17 00:00:00 2001 From: Volodymyr Nevmerzhytskyi Date: Mon, 11 Aug 2025 10:51:24 +0300 Subject: [PATCH 2/3] - added initial classes and tests --- gradle/libs.versions.toml | 5 + settings.gradle.kts | 1 + shared-domain/api/shared-domain.api | 76 +++ shared-domain/build.gradle.kts | 30 ++ .../dependencies/releaseRuntimeClasspath.txt | 9 + .../sdkforge/jwt/decode/domain/BaseClaim.kt | 26 + .../dev/sdkforge/jwt/decode/domain/Claim.kt | 84 ++++ .../sdkforge/jwt/decode/domain/ClaimImpl.kt | 82 ++++ .../jwt/decode/domain/DecodeException.kt | 6 + .../dev/sdkforge/jwt/decode/domain/JWT.kt | 189 ++++++++ .../sdkforge/jwt/decode/domain/JWTPayload.kt | 100 ++++ .../jwt/decode/domain/BaseClaimTest.kt | 55 +++ .../jwt/decode/domain/ClaimImplTest.kt | 231 +++++++++ .../dev/sdkforge/jwt/decode/domain/JWTTest.kt | 446 ++++++++++++++++++ .../sdkforge/jwt/decode/domain/UserPojo.kt | 11 + .../dependencies/releaseRuntimeClasspath.txt | 7 + 16 files changed, 1358 insertions(+) create mode 100644 shared-domain/api/shared-domain.api create mode 100644 shared-domain/build.gradle.kts create mode 100644 shared-domain/dependencies/releaseRuntimeClasspath.txt create mode 100644 shared-domain/src/commonMain/kotlin/dev/sdkforge/jwt/decode/domain/BaseClaim.kt create mode 100644 shared-domain/src/commonMain/kotlin/dev/sdkforge/jwt/decode/domain/Claim.kt create mode 100644 shared-domain/src/commonMain/kotlin/dev/sdkforge/jwt/decode/domain/ClaimImpl.kt create mode 100644 shared-domain/src/commonMain/kotlin/dev/sdkforge/jwt/decode/domain/DecodeException.kt create mode 100644 shared-domain/src/commonMain/kotlin/dev/sdkforge/jwt/decode/domain/JWT.kt create mode 100644 shared-domain/src/commonMain/kotlin/dev/sdkforge/jwt/decode/domain/JWTPayload.kt create mode 100644 shared-domain/src/commonTest/kotlin/dev/sdkforge/jwt/decode/domain/BaseClaimTest.kt create mode 100644 shared-domain/src/commonTest/kotlin/dev/sdkforge/jwt/decode/domain/ClaimImplTest.kt create mode 100644 shared-domain/src/commonTest/kotlin/dev/sdkforge/jwt/decode/domain/JWTTest.kt create mode 100644 shared-domain/src/commonTest/kotlin/dev/sdkforge/jwt/decode/domain/UserPojo.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 13dcc8a..5504b75 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,6 +8,8 @@ androidx-activityCompose = "1.10.1" binary-compatibility-validator = "0.18.1" dokka = "2.0.0" dependency-guard = "0.5.0" +kotlinxDatetime = "0.7.1" +kotlinxSerializationJson = "1.9.0" kover = "0.9.1" versions = "0.52.0" benchmark = "1.4.0" @@ -26,6 +28,8 @@ compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" } compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose" } compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "compose-material3" } +kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } androidx-runner = { group = "androidx.test", name = "runner", version.ref = "runner" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junit" } junit = { group = "junit", name = "junit", version.ref = "junitVersion" } @@ -36,6 +40,7 @@ androidApplication = { id = "com.android.application", version.ref = "agp" } androidLibrary = { id = "com.android.library", version.ref = "agp" } kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } compose-multiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } binaryCompatibilityValidator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" } diff --git a/settings.gradle.kts b/settings.gradle.kts index bd2ef92..82576c4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -39,6 +39,7 @@ include(":app-android") include(":app-shared") include(":shared") include(":shared-core") +include(":shared-domain") include(":internal-ktlint") // uncomment if it's needed for development diff --git a/shared-domain/api/shared-domain.api b/shared-domain/api/shared-domain.api new file mode 100644 index 0000000..32de6fa --- /dev/null +++ b/shared-domain/api/shared-domain.api @@ -0,0 +1,76 @@ +public final class dev/sdkforge/jwt/decode/domain/AudienceAsStringSerializer : kotlinx/serialization/KSerializer { + public static final field INSTANCE Ldev/sdkforge/jwt/decode/domain/AudienceAsStringSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/util/List; + public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/util/List;)V +} + +public abstract interface class dev/sdkforge/jwt/decode/domain/Claim { + public static final field Companion Ldev/sdkforge/jwt/decode/domain/Claim$Companion; + public abstract fun asBoolean ()Ljava/lang/Boolean; + public abstract fun asDate ()Lkotlin/time/Instant; + public abstract fun asDouble ()Ljava/lang/Double; + public abstract fun asInt ()Ljava/lang/Integer; + public abstract fun asList (Lkotlinx/serialization/DeserializationStrategy;)Ljava/util/List; + public abstract fun asLong ()Ljava/lang/Long; + public abstract fun asObject (Lkotlinx/serialization/DeserializationStrategy;)Ljava/lang/Object; + public abstract fun asString ()Ljava/lang/String; +} + +public final class dev/sdkforge/jwt/decode/domain/Claim$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class dev/sdkforge/jwt/decode/domain/ClaimAsMapSerializer : kotlinx/serialization/KSerializer { + public static final field INSTANCE Ldev/sdkforge/jwt/decode/domain/ClaimAsMapSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/util/Map; + public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/util/Map;)V +} + +public final class dev/sdkforge/jwt/decode/domain/ClaimAsStringSerializer : kotlinx/serialization/KSerializer { + public static final field INSTANCE Ldev/sdkforge/jwt/decode/domain/ClaimAsStringSerializer; + public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ldev/sdkforge/jwt/decode/domain/Claim; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public fun serialize (Lkotlinx/serialization/encoding/Encoder;Ldev/sdkforge/jwt/decode/domain/Claim;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V +} + +public final class dev/sdkforge/jwt/decode/domain/DecodeException : java/lang/RuntimeException { +} + +public final class dev/sdkforge/jwt/decode/domain/InstantAsStringSerializer : kotlinx/serialization/KSerializer { + public static final field INSTANCE Ldev/sdkforge/jwt/decode/domain/InstantAsStringSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lkotlin/time/Instant; + public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lkotlin/time/Instant;)V +} + +public final class dev/sdkforge/jwt/decode/domain/JWT { + public static final field Companion Ldev/sdkforge/jwt/decode/domain/JWT$Companion; + public fun (Ljava/lang/String;)V + public final fun getAudience ()Ljava/util/List; + public final fun getClaim (Ljava/lang/String;)Ldev/sdkforge/jwt/decode/domain/Claim; + public final fun getClaims ()Ljava/util/Map; + public final fun getExpiresAt ()Lkotlin/time/Instant; + public final fun getHeader ()Ljava/util/Map; + public final fun getId ()Ljava/lang/String; + public final fun getIssuedAt ()Lkotlin/time/Instant; + public final fun getIssuer ()Ljava/lang/String; + public final fun getNotBefore ()Lkotlin/time/Instant; + public final fun getSignature ()Ljava/lang/String; + public final fun getSubject ()Ljava/lang/String; + public final fun isExpired-LRDsOJo (J)Z + public fun toString ()Ljava/lang/String; +} + +public final class dev/sdkforge/jwt/decode/domain/JWT$Companion { +} + diff --git a/shared-domain/build.gradle.kts b/shared-domain/build.gradle.kts new file mode 100644 index 0000000..ba0b5ef --- /dev/null +++ b/shared-domain/build.gradle.kts @@ -0,0 +1,30 @@ +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.androidLibrary) + alias(libs.plugins.kotlinSerialization) + alias(libs.plugins.binaryCompatibilityValidator) + alias(libs.plugins.dokka) + alias(libs.plugins.build.logic.library.kmp) + alias(libs.plugins.build.logic.library.android) + alias(libs.plugins.build.logic.library.publishing) +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(libs.kotlinx.datetime) + implementation(libs.kotlinx.serialization.json) + } + } + commonTest { + dependencies { + implementation(libs.kotlin.test) + } + } + } +} + +android { + namespace = "dev.sdkforge.jwt.decode.domain" +} diff --git a/shared-domain/dependencies/releaseRuntimeClasspath.txt b/shared-domain/dependencies/releaseRuntimeClasspath.txt new file mode 100644 index 0000000..7bd7eef --- /dev/null +++ b/shared-domain/dependencies/releaseRuntimeClasspath.txt @@ -0,0 +1,9 @@ +org.jetbrains.kotlin:kotlin-stdlib:2.2.20-Beta2 +org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.7.1 +org.jetbrains.kotlinx:kotlinx-datetime:0.7.1 +org.jetbrains.kotlinx:kotlinx-serialization-bom:1.9.0 +org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.9.0 +org.jetbrains.kotlinx:kotlinx-serialization-core:1.9.0 +org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.9.0 +org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0 +org.jetbrains:annotations:13.0 diff --git a/shared-domain/src/commonMain/kotlin/dev/sdkforge/jwt/decode/domain/BaseClaim.kt b/shared-domain/src/commonMain/kotlin/dev/sdkforge/jwt/decode/domain/BaseClaim.kt new file mode 100644 index 0000000..2e6768a --- /dev/null +++ b/shared-domain/src/commonMain/kotlin/dev/sdkforge/jwt/decode/domain/BaseClaim.kt @@ -0,0 +1,26 @@ +@file:Suppress("ktlint:standard:function-signature") + +package dev.sdkforge.jwt.decode.domain + +import kotlin.time.ExperimentalTime +import kotlin.time.Instant +import kotlinx.serialization.DeserializationStrategy + +/** + * The BaseClaim class is a Claim implementation that returns null when any of it's methods it's called. + */ +@OptIn(ExperimentalTime::class) +internal open class BaseClaim : Claim { + override fun asBoolean(): Boolean? = null + override fun asInt(): Int? = null + override fun asLong(): Long? = null + override fun asDouble(): Double? = null + override fun asString(): String? = null + override fun asDate(): Instant? = null + + @Throws(DecodeException::class) + override fun asList(deserializer: DeserializationStrategy): List = emptyList() + + @Throws(DecodeException::class) + override fun asObject(deserializer: DeserializationStrategy): T? = null +} diff --git a/shared-domain/src/commonMain/kotlin/dev/sdkforge/jwt/decode/domain/Claim.kt b/shared-domain/src/commonMain/kotlin/dev/sdkforge/jwt/decode/domain/Claim.kt new file mode 100644 index 0000000..55bc5f8 --- /dev/null +++ b/shared-domain/src/commonMain/kotlin/dev/sdkforge/jwt/decode/domain/Claim.kt @@ -0,0 +1,84 @@ +@file:Suppress("ktlint:standard:function-signature") + +package dev.sdkforge.jwt.decode.domain + +import kotlin.time.ExperimentalTime +import kotlin.time.Instant +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.Serializable + +/** + * The Claim class holds the value in a generic way so that it can be recovered in many representations. + */ + +@OptIn(ExperimentalTime::class) +@Serializable(with = ClaimAsStringSerializer::class) +interface Claim { + /** + * Get this Claim as a Boolean. + * If the value isn't of type Boolean or it can't be converted to a Boolean, null will be returned. + * + * @return the value as a Boolean or null. + */ + fun asBoolean(): Boolean? + + /** + * Get this Claim as an Integer. + * If the value isn't of type Integer or it can't be converted to an Integer, null will be returned. + * + * @return the value as an Integer or null. + */ + fun asInt(): Int? + + /** + * Get this Claim as an Long. + * If the value isn't of type Long or it can't be converted to an Long, null will be returned. + * + * @return the value as an Long or null. + */ + fun asLong(): Long? + + /** + * Get this Claim as a Double. + * If the value isn't of type Double or it can't be converted to a Double, null will be returned. + * + * @return the value as a Double or null. + */ + fun asDouble(): Double? + + /** + * Get this Claim as a String. + * If the value isn't of type String or it can't be converted to a String, null will be returned. + * + * @return the value as a String or null. + */ + fun asString(): String? + + /** + * Get this Claim as a Date. + * If the value can't be converted to a Date, null will be returned. + * + * @return the value as a Date or null. + */ + fun asDate(): Instant? + + /** + * Get this Claim as a List of type T. + * If the value isn't an Array, an empty List will be returned. + * + * @return the value as a List or an empty List. + * @throws DecodeException if the values inside the List can't be converted to a class T. + */ + @Throws(DecodeException::class) + fun asList(deserializer: DeserializationStrategy): List + + /** + * Get this Claim as a Object of type T. + * If the value isn't of type Object, null will be returned. + * + * @return the value as a Object of type T or null. + * @throws DecodeException if the value can't be converted to a class T. + */ + @Throws(DecodeException::class) + fun asObject(deserializer: DeserializationStrategy): T? +} diff --git a/shared-domain/src/commonMain/kotlin/dev/sdkforge/jwt/decode/domain/ClaimImpl.kt b/shared-domain/src/commonMain/kotlin/dev/sdkforge/jwt/decode/domain/ClaimImpl.kt new file mode 100644 index 0000000..fe5c7d0 --- /dev/null +++ b/shared-domain/src/commonMain/kotlin/dev/sdkforge/jwt/decode/domain/ClaimImpl.kt @@ -0,0 +1,82 @@ +@file:Suppress("ktlint:standard:function-signature") + +package dev.sdkforge.jwt.decode.domain + +import kotlin.time.ExperimentalTime +import kotlin.time.Instant +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.doubleOrNull +import kotlinx.serialization.json.intOrNull +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.longOrNull + +/** + * The ClaimImpl class implements the Claim interface. + */ +@OptIn(ExperimentalTime::class) +internal class ClaimImpl(private val value: JsonElement) : BaseClaim() { + + override fun asBoolean(): Boolean? = when (value) { + !is JsonPrimitive -> null + else -> value.jsonPrimitive.booleanOrNull + } + + override fun asInt(): Int? = when (value) { + !is JsonPrimitive -> null + else -> value.jsonPrimitive.intOrNull + } + + override fun asLong(): Long? = when (value) { + !is JsonPrimitive -> null + else -> value.jsonPrimitive.longOrNull + } + + override fun asDouble(): Double? = when (value) { + !is JsonPrimitive -> null + else -> value.jsonPrimitive.doubleOrNull + } + + override fun asString(): String? = when (value) { + !is JsonPrimitive -> null + else -> value.jsonPrimitive.content + } + + override fun asDate(): Instant? = when (value) { + !is JsonPrimitive -> null + else -> value.jsonPrimitive.longOrNull?.run { + Instant.fromEpochSeconds(this) + } + } + + @Throws(DecodeException::class) + override fun asList(deserializer: DeserializationStrategy): List { + try { + if (value !is JsonArray) { + return emptyList() + } + + return List(value.size) { index -> Json.decodeFromJsonElement(deserializer, value[index]) } + } catch (e: IllegalArgumentException) { + throw DecodeException("Failed to decode claim as list", e) + } + } + + @Throws(DecodeException::class) + override fun asObject(deserializer: DeserializationStrategy): T? { + try { + if (value is JsonNull) { + return null + } + + return Json.decodeFromJsonElement(deserializer, value) + } catch (e: IllegalArgumentException) { + throw DecodeException("Failed to decode claim", e) + } + } +} diff --git a/shared-domain/src/commonMain/kotlin/dev/sdkforge/jwt/decode/domain/DecodeException.kt b/shared-domain/src/commonMain/kotlin/dev/sdkforge/jwt/decode/domain/DecodeException.kt new file mode 100644 index 0000000..ad84733 --- /dev/null +++ b/shared-domain/src/commonMain/kotlin/dev/sdkforge/jwt/decode/domain/DecodeException.kt @@ -0,0 +1,6 @@ +package dev.sdkforge.jwt.decode.domain + +class DecodeException : RuntimeException { + internal constructor(message: String?) : super(message) + internal constructor(message: String?, cause: Throwable?) : super(message, cause) +} diff --git a/shared-domain/src/commonMain/kotlin/dev/sdkforge/jwt/decode/domain/JWT.kt b/shared-domain/src/commonMain/kotlin/dev/sdkforge/jwt/decode/domain/JWT.kt new file mode 100644 index 0000000..d6485d5 --- /dev/null +++ b/shared-domain/src/commonMain/kotlin/dev/sdkforge/jwt/decode/domain/JWT.kt @@ -0,0 +1,189 @@ +@file:Suppress("ktlint:standard:function-signature", "ktlint:standard:function-expression-body") + +package dev.sdkforge.jwt.decode.domain + +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi +import kotlin.time.Clock +import kotlin.time.Duration +import kotlin.time.ExperimentalTime +import kotlin.time.Instant +import kotlinx.serialization.json.Json + +/** + * Wrapper class for values contained inside a Json Web Token (JWT). + */ +@OptIn(ExperimentalTime::class) +class JWT(private var token: String) { + + private val json: Json = Json { ignoreUnknownKeys = true } + + private var payload: JWTPayload? = null + + /** + * Decode a given string JWT token. + * + * @param token the string JWT token. + * @throws DecodeException if the token cannot be decoded + */ + init { + decode(token) + + this.token = token + } + + /** + * Get the Header values from this JWT as a Map of Strings. + * + * @return the Header values of the JWT. + */ + var header: Map? = null + private set + + /** + * Get the Signature from this JWT as a Base64 encoded String. + * + * @return the Signature of the JWT. + */ + var signature: String? = null + private set + + /** + * Get the value of the "iss" claim, or null if it's not available. + * + * @return the Issuer value or null. + */ + val issuer: String? get() = payload?.iss + + /** + * Get the value of the "sub" claim, or null if it's not available. + * + * @return the Subject value or null. + */ + val subject: String? get() = payload?.sub + + /** + * Get the value of the "aud" claim, or an empty list if it's not available. + * + * @return the Audience value or an empty list. + */ + val audience: List get() = payload?.aud.orEmpty() + + /** + * Get the value of the "exp" claim, or null if it's not available. + * + * @return the Expiration Time value or null. + */ + val expiresAt: Instant? get() = payload?.exp + + /** + * Get the value of the "nbf" claim, or null if it's not available. + * + * @return the Not Before value or null. + */ + val notBefore: Instant? get() = payload?.nbf + + /** + * Get the value of the "iat" claim, or null if it's not available. + * + * @return the Issued At value or null. + */ + val issuedAt: Instant? get() = payload?.iat + + /** + * Get the value of the "jti" claim, or null if it's not available. + * + * @return the JWT ID value or null. + */ + val id: String? get() = payload?.jti + + /** + * Get a Claim given it's name. If the Claim wasn't specified in the JWT payload, a BaseClaim will be returned. + * + * @param name the name of the Claim to retrieve. + * @return a valid Claim. + */ + fun getClaim(name: String): Claim { + return payload?.claimForName(name) ?: BaseClaim() + } + + /** + * Get all the Claims. + * + * @return a valid Map of Claims. + */ + val claims: Map? get() = payload?.tree.orEmpty() + + /** + * Validates that this JWT was issued in the past and hasn't expired yet. + * + * @param leeway the time leeway in seconds in which the token should still be considered valid. + * @return if this JWT has already expired or not. + */ + fun isExpired(leeway: Duration): Boolean { + require(leeway.inWholeSeconds >= 0) { "The leeway must be a positive value." } + val todayTime = Instant.fromEpochSeconds(Clock.System.now().epochSeconds) + val futureToday = (todayTime + leeway) + val pastToday = (todayTime - leeway) + val exp = payload?.exp + val iat = payload?.iat + val expValid = exp == null || pastToday <= exp + val iatValid = iat == null || futureToday >= iat + return !expValid || !iatValid + } + + /** + * Returns the String representation of this JWT. + * + * @return the String Token. + */ + override fun toString(): String = token + + // ===================================== + // ===========Private Methods=========== + // ===================================== + private fun decode(token: String) { + val parts = splitToken(token) + + header = parseJson?>(base64Decode(parts[0])) + payload = parseJson(base64Decode(parts[1])) + signature = parts[2] + } + + private fun splitToken(token: String): List { + var parts: List = token.split('.') + if (parts.size == 2 && token.endsWith('.')) { + // Tokens with alg='none' have empty String as Signature. + parts = listOf(parts[0], parts[1], "") + } + if (parts.size != 3) { + throw DecodeException("The token was expected to have 3 parts, but got ${parts.size}.") + } + return parts + } + + @OptIn(ExperimentalEncodingApi::class) + private fun base64Decode(string: String?): String? { + string ?: return null + + try { + return Base64.UrlSafe.withPadding(Base64.PaddingOption.ABSENT).decode(string).decodeToString() + } catch (e: IllegalArgumentException) { + throw DecodeException("Received bytes didn't correspond to a valid Base64 encoded string.", e) + } + } + + private inline fun parseJson(json: String?): T? { + json ?: return null + + try { + return this.json.decodeFromString(json) + } catch (e: Exception) { + throw DecodeException("The token's payload had an invalid JSON format.", e) + } + } + + companion object { + private val TAG: String? = JWT::class.simpleName + } +} diff --git a/shared-domain/src/commonMain/kotlin/dev/sdkforge/jwt/decode/domain/JWTPayload.kt b/shared-domain/src/commonMain/kotlin/dev/sdkforge/jwt/decode/domain/JWTPayload.kt new file mode 100644 index 0000000..e3ecd60 --- /dev/null +++ b/shared-domain/src/commonMain/kotlin/dev/sdkforge/jwt/decode/domain/JWTPayload.kt @@ -0,0 +1,100 @@ +@file:Suppress("ktlint:standard:function-signature", "ktlint:standard:function-expression-body") + +package dev.sdkforge.jwt.decode.domain + +import kotlin.time.ExperimentalTime +import kotlin.time.Instant +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.builtins.MapSerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.listSerialDescriptor +import kotlinx.serialization.descriptors.mapSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.Json + +@OptIn(ExperimentalTime::class) +@Serializable +internal class JWTPayload( + @SerialName("iss") val iss: String? = null, + @SerialName("sub") val sub: String? = null, + @Serializable(with = InstantAsStringSerializer::class) @SerialName("exp") val exp: Instant? = null, + @Serializable(with = InstantAsStringSerializer::class) @SerialName("nbf") val nbf: Instant? = null, + @Serializable(with = InstantAsStringSerializer::class) @SerialName("iat") val iat: Instant? = null, + @SerialName("jti") val jti: String? = null, + @Serializable(with = AudienceAsStringSerializer::class) @SerialName("aud") val aud: List? = null, + @Serializable(with = ClaimAsMapSerializer::class) @SerialName("tree") val tree: Map? = null, +) { + internal fun claimForName(name: String): Claim { + return this.tree?.get(name) ?: BaseClaim() + } +} + +@OptIn(ExperimentalTime::class) +object InstantAsStringSerializer : KSerializer { + // Serial names of descriptors should be unique, this is why we advise including app package in the name. + override val descriptor: SerialDescriptor + get() = PrimitiveSerialDescriptor("dev.sdkforge.jwt.decode.domain.Instant", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: Instant) { + encoder.encodeString(value.epochSeconds.toString()) + } + + override fun deserialize(decoder: Decoder): Instant { + val seconds = decoder.decodeString() + return Instant.fromEpochSeconds(seconds.toLong()) + } +} + +object ClaimAsStringSerializer : KSerializer { + override val descriptor: SerialDescriptor + get() = PrimitiveSerialDescriptor("dev.sdkforge.jwt.decode.domain.Claim", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: Claim) { + encoder.encodeString(value.toString()) + } + + override fun deserialize(decoder: Decoder): Claim { + return ClaimImpl(Json.parseToJsonElement(decoder.decodeString())) + } +} + +object ClaimAsMapSerializer : KSerializer> { + @OptIn(ExperimentalSerializationApi::class) + override val descriptor: SerialDescriptor + get() = mapSerialDescriptor() + + override fun serialize(encoder: Encoder, value: Map) { + encoder.encodeString(value.toString()) + } + + override fun deserialize(decoder: Decoder): Map { + return decoder.decodeSerializableValue>(MapSerializer(String.serializer(), Claim.serializer())) + } +} + +object AudienceAsStringSerializer : KSerializer> { + @OptIn(ExperimentalSerializationApi::class) + override val descriptor: SerialDescriptor + get() = listSerialDescriptor() + + override fun serialize(encoder: Encoder, value: List) { + encoder.encodeString(value.toString()) + } + + @OptIn(ExperimentalSerializationApi::class) + override fun deserialize(decoder: Decoder): List { + val singleAudience = decoder.decodeNullableSerializableValue(String.serializer()) + + if (singleAudience != null) return listOf(singleAudience) + + return decoder.decodeSerializableValue>(ListSerializer(String.serializer())) + } +} diff --git a/shared-domain/src/commonTest/kotlin/dev/sdkforge/jwt/decode/domain/BaseClaimTest.kt b/shared-domain/src/commonTest/kotlin/dev/sdkforge/jwt/decode/domain/BaseClaimTest.kt new file mode 100644 index 0000000..3056f16 --- /dev/null +++ b/shared-domain/src/commonTest/kotlin/dev/sdkforge/jwt/decode/domain/BaseClaimTest.kt @@ -0,0 +1,55 @@ +package dev.sdkforge.jwt.decode.domain + +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.time.ExperimentalTime +import kotlinx.serialization.builtins.serializer + +@OptIn(ExperimentalTime::class) +class BaseClaimTest { + + private val claim: BaseClaim = BaseClaim() + + @Test + fun shouldGetAsBoolean() { + assertNull(claim.asBoolean()) + } + + @Test + fun shouldGetAsInt() { + assertNull(claim.asInt()) + } + + @Test + fun shouldGetAsLong() { + assertNull(claim.asLong()) + } + + @Test + fun shouldGetAsDouble() { + assertNull(claim.asDouble()) + } + + @Test + fun shouldGetAsString() { + assertNull(claim.asString()) + } + + @Test + fun shouldGetAsDate() { + assertNull(claim.asDate()) + } + + @Test + fun shouldGetAsList() { + assertNotNull(claim.asList(Unit.serializer())) + assertContentEquals(emptyList(), claim.asList(Unit.serializer())) + } + + @Test + fun shouldGetAsObject() { + assertNull(claim.asObject(Unit.serializer())) + } +} diff --git a/shared-domain/src/commonTest/kotlin/dev/sdkforge/jwt/decode/domain/ClaimImplTest.kt b/shared-domain/src/commonTest/kotlin/dev/sdkforge/jwt/decode/domain/ClaimImplTest.kt new file mode 100644 index 0000000..226a208 --- /dev/null +++ b/shared-domain/src/commonTest/kotlin/dev/sdkforge/jwt/decode/domain/ClaimImplTest.kt @@ -0,0 +1,231 @@ +package dev.sdkforge.jwt.decode.domain + +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.time.ExperimentalTime +import kotlin.time.Instant +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.encodeToJsonElement + +@OptIn(ExperimentalTime::class) +class ClaimImplTest { + private val json = Json.Default + + @Test + fun shouldGetBooleanValue() { + val value: JsonElement = json.encodeToJsonElement(true) + val claim = ClaimImpl(value) + + assertNotNull(claim.asBoolean()) + assertEquals(true, claim.asBoolean()) + } + + @Test + fun shouldGetNullBooleanIfNotPrimitiveValue() { + val value: JsonElement = json.encodeToJsonElement(Unit) + val claim = ClaimImpl(value) + + assertNull(claim.asBoolean()) + } + + @Test + fun shouldGetIntValue() { + val value: JsonElement = json.encodeToJsonElement(123) + val claim = ClaimImpl(value) + + assertNotNull(claim.asInt()) + assertEquals(123, claim.asInt()) + } + + @Test + fun shouldGetLongValue() { + val value: JsonElement = json.encodeToJsonElement(123L) + val claim = ClaimImpl(value) + + assertNotNull(claim.asLong()) + assertEquals(123L, claim.asLong()) + } + + @Test + fun shouldGetNullIntIfNotPrimitiveValue() { + val value: JsonElement = json.encodeToJsonElement(Unit) + val claim = ClaimImpl(value) + + assertNull(claim.asInt()) + } + + @Test + fun shouldGetNullLongIfNotPrimitiveValue() { + val value: JsonElement = json.encodeToJsonElement(Unit) + val claim = ClaimImpl(value) + + assertNull(claim.asLong()) + } + + @Test + fun shouldGetDoubleValue() { + val value: JsonElement = json.encodeToJsonElement(1.5) + val claim = ClaimImpl(value) + + assertNotNull(claim.asDouble()) + assertEquals(1.5, claim.asDouble()) + } + + @Test + fun shouldGetNullDoubleIfNotPrimitiveValue() { + val value: JsonElement = json.encodeToJsonElement(Unit) + val claim = ClaimImpl(value) + + assertNull(claim.asDouble()) + } + + @Test + fun shouldGetLargeDateValue() { + val seconds: Long = Int.MAX_VALUE + 10000L + val value: JsonElement = json.encodeToJsonElement(seconds) + val claim = ClaimImpl(value) + + val date: Instant? = claim.asDate() + assertNotNull(date) + assertEquals(seconds, date.epochSeconds) + assertEquals(2147493647, date.epochSeconds) + } + + @Test + fun shouldGetDateValue() { + val value: JsonElement = json.encodeToJsonElement("1476824844") + val claim = ClaimImpl(value) + + assertNotNull(claim.asDate()) + assertEquals(Instant.fromEpochSeconds(1476824844), claim.asDate()) + } + + @Test + fun shouldGetNullDateIfNotPrimitiveValue() { + val value: JsonElement = json.encodeToJsonElement(Unit) + val claim = ClaimImpl(value) + + assertNull(claim.asDate()) + } + + @Test + fun shouldGetStringValue() { + val value: JsonElement = json.encodeToJsonElement("string") + val claim = ClaimImpl(value) + + assertNotNull(claim.asString()) + assertEquals("string", claim.asString()) + } + + @Test + fun shouldGetNullStringIfNotPrimitiveValue() { + val value: JsonElement = json.encodeToJsonElement(Unit) + val claim = ClaimImpl(value) + + assertNull(claim.asString()) + } + + @Test + fun shouldGetListValueOfCustomClass() { + val value: JsonElement = json.encodeToJsonElement(listOf(UserPojo("George", 1), UserPojo("Mark", 2))) + val claim = ClaimImpl(value) + + assertNotNull(claim.asList(UserPojo.serializer())) + assertContentEquals( + listOf(UserPojo("George", 1), UserPojo("Mark", 2)), + claim.asList(UserPojo.serializer()), + ) + } + + @Test + fun shouldGetListValue() { + val value: JsonElement = json.encodeToJsonElement(listOf("string1", "string2")) + val claim = ClaimImpl(value) + + assertNotNull(claim.asList(String.serializer())) + assertContentEquals( + listOf("string1", "string2"), + claim.asList(String.serializer()), + ) + } + + @Test + fun shouldGetEmptyListIfNullValue() { + val value: JsonElement = json.encodeToJsonElement(null.orEmpty()) + val claim = ClaimImpl(value) + + assertNotNull(claim.asList(String.serializer())) + assertContentEquals( + emptyList(), + claim.asList(String.serializer()), + ) + } + + @Test + fun shouldGetEmptyListIfNonArrayValue() { + val value: JsonElement = json.encodeToJsonElement(1) + val claim = ClaimImpl(value) + + assertNotNull(claim.asList(String.serializer())) + assertContentEquals( + emptyList(), + claim.asList(String.serializer()), + ) + } + + @Test + fun shouldThrowIfListClassMismatch() { + val value: JsonElement = json.encodeToJsonElement(arrayOf("keys", "values")) + val claim = ClaimImpl(value) + + assertFailsWith { + claim.asList(UserPojo.serializer()) + } + } + + @Test + fun shouldGetAsObject() { + val data = UserPojo("George", 1) + val userValue: JsonElement = json.encodeToJsonElement(data) + val userClaim = ClaimImpl(userValue) + + val intValue: JsonElement = json.encodeToJsonElement(1) + val intClaim = ClaimImpl(intValue) + + val booleanValue: JsonElement = json.encodeToJsonElement(true) + val booleanClaim = ClaimImpl(booleanValue) + + assertNotNull(userClaim.asObject(UserPojo.serializer())) + assertEquals(UserPojo("George", 1), userClaim.asObject(UserPojo.serializer())) + + assertNotNull(intClaim.asObject(Int.serializer())) + assertEquals(1, intClaim.asObject(Int.serializer())) + + assertNotNull(booleanClaim.asObject(Boolean.serializer())) + assertEquals(true, booleanClaim.asObject(Boolean.serializer())) + } + + @Test + fun shouldGetNullObjectIfNullValue() { + val claim = ClaimImpl(JsonNull) + + assertNull(claim.asObject(UserPojo.serializer())) + } + + @Test + fun shouldThrowIfObjectClassMismatch() { + val value: JsonElement = json.encodeToJsonElement(1) + val claim = ClaimImpl(value) + + assertFailsWith { + claim.asObject(UserPojo.serializer()) + } + } +} diff --git a/shared-domain/src/commonTest/kotlin/dev/sdkforge/jwt/decode/domain/JWTTest.kt b/shared-domain/src/commonTest/kotlin/dev/sdkforge/jwt/decode/domain/JWTTest.kt new file mode 100644 index 0000000..c97075a --- /dev/null +++ b/shared-domain/src/commonTest/kotlin/dev/sdkforge/jwt/decode/domain/JWTTest.kt @@ -0,0 +1,446 @@ +@file:Suppress("ktlint:standard:function-signature", "ktlint:standard:function-expression-body") + +package dev.sdkforge.jwt.decode.domain + +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi +import kotlin.test.Ignore +import kotlin.test.Test +import kotlin.test.assertContains +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue +import kotlin.time.Clock +import kotlin.time.Duration.Companion.seconds +import kotlin.time.ExperimentalTime +import kotlin.time.Instant + +@OptIn(ExperimentalTime::class) +class JWTTest { + + // Exceptions + @Test + fun shouldThrowIfLessThan3Parts() { + assertFailsWith( + message = "The token was expected to have 3 parts, but got 2.", + ) { + JWT("two.parts") + } + } + + @Test + fun shouldThrowIfMoreThan3Parts() { + assertFailsWith( + message = "The token was expected to have 3 parts, but got 4.", + ) { + JWT("this.has.four.parts") + } + } + + @Test + fun shouldThrowIfItsNotBase64Encoded() { + assertFailsWith( + message = "Received bytes didn't correspond to a valid Base64 encoded string.", + ) { + JWT("thisIsNot.Base64_Enc.oded") + } + } + + @Test + fun shouldThrowIfPayloadHasInvalidJSONFormat() { + assertFailsWith( + message = "The token's payload had an invalid JSON format.", + ) { + JWT("eyJhbGciOiJIUzI1NiJ9.e30ijfe923.XmNK3GpH3Ys_7lyQ") + } + } + + // toString + @Test + fun shouldGetStringToken() { + val jwt = JWT("eyJhbGciOiJIUzI1NiJ9.e30.XmNK3GpH3Ys_7wsYBfq4C3M6goz71I7dTgUkuIa5lyQ") + + assertEquals("eyJhbGciOiJIUzI1NiJ9.e30.XmNK3GpH3Ys_7wsYBfq4C3M6goz71I7dTgUkuIa5lyQ", jwt.toString()) + } + + // Parts + @Test + fun shouldGetHeader() { + val jwt = JWT("eyJhbGciOiJIUzI1NiJ9.e30.XmNK3GpH3Ys_7wsYBfq4C3M6goz71I7dTgUkuIa5lyQ") + val header = jwt.header + + assertNotNull(header) + assertEquals("HS256", header["alg"]) + } + + @Test + fun shouldGetSignature() { + val jwt = JWT("eyJhbGciOiJIUzI1NiJ9.e30.XmNK3GpH3Ys_7wsYBfq4C3M6goz71I7dTgUkuIa5lyQ") + assertEquals("XmNK3GpH3Ys_7wsYBfq4C3M6goz71I7dTgUkuIa5lyQ", jwt.signature) + } + + @Test + fun shouldGetEmptySignature() { + val jwt = JWT("eyJhbGciOiJIUzI1NiJ9.e30.") + + assertEquals("", jwt.signature) + } + + // Public Claims + @Test + fun shouldGetIssuer() { + val jwt = JWT("eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJKb2huIERvZSJ9.SgXosfRR_IwCgHq5lF3tlM-JHtpucWCRSaVuoHTbWbQ") + assertEquals("John Doe", jwt.issuer) + } + + @Test + fun shouldGetNullIssuerIfMissing() { + val jwt = JWT("eyJhbGciOiJIUzI1NiJ9.e30.something") + + assertNull(jwt.issuer) + } + + @Test + fun shouldGetSubject() { + val jwt = JWT("eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJUb2szbnMifQ.RudAxkslimoOY3BLl2Ghny3BrUKu9I1ZrXzCZGDJtNs") + + assertEquals("Tok3ns", jwt.subject) + } + + @Test + fun shouldGetNullSubjectIfMissing() { + val jwt = JWT("eyJhbGciOiJIUzI1NiJ9.e30.something") + + assertNull(jwt.subject) + } + + @Ignore + @Test + fun shouldGetArrayAudience() { + val jwt = JWT("eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOlsiSG9wZSIsIlRyYXZpcyIsIlNvbG9tb24iXX0.Tm4W8WnfPjlmHSmKFakdij0on2rWPETpoM7Sh0u6-S4") + + val audience = jwt.audience + + assertNotNull(audience) + assertEquals(3, audience.size) + assertContains(audience, "Hope") + assertContains(audience, "Travis") + assertContains(audience, "Solomon") + } + + @Test + fun shouldGetStringAudience() { + val jwt = JWT("eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJKYWNrIFJleWVzIn0.a4I9BBhPt1OB1GW67g2P1bEHgi6zgOjGUL4LvhE9Dgc") + + val audience = jwt.audience + + assertNotNull(audience) + assertEquals(1, audience.size) + assertContains(audience, "Jack Reyes") + } + + @Test + fun shouldGetEmptyListAudienceIfMissing() { + val jwt = JWT("eyJhbGciOiJIUzI1NiJ9.e30.something") + + val audience = jwt.audience + + assertNotNull(audience) + assertEquals(emptyList(), audience) + } + + @Ignore + @Test + fun shouldDeserializeDatesUsingLong() { + val jwt = JWT( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjIxNDc0OTM2NDcsIm5iZiI6MjE0NzQ5MzY0NywiZXhwIjoyMTQ3NDkzNjQ3LCJjdG0iOjIxNDc0OTM2NDd9.txmUJ0UCy2pqTFrEgj49eNDQCWUSW_XRMjMaRqcrgLg", + ) + + val seconds: Long = Int.MAX_VALUE + 10000L + val expectedDate = Instant.fromEpochSeconds(seconds) + + assertEquals(expectedDate, jwt.issuedAt) + assertEquals(expectedDate, jwt.notBefore) + assertEquals(expectedDate, jwt.expiresAt) + assertEquals(expectedDate, jwt.getClaim("ctm").asDate()) + } + + @Test + fun shouldGetExpirationTime() { + val jwt = JWT("eyJhbGciOiJIUzI1NiJ9.eyJleHAiOiIxNDc2NzI3MDg2In0.XwZztHlQwnAgmnQvrcWXJloLOUaLZGiY0HOXJCKRaks") + val expectedDate = Instant.fromEpochSeconds(1476727086L) + + assertNotNull(jwt.expiresAt) + assertEquals(expectedDate, jwt.expiresAt) + } + + @Test + fun shouldGetNullExpirationTimeIfMissing() { + val jwt = JWT("eyJhbGciOiJIUzI1NiJ9.e30.something") + + assertNull(jwt.expiresAt) + } + + @Test + fun shouldGetNotBefore() { + val jwt = JWT("eyJhbGciOiJIUzI1NiJ9.eyJuYmYiOiIxNDc2NzI3MDg2In0.pi3Fi3oFiXk5A5AetDdL0hjVx_rt6F5r_YiG6HoCYDw") + val expectedDate = Instant.fromEpochSeconds(1476727086L) + + assertNotNull(jwt.notBefore) + assertEquals(expectedDate, jwt.notBefore) + } + + @Test + fun shouldGetNullNotBeforeIfMissing() { + val jwt = JWT("eyJhbGciOiJIUzI1NiJ9.e30.something") + + assertNull(jwt.notBefore) + } + + @Test + fun shouldGetIssuedAt() { + val jwt = JWT("eyJhbGciOiJIUzI1NiJ9.eyJpYXQiOiIxNDc2NzI3MDg2In0.u6BxwrO7S0sqDY8-1cUOLzU2uejAJBzQQF8g_o5BAgo") + + val expectedDate = Instant.fromEpochSeconds(1476727086L) + assertNotNull(jwt.issuedAt) + assertEquals(expectedDate, jwt.issuedAt) + } + + @Test + fun shouldGetNullIssuedAtIfMissing() { + val jwt = JWT("eyJhbGciOiJIUzI1NiJ9.e30.something") + + assertNull(jwt.issuedAt) + } + + @Test + fun shouldGetId() { + val jwt = JWT("eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMjM0NTY3ODkwIn0.m3zgEfVUFOd-CvL3xG5BuOWLzb0zMQZCqiVNQQOPOvA") + + assertEquals("1234567890", jwt.id) + } + + @Test + fun shouldGetNullIdIfMissing() { + val jwt = JWT("eyJhbGciOiJIUzI1NiJ9.e30.something") + + assertNull(jwt.id) + } + + @Test + fun shouldNotBeDeemedExpiredWithoutDateClaims() { + val jwt = customTimeJWT(null, null) + + assertFalse { jwt.isExpired(0.seconds) } + } + + @Test + fun shouldNotBeDeemedExpired() { + val jwt = customTimeJWT(null, Clock.System.now().toEpochMilliseconds() + 2000) + + assertFalse { jwt.isExpired(0.seconds) } + } + + @Test + fun shouldBeDeemedExpired() { + val jwt = customTimeJWT(null, Clock.System.now().toEpochMilliseconds() - 2000) + + assertTrue { jwt.isExpired(0.seconds) } + } + + @Test + fun shouldNotBeDeemedExpiredByLeeway() { + val jwt = customTimeJWT(null, Clock.System.now().toEpochMilliseconds() - 1000) + + assertFalse { jwt.isExpired(2.seconds) } + } + + @Test + fun shouldBeDeemedExpiredByLeeway() { + val jwt = customTimeJWT(null, Clock.System.now().toEpochMilliseconds() - 2000) + + assertTrue { jwt.isExpired(1.seconds) } + } + + @Test + fun shouldNotBeDeemedFutureIssued() { + val jwt = customTimeJWT(Clock.System.now().toEpochMilliseconds() - 2000, null) + + assertFalse { jwt.isExpired(0.seconds) } + } + + @Test + fun shouldBeDeemedFutureIssued() { + val jwt = customTimeJWT(Clock.System.now().toEpochMilliseconds() + 2000, null) + + assertTrue { jwt.isExpired(0.seconds) } + } + + @Test + fun shouldNotBeDeemedFutureIssuedByLeeway() { + val jwt = customTimeJWT(Clock.System.now().toEpochMilliseconds() + 1000, null) + + assertFalse { jwt.isExpired(2.seconds) } + } + + @Test + fun shouldBeDeemedFutureIssuedByLeeway() { + val jwt = customTimeJWT(Clock.System.now().toEpochMilliseconds() + 2000, null) + + assertTrue { jwt.isExpired(1.seconds) } + } + + @Test + fun shouldBeDeemedNotTimeValid() { + val jwt = customTimeJWT(Clock.System.now().toEpochMilliseconds() + 1000, Clock.System.now().toEpochMilliseconds() - 1000) + + assertTrue { jwt.isExpired(0.seconds) } + } + + @Test + fun shouldBeDeemedTimeValid() { + val jwt = customTimeJWT(Clock.System.now().toEpochMilliseconds() - 1000, Clock.System.now().toEpochMilliseconds() + 1000) + + assertFalse { jwt.isExpired(0.seconds) } + } + + @Test + fun shouldThrowIfLeewayIsNegative() { + assertFailsWith( + message = "The leeway must be a positive value.", + ) { + customTimeJWT(null, null).isExpired(-(1.seconds)) + } + } + + @Ignore + @Test + fun shouldNotRemoveKnownPublicClaimsFromTree() { + val jwt = JWT( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCIsInN1YiI6ImVtYWlscyIsImF1ZCI6InVzZXJzIiwiaWF0IjoxMDEwMTAxMCwiZXhwIjoxMTExMTExMSwibmJmIjoxMDEwMTAxMSwianRpIjoiaWRpZCIsInJvbGVzIjoiYWRtaW4ifQ.jCchxb-mdMTq5EpeVMSQyTp6zSwByKnfl9U-Zc9kg_w", + ) + + assertEquals("auth0", jwt.issuer) + assertEquals("emails", jwt.subject) + assertContains(jwt.audience.orEmpty(), "users") + + assertEquals(Instant.fromEpochSeconds(10101010L), jwt.issuedAt) + assertEquals(Instant.fromEpochSeconds(11111111L), jwt.expiresAt) + assertEquals(Instant.fromEpochSeconds(10101011L), jwt.notBefore) + assertEquals("idid", jwt.id) + + assertEquals("admin", jwt.getClaim("roles").asString()) + assertEquals("auth0", jwt.getClaim("iss").asString()) + assertEquals("emails", jwt.getClaim("sub").asString()) + assertEquals("users", jwt.getClaim("aud").asString()) + assertEquals(10101010.0, jwt.getClaim("iat").asDouble()) + assertEquals(11111111.0, jwt.getClaim("exp").asDouble()) + assertEquals(10101011.0, jwt.getClaim("nbf").asDouble()) + assertEquals("idid", jwt.getClaim("jti").asString()) + } + + // Private Claims + @Ignore + @Test + fun shouldGetBaseClaimIfClaimIsMissing() { + val jwt = JWT("eyJhbGciOiJIUzI1NiJ9.e30.K17vlwhE8FCMShdl1_65jEYqsQqBOVMPUU9IgG-QlTM") + assertNotNull(jwt.getClaim("notExisting")) + assertTrue { jwt.getClaim("notExisting") is ClaimImpl } + assertTrue { jwt.getClaim("notExisting") is BaseClaim } + } + + @Ignore + @Test + fun shouldGetClaim() { + val jwt = JWT("eyJhbGciOiJIUzI1NiJ9.eyJvYmplY3QiOnsibmFtZSI6ImpvaG4ifX0.lrU1gZlOdlmTTeZwq0VI-pZx2iV46UWYd5-lCjy6-c4") + + assertNotNull(jwt.getClaim("object")) + assertTrue { jwt.getClaim("object") is ClaimImpl } + } + + @Ignore + @Test + fun shouldGetAllClaims() { + val jwt = + JWT("eyJhbGciOiJIUzI1NiJ9.eyJvYmplY3QiOnsibmFtZSI6ImpvaG4ifSwic3ViIjoiYXV0aDAifQ.U20MgOAV81c54mRelwYDJiLllb5OVwUAtMGn-eUOpTA") + val claims: Map? = jwt.claims + + assertNotNull(claims) + + val objectClaim = claims["object"] + + assertNotNull(objectClaim) + + assertTrue { objectClaim is ClaimImpl } + + val extraClaim: Claim = claims["sub"]!! + + assertNotNull(extraClaim) + + assertEquals("auth0", extraClaim.asString()) + } + + @Test + fun shouldGetEmptyAllClaims() { + val jwt = JWT("eyJhbGciOiJIUzI1NiJ9.e30.ZRrHA1JJJW8opsbCGfG_HACGpVUMN_a9IV7pAx_Zmeo") + val claims: Map = assertNotNull(jwt.claims) + + assertTrue { claims.isEmpty() } + } + + // Parcelable + @Test + fun shouldBeParceled() { + val jwtOrigin = JWT("eyJhbGciOiJIUzI1NiJ9.e30.K17vlwhE8FCMShdl1_65jEYqsQqBOVMPUU9IgG-QlTM") + +// val bundleOrigin: Bundle = Bundle() +// bundleOrigin.putParcelable("jwt", jwtOrigin) +// val parcel: Parcel = Parcel.obtain() +// bundleOrigin.writeToParcel(parcel, 0) +// +// //Extract bundle from parcel +// parcel.setDataPosition(0) +// val bundleDest: Bundle = parcel.readBundle(JWT::class.java.getClassLoader()) +// val jwtDest: JWT = bundleDest.getParcelable("jwt") +// +// assertThat(jwtDest, `is`(notNullValue())) +// assertThat(bundleOrigin, `is`(not(bundleDest))) +// assertThat(jwtOrigin, `is`(not(jwtDest))) +// assertThat(jwtOrigin.toString(), `is`(jwtDest.toString())) + } + + /** + * Creates a new JWT with custom time claims. + * + * @param iatMs iat value in MILLISECONDS + * @param expMs exp value in MILLISECONDS + * @return a JWT + */ + private fun customTimeJWT(iatMs: Long?, expMs: Long?): JWT { + val header = encodeString("{}") + val bodyBuilder = StringBuilder("{") + if (iatMs != null) { + val iatSeconds = (iatMs / 1000).toLong() + bodyBuilder.append("\"iat\":\"").append(iatSeconds).append("\"") + } + if (expMs != null) { + if (iatMs != null) { + bodyBuilder.append(",") + } + val expSeconds = (expMs / 1000).toLong() + bodyBuilder.append("\"exp\":\"").append(expSeconds).append("\"") + } + bodyBuilder.append("}") + val body = encodeString(bodyBuilder.toString()) + val signature = "sign" + return JWT("$header.$body.$signature") + } + + @OptIn(ExperimentalEncodingApi::class) + private fun encodeString(source: String): String { + return Base64.UrlSafe.withPadding(Base64.PaddingOption.ABSENT).encode(source.encodeToByteArray()) + } +} diff --git a/shared-domain/src/commonTest/kotlin/dev/sdkforge/jwt/decode/domain/UserPojo.kt b/shared-domain/src/commonTest/kotlin/dev/sdkforge/jwt/decode/domain/UserPojo.kt new file mode 100644 index 0000000..b55ebfb --- /dev/null +++ b/shared-domain/src/commonTest/kotlin/dev/sdkforge/jwt/decode/domain/UserPojo.kt @@ -0,0 +1,11 @@ +@file:Suppress("ktlint:standard:class-signature") + +package dev.sdkforge.jwt.decode.domain + +import kotlinx.serialization.Serializable + +@Serializable +internal data class UserPojo( + private val name: String?, + private val id: Int, +) diff --git a/shared/dependencies/releaseRuntimeClasspath.txt b/shared/dependencies/releaseRuntimeClasspath.txt index 9f2a336..7bd7eef 100644 --- a/shared/dependencies/releaseRuntimeClasspath.txt +++ b/shared/dependencies/releaseRuntimeClasspath.txt @@ -1,2 +1,9 @@ org.jetbrains.kotlin:kotlin-stdlib:2.2.20-Beta2 +org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.7.1 +org.jetbrains.kotlinx:kotlinx-datetime:0.7.1 +org.jetbrains.kotlinx:kotlinx-serialization-bom:1.9.0 +org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.9.0 +org.jetbrains.kotlinx:kotlinx-serialization-core:1.9.0 +org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.9.0 +org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0 org.jetbrains:annotations:13.0 From b8dc374418e0321dfdb8a3721203d715c680fdf0 Mon Sep 17 00:00:00 2001 From: tberchanov Date: Mon, 18 Aug 2025 22:50:38 +0300 Subject: [PATCH 3/3] Renamed ClaimImpl.kt to JsonClaim.kt. Cached base64Decoder. Generated IMPROVEMENTS.md. --- IMPROVEMENTS.md | 656 ++++++++++++++++++ .../dev/sdkforge/jwt/decode/domain/JWT.kt | 3 +- .../sdkforge/jwt/decode/domain/JWTPayload.kt | 2 +- .../domain/{ClaimImpl.kt => JsonClaim.kt} | 2 +- .../jwt/decode/domain/ClaimImplTest.kt | 46 +- .../dev/sdkforge/jwt/decode/domain/JWTTest.kt | 6 +- 6 files changed, 686 insertions(+), 29 deletions(-) create mode 100644 IMPROVEMENTS.md rename shared-domain/src/commonMain/kotlin/dev/sdkforge/jwt/decode/domain/{ClaimImpl.kt => JsonClaim.kt} (97%) diff --git a/IMPROVEMENTS.md b/IMPROVEMENTS.md new file mode 100644 index 0000000..863e0bf --- /dev/null +++ b/IMPROVEMENTS.md @@ -0,0 +1,656 @@ +# JWTDecode Shared-Domain Module - Improvement Considerations + +## Overview +This document outlines potential issues and areas for improvement in the `shared-domain` module of the JWTDecode project. The module currently provides basic JWT decoding functionality but requires significant enhancements to be production-ready and enterprise-grade. + +## ๐Ÿšจ Critical Security Issues + +### 1. No Signature Verification +**Current State**: The module only decodes JWTs but doesn't verify them cryptographically. + +**Code Example**: +```kotlin +// Current: Only decodes, doesn't verify +class JWT(private var token: String) { + // โŒ Accepts ANY token with valid format + // โŒ No cryptographic verification + // โŒ Vulnerable to token tampering +} +``` + +**Impact**: High security risk - attackers can modify JWT payloads without detection. + +**Attack Scenario**: +```kotlin +// Attacker can change user role from "user" to "admin" +val maliciousToken = "eyJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoiYWRtaW4ifQ.fake_signature" +val jwt = JWT(maliciousToken) // โœ… Accepted as valid! +``` + +**Recommended Solution**: +```kotlin +interface JWTVerifier { + fun verify(token: String): VerificationResult + fun verify(jwt: JWT): VerificationResult +} + +sealed class VerificationResult { + object Valid : VerificationResult() + object InvalidSignature : VerificationResult() + object Expired : VerificationResult() + object NotYetValid : VerificationResult() + object InvalidIssuer : VerificationResult() +} +``` + +### 2. No Algorithm Validation +**Current State**: No validation of the algorithm specified in the JWT header. + +**Code Example**: +```kotlin +// Current: No algorithm checking +header = parseJson?>(base64Decode(parts[0])) +// โŒ Doesn't validate "alg" field +// โŒ Vulnerable to algorithm confusion attacks +``` + +**Recommended Solution**: +```kotlin +enum class SupportedAlgorithm { + HS256, HS384, HS512, + RS256, RS384, RS512, + ES256, ES384, ES512 +} + +fun validateAlgorithm(header: Map): SupportedAlgorithm +``` + +### 3. No Input Validation +**Current State**: Accepts any string input without validation. + +**Code Example**: +```kotlin +// Current: Accepts any string +init { + decode(token) // โŒ No validation of input + this.token = token +} + +// โŒ Empty strings accepted +// โŒ Null tokens not handled +// โŒ No length limits +// โŒ No character validation +``` + +**Recommended Solution**: +```kotlin +init { + requireNotNull(token) { "Token cannot be null" } + require(token.isNotBlank()) { "Token cannot be empty or blank" } + require(token.length <= MAX_TOKEN_LENGTH) { "Token exceeds maximum length" } + require(token.matches(TOKEN_PATTERN)) { "Token contains invalid characters" } + + decode(token) + this.token = token +} + +companion object { + private const val MAX_TOKEN_LENGTH = 8192 + private val TOKEN_PATTERN = Regex("^[A-Za-z0-9+/=_-]+\\.([A-Za-z0-9+/=_-]+)\\.([A-Za-z0-9+/=_-]*)$") +} +``` + +## โš ๏ธ Functional Issues + +### 4. Limited Error Handling +**Current State**: Generic exception handling with limited error information. + +**Code Example**: +```kotlin +// Current: Generic exceptions +class DecodeException : RuntimeException { + internal constructor(message: String?) : super(message) + internal constructor(message: String?, cause: Throwable?) : super(message, cause) +} + +// โŒ No specific error types +// โŒ No error codes for programmatic handling +// โŒ Internal constructors limit usage +``` + +**Recommended Solution**: +```kotlin +sealed class JWTError : Exception() { + object InvalidFormat : JWTError() + object InvalidBase64 : JWTError() + object InvalidJson : JWTError() + object UnsupportedAlgorithm : JWTError() + object InvalidSignature : JWTError() + object TokenExpired : JWTError() + object TokenNotYetValid : JWTError() + object DecodeFailed : JWTError() +} +``` + +### 5. Time Validation Issues +**Current State**: Basic expiration checking with potential clock manipulation vulnerabilities. + +**Code Example**: +```kotlin +// Current: Basic expiration check +fun isExpired(leeway: Duration): Boolean { + val todayTime = Instant.fromEpochSeconds(Clock.System.now().epochSeconds) + // โŒ Clock.System.now() can be manipulated + // โŒ No timezone handling + // โŒ No clock skew tolerance +} +``` + +**Recommended Solution**: +```kotlin +interface ClockProvider { + fun now(): Instant + fun getClockSkew(): Duration +} + +fun isExpired(leeway: Duration, clockProvider: ClockProvider = SystemClockProvider): Boolean { + val now = clockProvider.now() + val clockSkew = clockProvider.getClockSkew() + val adjustedLeeway = leeway + clockSkew + + // Enhanced validation logic +} +``` + +## ๐Ÿ—๏ธ Architecture Issues + +### 6. Decoding Logic in Constructor (Anti-Pattern) +**Current State**: The JWT class performs expensive decoding operations in its constructor. + +**Code Example**: +```kotlin +// Current: Anti-pattern - expensive operations in constructor +class JWT(private var token: String) { + private var payload: JWTPayload? = null + + init { + decode(token) // โŒ Expensive operation in constructor + this.token = token + } + + private fun decode(token: String) { + val parts = splitToken(token) + header = parseJson?>(base64Decode(parts[0])) + payload = parseJson(base64Decode(parts[1])) + signature = parts[2] + } +} +``` + +**Problems with Current Approach**: +- **Constructor Side Effects**: Triggers decoding, file I/O, network calls, etc. +- **Exception Handling Issues**: Constructors throwing exceptions for business logic +- **Performance Problems**: Every object creation triggers expensive operations +- **Testing Difficulties**: Hard to test without valid tokens +- **Memory Allocation Issues**: Objects may be created but never used + +**Recommended Solutions**: + +#### **Solution 1: Factory Pattern with Lazy Decoding** +```kotlin +class JWT private constructor( + private val token: String, + private val header: Map?, + private val payload: JWTPayload?, + private val signature: String? +) { + companion object { + fun decode(token: String): JWT { + return try { + val parts = splitToken(token) + val header = parseJson?>(base64Decode(parts[0])) + val payload = parseJson(base64Decode(parts[1])) + val signature = parts[2] + + JWT(token, header, payload, signature) + } catch (e: Exception) { + throw JWTError.DecodeFailed("Failed to decode token", e) + } + } + + fun createUnverified(token: String): JWT { + return JWT(token, null, null, null) + } + } + + // Lazy initialization of decoded data + private val decodedHeader by lazy { header ?: decodeHeader() } + private val decodedPayload by lazy { payload ?: decodePayload() } +} +``` + +#### **Solution 2: Separate Decoder and JWT Classes** +```kotlin +// Separate concerns +class JWTDecoder { + fun decode(token: String): JWT { + val parts = splitToken(token) + val header = parseJson?>(base64Decode(parts[0])) + val payload = parseJson(base64Decode(parts[1])) + val signature = parts[2] + + return JWT(token, header, payload, signature) + } +} + +// JWT class becomes a simple data holder +class JWT( + val token: String, + val header: Map?, + val payload: JWTPayload?, + val signature: String? +) { + val issuer: String? get() = payload?.iss + val subject: String? get() = payload?.sub +} +``` + +### 7. Tight Coupling +**Current State**: Direct instantiation makes testing and extension difficult. + +**Code Example**: +```kotlin +// Current: Direct instantiation +class JWT(private var token: String) { + // โŒ Hard to mock for testing + // โŒ Hard to extend + // โŒ Violates dependency inversion +} +``` + +**Recommended Solution**: +```kotlin +interface JWTDecoder { + fun decode(token: String): JWT +} + +interface JWTValidator { + fun validate(jwt: JWT): ValidationResult +} + +class JWTDecoderImpl( + private val cache: JWTCache, + private val validator: JWTValidator +) : JWTDecoder { + // Implementation with dependencies injected +} +``` + +### 8. No Interface Abstraction +**Current State**: Concrete class only, no abstraction layer. + +**Recommended Solution**: +```kotlin +interface JWT { + val header: Map + val payload: JWTPayload + val signature: String? + + fun getClaim(name: String): Claim + fun isExpired(leeway: Duration): Boolean +} + +class JWTImpl( + private val token: String, + private val header: Map, + private val payload: JWTPayload, + private val signature: String? +) : JWT { + // Implementation +} +``` + +### 9. Violation of Single Responsibility +**Current State**: JWT class handles multiple responsibilities. + +**Current Responsibilities**: +- โŒ Decoding +- โŒ Validation +- โŒ Claim access +- โŒ Time checking +- โŒ String representation + +**Recommended Solution**: +```kotlin +// Separate concerns into different classes +class JWTDecoder { /* Decoding logic */ } +class JWTValidator { /* Validation logic */ } +class JWTClaims { /* Claim access logic */ } +class JWTTimeValidator { /* Time validation logic */ } +class JWTRepresentation { /* String representation logic */ } + +class JWT( + private val decoder: JWTDecoder, + private val validator: JWTValidator, + private val claims: JWTClaims, + private val timeValidator: JWTTimeValidator, + private val representation: JWTRepresentation +) { + // Orchestrates the different components +} +``` + +## ๐ŸŒ Performance Issues + +### 10. No Caching Mechanism +**Current State**: Decodes tokens from scratch every time. + +**Code Example**: +```kotlin +// Current: Decodes every time +val jwt = JWT(token) // โŒ Always decodes from scratch +val jwt2 = JWT(token) // โŒ Decodes again even for same token +``` + +**Recommended Solution**: +```kotlin +interface JWTCache { + fun get(token: String): CachedJWT? + fun put(token: String, jwt: CachedJWT) + fun invalidate(token: String) + fun clear() +} + +data class CachedJWT( + val jwt: JWT, + val cachedAt: Instant, + val ttl: Duration +) { + fun isExpired(): Boolean = Instant.now().isAfter(cachedAt + ttl) +} +``` + +### 11. Memory Allocation Issues +**Current State**: Creates new objects for each claim access. + +**Code Example**: +```kotlin +// Current: Creates new objects for each claim access +fun getClaim(name: String): Claim { + return payload?.claimForName(name) ?: BaseClaim() // โŒ New object each time +} +``` + +**Recommended Solution**: +```kotlin +companion object { + private val EMPTY_CLAIM = BaseClaim() +} + +fun getClaim(name: String): Claim { + return payload?.claimForName(name) ?: EMPTY_CLAIM +} +``` + +## ๐Ÿงช Testing Issues + +### 13. Limited Test Coverage +**Current State**: Tests focus mainly on happy path scenarios. + +**Current Limitations**: +- โŒ No stress testing with large tokens +- โŒ No memory leak testing +- โŒ No performance benchmarking +- โŒ No concurrent access testing + +**Recommended Improvements**: +```kotlin +@Test +fun `should handle large tokens efficiently`() { + val largeToken = generateLargeToken(10000) + val startTime = System.currentTimeMillis() + + val jwt = JWT(largeToken) + + val endTime = System.currentTimeMillis() + assertTrue(endTime - startTime < 100) // Should complete within 100ms +} + +@Test +fun `should not leak memory with repeated operations`() { + repeat(1000) { + JWT(validToken) + } + // Verify memory usage hasn't increased significantly +} +``` + +## ๐Ÿ“š Documentation Issues + +### 14. Poor API Documentation +**Current State**: Basic KDoc without comprehensive examples. + +**Code Example**: +```kotlin +// Current: Basic KDoc +/** + * Get the value of the "iss" claim, or null if it's not available. + * + * @return the Issuer value or null. + */ +val issuer: String? get() = payload?.iss + +// โŒ No usage examples +// โŒ No error scenarios +// โŒ No performance considerations +``` + +**Recommended Improvement**: +```kotlin +/** + * Get the value of the "iss" claim, or null if it's not available. + * + * The issuer claim identifies the principal that issued the JWT. + * This is typically the authorization server or identity provider. + * + * @return the Issuer value or null if not present + * + * @example + * ```kotlin + * val jwt = JWT(token) + * val issuer = jwt.issuer + * if (issuer != null) { + * println("Token issued by: $issuer") + * } + * ``` + * + * @throws JWTError.InvalidFormat if the token format is invalid + * @throws JWTError.InvalidJson if the payload contains malformed JSON + */ +val issuer: String? get() = payload?.iss +``` + +### 15. No Migration Guide +**Current Limitations**: +- โŒ No version compatibility notes +- โŒ No breaking change documentation +- โŒ No upgrade path +- โŒ No deprecation warnings + +## ๐Ÿ“ฑ Example Applications + +### 16. Missing Sample Applications +**Current State**: No example applications demonstrating how to use the JWTDecode library. + +**Missing Examples**: +- โŒ No Android sample app +- โŒ No iOS sample app +- โŒ No JVM/Desktop sample app +- โŒ No web sample app +- โŒ No integration examples with popular frameworks + +**Recommended Solutions**: + +#### **Android Sample App** +```kotlin +// Example: Android app with JWT authentication +class LoginActivity : AppCompatActivity() { + private lateinit var jwtDecoder: JWTDecoder + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_login) + + jwtDecoder = JWTDecoder() + + loginButton.setOnClickListener { + val token = tokenInput.text.toString() + try { + val jwt = jwtDecoder.decode(token) + if (jwt.isExpired(0.seconds)) { + showError("Token has expired") + } else { + navigateToMain(jwt) + } + } catch (e: JWTError) { + showError("Invalid token: ${e.message}") + } + } + } +} +``` + +#### **iOS Sample App** +```swift +// Example: iOS app with JWT validation +class LoginViewController: UIViewController { + private let jwtDecoder = JWTDecoder() + + @IBAction func loginButtonTapped(_ sender: UIButton) { + guard let token = tokenTextField.text else { return } + + do { + let jwt = try jwtDecoder.decode(token: token) + if jwt.isExpired(leeway: 0) { + showAlert(message: "Token has expired") + } else { + navigateToMain(jwt: jwt) + } + } catch { + showAlert(message: "Invalid token: \(error.localizedDescription)") + } + } +} +``` + +#### **Integration Examples** +```kotlin +// Example: Spring Boot integration +@Service +class JWTService( + private val jwtDecoder: JWTDecoder, + private val jwtValidator: JWTValidator +) { + fun validateToken(token: String): ValidationResult { + return try { + val jwt = jwtDecoder.decode(token) + jwtValidator.validate(jwt) + } catch (e: JWTError) { + ValidationResult.Invalid(e) + } + } +} +``` + +### **Benefits of Adding Examples** +1. **Developer Onboarding** - Faster adoption of the library +2. **Best Practices** - Show recommended usage patterns +3. **Integration Guidance** - Demonstrate framework integration +4. **Testing** - Examples serve as integration tests +5. **Documentation** - Living documentation of API usage +6. **Community** - Encourage contributions and feedback + +## ๐Ÿ”’ Security Enhancement Areas + +### 17. Add Key Management +**Implementation**: +```kotlin +interface JWTKeyProvider { + fun getKey(algorithm: SupportedAlgorithm, keyId: String?): CryptoKey + fun getPublicKey(issuer: String): PublicKey +} +``` + +## ๐Ÿ“Š Performance Enhancement Areas + +### 18. Add Streaming Support +**Implementation**: +```kotlin +// For large JWTs +interface JWTStreamDecoder { + fun decodeHeader(input: InputStream): JWTHeader + fun decodePayload(input: InputStream): JWTPayload +} +``` + +### 19. Add Batch Processing +**Implementation**: +```kotlin +interface JWTBatchProcessor { + fun decodeBatch(tokens: List): List + fun validateBatch(jwts: List): List +} +``` + +## ๐Ÿ”„ Future-Proofing Areas + +### 20. Add Plugin System +**Implementation**: +```kotlin +interface JWTPlugin { + fun beforeDecode(token: String): String + fun afterDecode(jwt: JWT): JWT + fun beforeValidate(jwt: JWT): JWT +} +``` + +### 21. Add Metrics and Monitoring +**Implementation**: +```kotlin +interface JWTMetrics { + fun recordDecodeTime(duration: Duration) + fun recordValidationResult(result: ValidationResult) + fun recordError(error: JWTError) +} +``` + +## ๐Ÿ“‹ Priority Ranking + +### ๐Ÿ”ฅ Critical (Fix Immediately) +1. **Signature verification** - Security vulnerability +2. **Algorithm validation** - Security vulnerability +3. **Input validation** - Security vulnerability +4. **Constructor anti-pattern** - Fundamental architectural issue +5. **Error handling improvements** - Production readiness + +### โšก High Priority (Next Sprint) +6. **Performance optimizations** - User experience +7. **Caching mechanism** - Performance improvement +8. **Better architecture** - Maintainability +9. **Comprehensive testing** - Quality assurance + +### ๐Ÿ“ˆ Medium Priority (Next Release) +10. **Platform optimizations** - Cross-platform support +11. **Documentation improvements** - Developer experience +12. **Sample applications** - Developer onboarding +13. **Metrics and monitoring** - Observability + +### ๐Ÿš€ Low Priority (Future Releases) +14. **Plugin system** - Extensibility +15. **Advanced features** - Feature completeness +16. **Performance benchmarking** - Optimization + +## ๐Ÿ”— Related Resources + +- [JWT RFC 7519](https://tools.ietf.org/html/rfc7519) +- [OWASP JWT Security Guidelines](https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/06-Session_Management_Testing/10-Testing_JWT_Token) diff --git a/shared-domain/src/commonMain/kotlin/dev/sdkforge/jwt/decode/domain/JWT.kt b/shared-domain/src/commonMain/kotlin/dev/sdkforge/jwt/decode/domain/JWT.kt index d6485d5..5f564ea 100644 --- a/shared-domain/src/commonMain/kotlin/dev/sdkforge/jwt/decode/domain/JWT.kt +++ b/shared-domain/src/commonMain/kotlin/dev/sdkforge/jwt/decode/domain/JWT.kt @@ -167,7 +167,7 @@ class JWT(private var token: String) { string ?: return null try { - return Base64.UrlSafe.withPadding(Base64.PaddingOption.ABSENT).decode(string).decodeToString() + return base64Decoder.decode(string).decodeToString() } catch (e: IllegalArgumentException) { throw DecodeException("Received bytes didn't correspond to a valid Base64 encoded string.", e) } @@ -185,5 +185,6 @@ class JWT(private var token: String) { companion object { private val TAG: String? = JWT::class.simpleName + private val base64Decoder = Base64.UrlSafe.withPadding(Base64.PaddingOption.ABSENT) } } diff --git a/shared-domain/src/commonMain/kotlin/dev/sdkforge/jwt/decode/domain/JWTPayload.kt b/shared-domain/src/commonMain/kotlin/dev/sdkforge/jwt/decode/domain/JWTPayload.kt index e3ecd60..22fd141 100644 --- a/shared-domain/src/commonMain/kotlin/dev/sdkforge/jwt/decode/domain/JWTPayload.kt +++ b/shared-domain/src/commonMain/kotlin/dev/sdkforge/jwt/decode/domain/JWTPayload.kt @@ -62,7 +62,7 @@ object ClaimAsStringSerializer : KSerializer { } override fun deserialize(decoder: Decoder): Claim { - return ClaimImpl(Json.parseToJsonElement(decoder.decodeString())) + return JsonClaim(Json.parseToJsonElement(decoder.decodeString())) } } diff --git a/shared-domain/src/commonMain/kotlin/dev/sdkforge/jwt/decode/domain/ClaimImpl.kt b/shared-domain/src/commonMain/kotlin/dev/sdkforge/jwt/decode/domain/JsonClaim.kt similarity index 97% rename from shared-domain/src/commonMain/kotlin/dev/sdkforge/jwt/decode/domain/ClaimImpl.kt rename to shared-domain/src/commonMain/kotlin/dev/sdkforge/jwt/decode/domain/JsonClaim.kt index fe5c7d0..c15ff58 100644 --- a/shared-domain/src/commonMain/kotlin/dev/sdkforge/jwt/decode/domain/ClaimImpl.kt +++ b/shared-domain/src/commonMain/kotlin/dev/sdkforge/jwt/decode/domain/JsonClaim.kt @@ -20,7 +20,7 @@ import kotlinx.serialization.json.longOrNull * The ClaimImpl class implements the Claim interface. */ @OptIn(ExperimentalTime::class) -internal class ClaimImpl(private val value: JsonElement) : BaseClaim() { +internal class JsonClaim(private val value: JsonElement) : BaseClaim() { override fun asBoolean(): Boolean? = when (value) { !is JsonPrimitive -> null diff --git a/shared-domain/src/commonTest/kotlin/dev/sdkforge/jwt/decode/domain/ClaimImplTest.kt b/shared-domain/src/commonTest/kotlin/dev/sdkforge/jwt/decode/domain/ClaimImplTest.kt index 226a208..60b511d 100644 --- a/shared-domain/src/commonTest/kotlin/dev/sdkforge/jwt/decode/domain/ClaimImplTest.kt +++ b/shared-domain/src/commonTest/kotlin/dev/sdkforge/jwt/decode/domain/ClaimImplTest.kt @@ -21,7 +21,7 @@ class ClaimImplTest { @Test fun shouldGetBooleanValue() { val value: JsonElement = json.encodeToJsonElement(true) - val claim = ClaimImpl(value) + val claim = JsonClaim(value) assertNotNull(claim.asBoolean()) assertEquals(true, claim.asBoolean()) @@ -30,7 +30,7 @@ class ClaimImplTest { @Test fun shouldGetNullBooleanIfNotPrimitiveValue() { val value: JsonElement = json.encodeToJsonElement(Unit) - val claim = ClaimImpl(value) + val claim = JsonClaim(value) assertNull(claim.asBoolean()) } @@ -38,7 +38,7 @@ class ClaimImplTest { @Test fun shouldGetIntValue() { val value: JsonElement = json.encodeToJsonElement(123) - val claim = ClaimImpl(value) + val claim = JsonClaim(value) assertNotNull(claim.asInt()) assertEquals(123, claim.asInt()) @@ -47,7 +47,7 @@ class ClaimImplTest { @Test fun shouldGetLongValue() { val value: JsonElement = json.encodeToJsonElement(123L) - val claim = ClaimImpl(value) + val claim = JsonClaim(value) assertNotNull(claim.asLong()) assertEquals(123L, claim.asLong()) @@ -56,7 +56,7 @@ class ClaimImplTest { @Test fun shouldGetNullIntIfNotPrimitiveValue() { val value: JsonElement = json.encodeToJsonElement(Unit) - val claim = ClaimImpl(value) + val claim = JsonClaim(value) assertNull(claim.asInt()) } @@ -64,7 +64,7 @@ class ClaimImplTest { @Test fun shouldGetNullLongIfNotPrimitiveValue() { val value: JsonElement = json.encodeToJsonElement(Unit) - val claim = ClaimImpl(value) + val claim = JsonClaim(value) assertNull(claim.asLong()) } @@ -72,7 +72,7 @@ class ClaimImplTest { @Test fun shouldGetDoubleValue() { val value: JsonElement = json.encodeToJsonElement(1.5) - val claim = ClaimImpl(value) + val claim = JsonClaim(value) assertNotNull(claim.asDouble()) assertEquals(1.5, claim.asDouble()) @@ -81,7 +81,7 @@ class ClaimImplTest { @Test fun shouldGetNullDoubleIfNotPrimitiveValue() { val value: JsonElement = json.encodeToJsonElement(Unit) - val claim = ClaimImpl(value) + val claim = JsonClaim(value) assertNull(claim.asDouble()) } @@ -90,7 +90,7 @@ class ClaimImplTest { fun shouldGetLargeDateValue() { val seconds: Long = Int.MAX_VALUE + 10000L val value: JsonElement = json.encodeToJsonElement(seconds) - val claim = ClaimImpl(value) + val claim = JsonClaim(value) val date: Instant? = claim.asDate() assertNotNull(date) @@ -101,7 +101,7 @@ class ClaimImplTest { @Test fun shouldGetDateValue() { val value: JsonElement = json.encodeToJsonElement("1476824844") - val claim = ClaimImpl(value) + val claim = JsonClaim(value) assertNotNull(claim.asDate()) assertEquals(Instant.fromEpochSeconds(1476824844), claim.asDate()) @@ -110,7 +110,7 @@ class ClaimImplTest { @Test fun shouldGetNullDateIfNotPrimitiveValue() { val value: JsonElement = json.encodeToJsonElement(Unit) - val claim = ClaimImpl(value) + val claim = JsonClaim(value) assertNull(claim.asDate()) } @@ -118,7 +118,7 @@ class ClaimImplTest { @Test fun shouldGetStringValue() { val value: JsonElement = json.encodeToJsonElement("string") - val claim = ClaimImpl(value) + val claim = JsonClaim(value) assertNotNull(claim.asString()) assertEquals("string", claim.asString()) @@ -127,7 +127,7 @@ class ClaimImplTest { @Test fun shouldGetNullStringIfNotPrimitiveValue() { val value: JsonElement = json.encodeToJsonElement(Unit) - val claim = ClaimImpl(value) + val claim = JsonClaim(value) assertNull(claim.asString()) } @@ -135,7 +135,7 @@ class ClaimImplTest { @Test fun shouldGetListValueOfCustomClass() { val value: JsonElement = json.encodeToJsonElement(listOf(UserPojo("George", 1), UserPojo("Mark", 2))) - val claim = ClaimImpl(value) + val claim = JsonClaim(value) assertNotNull(claim.asList(UserPojo.serializer())) assertContentEquals( @@ -147,7 +147,7 @@ class ClaimImplTest { @Test fun shouldGetListValue() { val value: JsonElement = json.encodeToJsonElement(listOf("string1", "string2")) - val claim = ClaimImpl(value) + val claim = JsonClaim(value) assertNotNull(claim.asList(String.serializer())) assertContentEquals( @@ -159,7 +159,7 @@ class ClaimImplTest { @Test fun shouldGetEmptyListIfNullValue() { val value: JsonElement = json.encodeToJsonElement(null.orEmpty()) - val claim = ClaimImpl(value) + val claim = JsonClaim(value) assertNotNull(claim.asList(String.serializer())) assertContentEquals( @@ -171,7 +171,7 @@ class ClaimImplTest { @Test fun shouldGetEmptyListIfNonArrayValue() { val value: JsonElement = json.encodeToJsonElement(1) - val claim = ClaimImpl(value) + val claim = JsonClaim(value) assertNotNull(claim.asList(String.serializer())) assertContentEquals( @@ -183,7 +183,7 @@ class ClaimImplTest { @Test fun shouldThrowIfListClassMismatch() { val value: JsonElement = json.encodeToJsonElement(arrayOf("keys", "values")) - val claim = ClaimImpl(value) + val claim = JsonClaim(value) assertFailsWith { claim.asList(UserPojo.serializer()) @@ -194,13 +194,13 @@ class ClaimImplTest { fun shouldGetAsObject() { val data = UserPojo("George", 1) val userValue: JsonElement = json.encodeToJsonElement(data) - val userClaim = ClaimImpl(userValue) + val userClaim = JsonClaim(userValue) val intValue: JsonElement = json.encodeToJsonElement(1) - val intClaim = ClaimImpl(intValue) + val intClaim = JsonClaim(intValue) val booleanValue: JsonElement = json.encodeToJsonElement(true) - val booleanClaim = ClaimImpl(booleanValue) + val booleanClaim = JsonClaim(booleanValue) assertNotNull(userClaim.asObject(UserPojo.serializer())) assertEquals(UserPojo("George", 1), userClaim.asObject(UserPojo.serializer())) @@ -214,7 +214,7 @@ class ClaimImplTest { @Test fun shouldGetNullObjectIfNullValue() { - val claim = ClaimImpl(JsonNull) + val claim = JsonClaim(JsonNull) assertNull(claim.asObject(UserPojo.serializer())) } @@ -222,7 +222,7 @@ class ClaimImplTest { @Test fun shouldThrowIfObjectClassMismatch() { val value: JsonElement = json.encodeToJsonElement(1) - val claim = ClaimImpl(value) + val claim = JsonClaim(value) assertFailsWith { claim.asObject(UserPojo.serializer()) diff --git a/shared-domain/src/commonTest/kotlin/dev/sdkforge/jwt/decode/domain/JWTTest.kt b/shared-domain/src/commonTest/kotlin/dev/sdkforge/jwt/decode/domain/JWTTest.kt index c97075a..0d26e99 100644 --- a/shared-domain/src/commonTest/kotlin/dev/sdkforge/jwt/decode/domain/JWTTest.kt +++ b/shared-domain/src/commonTest/kotlin/dev/sdkforge/jwt/decode/domain/JWTTest.kt @@ -348,7 +348,7 @@ class JWTTest { fun shouldGetBaseClaimIfClaimIsMissing() { val jwt = JWT("eyJhbGciOiJIUzI1NiJ9.e30.K17vlwhE8FCMShdl1_65jEYqsQqBOVMPUU9IgG-QlTM") assertNotNull(jwt.getClaim("notExisting")) - assertTrue { jwt.getClaim("notExisting") is ClaimImpl } + assertTrue { jwt.getClaim("notExisting") is JsonClaim } assertTrue { jwt.getClaim("notExisting") is BaseClaim } } @@ -358,7 +358,7 @@ class JWTTest { val jwt = JWT("eyJhbGciOiJIUzI1NiJ9.eyJvYmplY3QiOnsibmFtZSI6ImpvaG4ifX0.lrU1gZlOdlmTTeZwq0VI-pZx2iV46UWYd5-lCjy6-c4") assertNotNull(jwt.getClaim("object")) - assertTrue { jwt.getClaim("object") is ClaimImpl } + assertTrue { jwt.getClaim("object") is JsonClaim } } @Ignore @@ -374,7 +374,7 @@ class JWTTest { assertNotNull(objectClaim) - assertTrue { objectClaim is ClaimImpl } + assertTrue { objectClaim is JsonClaim } val extraClaim: Claim = claims["sub"]!!