diff --git a/auth/src/commonMain/kotlin/com/quran/shared/auth/di/AuthModule.kt b/auth/src/commonMain/kotlin/com/quran/shared/auth/di/AuthModule.kt index 54b42b2d..5d2ef3da 100644 --- a/auth/src/commonMain/kotlin/com/quran/shared/auth/di/AuthModule.kt +++ b/auth/src/commonMain/kotlin/com/quran/shared/auth/di/AuthModule.kt @@ -7,6 +7,7 @@ import com.quran.shared.auth.repository.AuthRepository import com.quran.shared.auth.repository.AuthNetworkDataSource import com.quran.shared.auth.repository.OidcAuthRepository import com.quran.shared.auth.repository.UnconfiguredAuthRepository +import com.quran.shared.auth.utils.redactSensitiveAuthData import com.quran.shared.di.AppScope import dev.zacsweers.metro.BindingContainer import dev.zacsweers.metro.ContributesTo @@ -17,6 +18,7 @@ import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.logging.LogLevel import io.ktor.client.plugins.logging.Logger import io.ktor.client.plugins.logging.Logging +import io.ktor.http.HttpHeaders import io.ktor.serialization.kotlinx.json.json import kotlin.native.HiddenFromObjC import kotlinx.serialization.json.Json @@ -70,9 +72,17 @@ abstract class AuthModule { return HttpClient { install(Logging) { logger = object : Logger { - override fun log(message: String) = println("HTTP Client: $message") + override fun log(message: String) { + println("HTTP Client: ${redactSensitiveAuthData(message)}") + } } level = if (config.environment.enableVerboseLogging) LogLevel.ALL else LogLevel.NONE + sanitizeHeader { header -> + header.equals(HttpHeaders.Authorization, ignoreCase = true) || + header.equals(HttpHeaders.Cookie, ignoreCase = true) || + header.equals(HttpHeaders.SetCookie, ignoreCase = true) || + header.equals("x-auth-token", ignoreCase = true) + } } install(ContentNegotiation) { json(json) diff --git a/auth/src/commonMain/kotlin/com/quran/shared/auth/utils/AuthLogSanitizer.kt b/auth/src/commonMain/kotlin/com/quran/shared/auth/utils/AuthLogSanitizer.kt new file mode 100644 index 00000000..48ac6c76 --- /dev/null +++ b/auth/src/commonMain/kotlin/com/quran/shared/auth/utils/AuthLogSanitizer.kt @@ -0,0 +1,23 @@ +package com.quran.shared.auth.utils + +private val sensitiveHeaderPattern = Regex( + pattern = """(?im)^(\s*(?:->\s*)?(?:Authorization|Cookie|Set-Cookie|x-auth-token):)\s*.*$""" +) +private val sensitiveJsonFieldPattern = Regex( + pattern = """(?i)("(?:access_token|id_token|refresh_token)"\s*:\s*")[^"]*(")""" +) +private val sensitiveFormFieldPattern = Regex( + pattern = """(?i)(^|[&\n])((?:token|code|code_verifier)=)[^&\r\n]*""" +) + +internal fun redactSensitiveAuthData(message: String): String { + val redactedHeaders = sensitiveHeaderPattern.replace(message) { + "${it.groupValues[1]} ***" + } + val redactedJson = sensitiveJsonFieldPattern.replace(redactedHeaders) { + "${it.groupValues[1]}***${it.groupValues[2]}" + } + return sensitiveFormFieldPattern.replace(redactedJson) { + "${it.groupValues[1]}${it.groupValues[2]}***" + } +} diff --git a/auth/src/commonTest/kotlin/com/quran/shared/auth/utils/AuthLogSanitizerTest.kt b/auth/src/commonTest/kotlin/com/quran/shared/auth/utils/AuthLogSanitizerTest.kt new file mode 100644 index 00000000..29c079f3 --- /dev/null +++ b/auth/src/commonTest/kotlin/com/quran/shared/auth/utils/AuthLogSanitizerTest.kt @@ -0,0 +1,28 @@ +package com.quran.shared.auth.utils + +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class AuthLogSanitizerTest { + + @Test + fun redactsSensitiveHeadersAndBodyFields() { + val sensitiveValue = "sensitive-value" + val message = """ + -> Authorization: Bearer $sensitiveValue + -> x-auth-token: $sensitiveValue + BODY START + token=$sensitiveValue&code=$sensitiveValue&code_verifier=$sensitiveValue + {"access_token":"$sensitiveValue","id_token":"$sensitiveValue","refresh_token":"$sensitiveValue"} + BODY END + """.trimIndent() + + val result = redactSensitiveAuthData(message) + + assertFalse(result.contains(sensitiveValue)) + assertTrue(result.contains("-> Authorization: ***")) + assertTrue(result.contains("token=***&code=***&code_verifier=***")) + assertTrue(result.contains(""""access_token":"***"""")) + } +} diff --git a/syncengine/src/iosMain/kotlin/com/quran/shared/syncengine/network/HttpClientFactory.apple.kt b/syncengine/src/iosMain/kotlin/com/quran/shared/syncengine/network/HttpClientFactory.apple.kt index d2d84fca..ba649f46 100644 --- a/syncengine/src/iosMain/kotlin/com/quran/shared/syncengine/network/HttpClientFactory.apple.kt +++ b/syncengine/src/iosMain/kotlin/com/quran/shared/syncengine/network/HttpClientFactory.apple.kt @@ -4,9 +4,14 @@ import io.ktor.client.HttpClient import io.ktor.client.engine.darwin.Darwin import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.logging.LogLevel +import io.ktor.client.plugins.logging.Logger as KtorLogger import io.ktor.client.plugins.logging.Logging +import io.ktor.http.HttpHeaders import kotlinx.serialization.json.Json import io.ktor.serialization.kotlinx.json.json +import platform.Foundation.NSProcessInfo + +private const val HTTP_LOG_LEVEL_ENVIRONMENT_KEY = "MOBILE_SYNC_HTTP_LOG_LEVEL" actual object HttpClientFactory { actual fun createHttpClient(): HttpClient { @@ -15,8 +20,30 @@ actual object HttpClientFactory { json(Json { explicitNulls = false }) } install(Logging) { - level = LogLevel.INFO + logger = object : KtorLogger { + override fun log(message: String) { + println("KtorHTTP: $message") + } + } + level = configuredHttpLogLevel() + sanitizeHeader { header -> + header.equals(HttpHeaders.Authorization, ignoreCase = true) || + header.equals(HttpHeaders.Cookie, ignoreCase = true) || + header.equals(HttpHeaders.SetCookie, ignoreCase = true) || + header.equals("x-auth-token", ignoreCase = true) + } } } } -} +} + +private fun configuredHttpLogLevel(): LogLevel { + val configuredLevel = NSProcessInfo.processInfo.environment[HTTP_LOG_LEVEL_ENVIRONMENT_KEY] as? String + return when (configuredLevel?.uppercase()) { + "ALL" -> LogLevel.ALL + "BODY" -> LogLevel.BODY + "HEADERS" -> LogLevel.HEADERS + "NONE" -> LogLevel.NONE + else -> LogLevel.INFO + } +}