Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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) ||

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we ever send a cookie header?

header.equals(HttpHeaders.SetCookie, ignoreCase = true) ||
header.equals("x-auth-token", ignoreCase = true)
}
}
install(ContentNegotiation) {
json(json)
Expand Down
Original file line number Diff line number Diff line change
@@ -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]}***"
}
}
Original file line number Diff line number Diff line change
@@ -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":"***""""))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}
}