Skip to content

Commit

Permalink
WIP: Fix idToken parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
ILIYANGERMANOV committed Nov 30, 2024
1 parent 9345f63 commit ae41b85
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 9 deletions.
2 changes: 2 additions & 0 deletions server/src/main/kotlin/ivy/learn/di/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import ivy.learn.ServerMode
import ivy.learn.config.Environment
import ivy.learn.config.EnvironmentImpl
import ivy.learn.config.ServerConfigurationProvider
import ivy.learn.util.Base64Util
import ivy.learn.util.Crypto
import ivy.learn.util.TimeProvider

Expand All @@ -22,5 +23,6 @@ class AppModule(private val devMode: Boolean) : Di.Module {
autoWireSingleton(::LearnServer)
autoWire(::Crypto)
autoWire(::TimeProvider)
autoWire(::Base64Util)
}
}
33 changes: 24 additions & 9 deletions server/src/main/kotlin/ivy/learn/domain/auth/GoogleOAuthUseCase.kt
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
package ivy.learn.domain.auth

import arrow.core.Either
import arrow.core.left
import arrow.core.raise.catch
import arrow.core.raise.either
import arrow.core.raise.ensure
import arrow.core.raise.ensureNotNull
import arrow.core.right
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.http.*
import ivy.learn.config.ServerConfiguration
import ivy.learn.util.Base64Util
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import java.util.*

class GoogleOAuthUseCase(
private val config: ServerConfiguration,
private val httpClient: HttpClient,
private val base64: Base64Util,
) {

suspend fun verify(
Expand Down Expand Up @@ -62,9 +65,11 @@ class GoogleOAuthUseCase(
}

private fun extractPublicProfile(idToken: String): Either<String, GooglePublicProfile> = either {
val idTokenPayload = catch({ decodeJwt(idToken) }) { e ->
raise("Failed to decode Google JWT idToken '$idToken': $e")
}
val idTokenPayload = decodeIdTokenPayload(idToken)
.mapLeft { errMsg ->
"Google ID token decode: $errMsg; idToken = $idToken"
}
.bind()
val audience = idTokenPayload["aud"]
ensure(audience != config.googleOAuth.clientId) {
"Google ID token is not intended for our client"
Expand All @@ -85,10 +90,21 @@ class GoogleOAuthUseCase(
)
}

private fun decodeJwt(jwt: String): Map<String, String> {
val payload = jwt.split(".")[1]
val decodedBytes = Base64.getUrlDecoder().decode(payload)
return Json.decodeFromString(decodedBytes.decodeToString())
private fun decodeIdTokenPayload(
idToken: String
): Either<String, Map<String, String>> = either {
val payloadBase64 = catch({
idToken.split(".")[1].right()
}) { e ->
"Split on '.' for \"$idToken\" because $e".left()
}.bind()
val payloadText = base64.decode(payloadBase64).bind()
val payload = catch({
Json.decodeFromString<Map<String, String>>(payloadText).right()
}) { e ->
"JSON decode of '$payloadText' because $e".left()
}.bind()
payload
}


Expand All @@ -99,7 +115,6 @@ class GoogleOAuthUseCase(
)
}


data class GooglePublicProfile(
val email: String,
val names: String?,
Expand Down
22 changes: 22 additions & 0 deletions server/src/main/kotlin/ivy/learn/util/Base64Util.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package ivy.learn.util

import arrow.core.Either
import arrow.core.raise.catch
import arrow.core.right
import kotlinx.io.bytestring.decodeToByteString
import kotlinx.io.bytestring.decodeToString
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi

class Base64Util {
@OptIn(ExperimentalEncodingApi::class)
fun decode(
textBase64: String,
): Either<String, String> = catch({
Base64.withPadding(Base64.PaddingOption.ABSENT_OPTIONAL)
.decodeToByteString(textBase64).decodeToString()
.right()
}) { e ->
Either.Left("Base64 decode of '$textBase64' failed because $e")
}
}
60 changes: 60 additions & 0 deletions server/src/test/kotlin/ivy/learn/util/Base64UtilTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package ivy.learn.util

import com.google.testing.junit.testparameterinjector.TestParameter
import com.google.testing.junit.testparameterinjector.TestParameterInjector
import io.kotest.assertions.arrow.core.shouldBeLeft
import io.kotest.assertions.arrow.core.shouldBeRight
import io.kotest.matchers.shouldBe
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(TestParameterInjector::class)
class Base64UtilTest {
private lateinit var base64: Base64Util

@Before
fun setup() {
base64 = Base64Util()
}

enum class ValidBase64TestCase(
val encoded: String,
val expectedDecoded: String,
) {
PADDED(
encoded = "SGVsbG8sIEJhc2U2NCE=",
expectedDecoded = "Hello, Base64!",
),
NOT_PADDED(
encoded = "SGVsbG8sIEJhc2U2NCE",
expectedDecoded = "Hello, Base64!"
)
}

@Test
fun `decodes valid base64`(
@TestParameter testCase: ValidBase64TestCase,
) {
// Given
val encoded = testCase.encoded

// When
val decoded = base64.decode(encoded)

// Then
decoded.shouldBeRight() shouldBe testCase.expectedDecoded
}

@Test
fun `fails to decode invalid base64`() {
// Given
val encoded = "Hello, Base64!"

// When
val decoded = base64.decode(encoded)

// Then
decoded.shouldBeLeft()
}
}

0 comments on commit ae41b85

Please sign in to comment.