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 @@ -9,8 +9,6 @@ import com.auth0.jwk.JwkProvider
import com.auth0.jwk.SigningKeyNotFoundException
import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import io.ktor.client.*
import io.ktor.client.engine.mock.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.server.auth.*
Expand All @@ -19,7 +17,6 @@ import io.ktor.server.engine.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.server.testing.*
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
Expand All @@ -35,7 +32,6 @@ import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFails
import kotlin.test.assertFailsWith
import kotlin.time.Duration.Companion.seconds

Expand All @@ -55,73 +51,6 @@ internal data class JwksKeys(val keys: List<JwksKey>)
class JwkDiscoveryTest {
private val discoveryJson = Json { ignoreUnknownKeys = true }

private fun String.toConfigUrl() = "$this/.well-known/openid-configuration"

@Test
fun testFetchOpenIdConfigurationWithMockProviders() = testApplication {
val googleIssuer = "https://accounts.google.com"
val auth0Issuer = "https://example.auth0.com"
val keycloakIssuer = "https://keycloak.example/realms/demo"

val responses = mapOf(
googleIssuer.toConfigUrl() to OpenIdConfiguration(
googleIssuer,
jwksUri = "https://www.googleapis.com/oauth2/v3/certs"
),
auth0Issuer.toConfigUrl() to OpenIdConfiguration(
auth0Issuer,
jwksUri = "https://example.auth0.com/.well-known/jwks.json"
),
keycloakIssuer.toConfigUrl() to OpenIdConfiguration(
keycloakIssuer,
jwksUri = "https://keycloak.example/realms/demo/protocol/openid-connect/certs"
)
).mapValues { discoveryJson.encodeToString(it.value) }

val engine = MockEngine { request ->
val responseBody =
responses[request.url.toString()] ?: return@MockEngine respondError(HttpStatusCode.NotFound)
respond(
content = responseBody,
headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString())
)
}
val client = HttpClient(engine)

val googleConfig = client.fetchOpenIdConfiguration(googleIssuer)
assertEquals("https://www.googleapis.com/oauth2/v3/certs", googleConfig.jwksUri)

val auth0Config = client.fetchOpenIdConfiguration(auth0Issuer)
assertEquals("https://example.auth0.com/.well-known/jwks.json", auth0Config.jwksUri)

val keycloakConfig = client.fetchOpenIdConfiguration(keycloakIssuer)
assertEquals(
"https://keycloak.example/realms/demo/protocol/openid-connect/certs",
keycloakConfig.jwksUri
)
}

@Test
fun testFetchOpenIdConfigurationFails(): Unit = runBlocking {
var counter = 0
val engine = MockEngine {
respond(
content = when {
counter++ == 0 -> "not json"
else -> "{}"
},
headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString())
)
}
val client = HttpClient(engine) { expectSuccess = false }
assertFails {
client.fetchOpenIdConfiguration("https://issuer.example")
}
assertFails {
client.fetchOpenIdConfiguration("https://issuer.example")
}
}

@Test
fun testJwkDiscoveryRotation() = testApplication {
val issuer = "https://issuer.example/"
Expand All @@ -143,6 +72,8 @@ class JwkDiscoveryTest {
validate { JWTPrincipal(it.payload) }
openIdConfig = OpenIdConfiguration(
issuer = issuer,
authorizationEndpoint = "$issuer/authorize",
tokenEndpoint = "$issuer/token",
jwksUri = "$issuer/.well-known/openid-configuration"
)
}
Expand Down Expand Up @@ -203,6 +134,8 @@ class JwkDiscoveryTest {

val openIdConfig = OpenIdConfiguration(
issuer = issuerUrl,
authorizationEndpoint = "$issuerUrl/authorize",
tokenEndpoint = "$issuerUrl/token",
jwksUri = "$issuerUrl/jwks"
)

Expand Down Expand Up @@ -279,6 +212,8 @@ class JwkDiscoveryTest {
val jwkProvider = JwkConfig().apply {
openIdConfig = OpenIdConfiguration(
issuer = "http://issuer.example",
authorizationEndpoint = "http://issuer.example/authorize",
tokenEndpoint = "http://issuer.example/token",
jwksUri = "http://127.0.0.1:$port"
)
cache(maxEntries = 1, duration = 1.seconds)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -485,8 +485,12 @@ public final class io/ktor/server/auth/OAuthGrantTypes {

public final class io/ktor/server/auth/OAuthProcedureKt {
public static final fun getOAuthKey ()Ljava/lang/Object;
public static final fun oauth (Lio/ktor/server/auth/AuthenticationConfig;Ljava/lang/String;Lio/ktor/server/auth/OpenIdConfiguration;Lkotlin/jvm/functions/Function1;)V
public static final fun oauth (Lio/ktor/server/auth/AuthenticationConfig;Ljava/lang/String;Ljava/lang/String;Lio/ktor/server/auth/OpenIdConfiguration;Lkotlin/jvm/functions/Function1;)V
public static final fun oauth (Lio/ktor/server/auth/AuthenticationConfig;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V
public static final fun oauth (Lio/ktor/server/auth/AuthenticationConfig;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V
public static synthetic fun oauth$default (Lio/ktor/server/auth/AuthenticationConfig;Ljava/lang/String;Lio/ktor/server/auth/OpenIdConfiguration;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
public static synthetic fun oauth$default (Lio/ktor/server/auth/AuthenticationConfig;Ljava/lang/String;Ljava/lang/String;Lio/ktor/server/auth/OpenIdConfiguration;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
Comment on lines +488 to +493
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find the OAuth-related source files
fd -e kt -t f oauth | grep -E "(auth|oauth)" | head -20

Repository: ktorio/ktor

Length of output: 1041


🏁 Script executed:

# Search for oauth function definitions
rg "fun.*oauth" --type kt -A 5 | head -100

Repository: ktorio/ktor

Length of output: 81


🏁 Script executed:

# Look for the main oauth implementation in ktor-server-auth
find . -path "*/ktor-server-auth/*" -name "*.kt" -type f | head -20

Repository: ktorio/ktor

Length of output: 2083


🏁 Script executed:

cat ./ktor-server/ktor-server-plugins/ktor-server-auth/jvm/src/io/ktor/server/auth/OAuth.kt

Repository: ktorio/ktor

Length of output: 2486


🏁 Script executed:

cat ./ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/OpenIdConfiguration.kt

Repository: ktorio/ktor

Length of output: 5695


🏁 Script executed:

# Search for oauth function definitions - extension functions
rg "fun.*oauth\(" --type-add 'kotlin:*.kt' -t kotlin | head -50

Repository: ktorio/ktor

Length of output: 602


🏁 Script executed:

# Check OAuth2.kt which likely contains the oauth extension functions
cat ./ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/OAuth2.kt | head -200

Repository: ktorio/ktor

Length of output: 6610


🏁 Script executed:

cat ./ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/OAuthProcedure.kt

Repository: ktorio/ktor

Length of output: 11535


🏁 Script executed:

# Check if there are any `@JvmName` annotations on the oauth functions
rg "@JvmName" ./ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/OAuthProcedure.kt

Repository: ktorio/ktor

Length of output: 37


🏁 Script executed:

# Look for Java test files that might use the oauth functions
find ./ktor-server/ktor-server-plugins/ktor-server-auth -name "*.java" -type f

Repository: ktorio/ktor

Length of output: 37


🏁 Script executed:

# Check if there's an actual overload ambiguity by creating a test case
cat > /tmp/test_ambiguity.kt << 'EOF'
// Simulating the overloads from the actual code
class TestAmbiguity {
    interface Config
    interface OpenIdOAuthConfig
    data class OpenIdConfiguration(val issuer: String)
    
    fun oauth(
        config: Any,
        name: String? = null,
        configure: Config.() -> Unit
    ) {}
    
    fun oauth(
        config: Any,
        name: String? = null,
        description: String? = null,
        configure: Config.() -> Unit
    ) {}
    
    fun oauth(
        config: Any,
        name: String? = null,
        openIdConfiguration: OpenIdConfiguration,
        configure: OpenIdOAuthConfig.() -> Unit
    ) {}
    
    fun oauth(
        config: Any,
        name: String? = null,
        description: String? = null,
        openIdConfiguration: OpenIdConfiguration,
        configure: OpenIdOAuthConfig.() -> Unit
    ) {}
    
    fun test() {
        // Test calls
        oauth(Any(), "name") {} // Should match first overload
        oauth(Any(), "name", null) {} // POTENTIALLY AMBIGUOUS - String or OpenIdConfiguration?
        oauth(Any(), "name", null as String?) {} // Should match second overload
        oauth(Any(), "name", OpenIdConfiguration("issuer")) {} // Should match third overload
    }
}
EOF
cat /tmp/test_ambiguity.kt

Repository: ktorio/ktor

Length of output: 1283


🏁 Script executed:

# Let's check the .api file to see what synthetic defaults were generated
grep -A 20 "oauth\$default" ./ktor-server/ktor-server-plugins/ktor-server-auth/api/ktor-server-auth.api | head -30

Repository: ktorio/ktor

Length of output: 2122


🏁 Script executed:

# Let's look at the actual parameter types more carefully - check if OpenIdConfiguration is nullable
cat ./ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/OAuthProcedure.kt | grep -A 10 "fun AuthenticationConfig.oauth"

Repository: ktorio/ktor

Length of output: 1700


🏁 Script executed:

# Also check the `@api` file more carefully around the oauth definitions to see full signatures
grep -B 5 -A 10 "fun oauth" ./ktor-server/ktor-server-plugins/ktor-server-auth/api/ktor-server-auth.api | head -50

Repository: ktorio/ktor

Length of output: 2176


🏁 Script executed:

# Let's see the exact lines around 488-493 in the api file
sed -n '480,500p' ./ktor-server/ktor-server-plugins/ktor-server-auth/api/ktor-server-auth.api

Repository: ktorio/ktor

Length of output: 2130


🏁 Script executed:

# Let's create a test to see if Kotlin actually allows calling with null and which overload gets picked
cat > /tmp/test_kotlin_overload.kt << 'EOF'
data class OpenIdConfiguration(val issuer: String)

fun test(
    name: String? = null,
    openIdConfiguration: OpenIdConfiguration,
    configure: () -> Unit
) {
    println("OpenIdConfiguration overload called")
}

fun test(
    name: String? = null,
    description: String? = null,
    configure: () -> Unit
) {
    println("String overload called")
}

fun main() {
    // This should be unambiguous because the 3rd parameter type is different
    test("name", OpenIdConfiguration("issuer")) {}
    
    // Can we call with null?
    // test("name", null) {} // This would be ambiguous!
}
EOF
cat /tmp/test_kotlin_overload.kt

Repository: ktorio/ktor

Length of output: 648


🏁 Script executed:

# Check if there are any existing tests for the oauth functions
find ./ktor-server/ktor-server-plugins/ktor-server-auth -name "*Test.kt" -o -name "*Test.java" | xargs grep -l "oauth" 2>/dev/null | head -5

Repository: ktorio/ktor

Length of output: 345


🏁 Script executed:

# Look at one of the test files to see how oauth is actually called
cat ./ktor-server/ktor-server-plugins/ktor-server-auth/common/test/io/ktor/tests/auth/OpenIdOAuthTest.kt | head -100

Repository: ktorio/ktor

Length of output: 3800


🏁 Script executed:

# Check if the oauth functions in OAuthProcedure.kt have any `@JvmName` annotations
grep -B 2 "fun AuthenticationConfig.oauth" ./ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/OAuthProcedure.kt | grep -E "@JvmName|fun AuthenticationConfig"

Repository: ktorio/ktor

Length of output: 210


🏁 Script executed:

# Let's verify if Java can actually call these functions with null - check for Java test files
find ./ktor-server/ktor-server-plugins/ktor-server-auth -name "*Test.java" -type f

Repository: ktorio/ktor

Length of output: 37


🏁 Script executed:

# Check the exact signature by looking at what parameters are required
sed -n '56,170p' ./ktor-server/ktor-server-plugins/ktor-server-auth/common/src/io/ktor/server/auth/OAuthProcedure.kt

Repository: ktorio/ktor

Length of output: 5233


🏁 Script executed:

# Let's verify the actual overload ambiguity by checking what Java sees
# Look at the complete function signatures with their parameter counts
cat ./ktor-server/ktor-server-plugins/ktor-server-auth/api/ktor-server-auth.api | grep -A 2 "OAuthProcedureKt"

Repository: ktorio/ktor

Length of output: 338


🏁 Script executed:

# Test if Kotlin compiler would accept a call with null for the 3rd parameter
cat > /tmp/test_null_ambiguity.kt << 'EOF'
package test

data class OpenIdConfiguration(val issuer: String)

interface AuthenticationConfig

fun AuthenticationConfig.oauth(
    name: String? = null,
    openIdConfiguration: OpenIdConfiguration,
    configure: (Any) -> Unit
) {
    println("OpenId overload")
}

fun AuthenticationConfig.oauth(
    name: String? = null,
    description: String? = null,
    configure: (Any) -> Unit
) {
    println("String overload")
}

fun main() {
    val config: AuthenticationConfig = object : AuthenticationConfig {}
    
    // Test 1: These should be unambiguous
    config.oauth("name", OpenIdConfiguration("issuer")) {}  // Clear - OpenId overload
    config.oauth("name", "desc") {}  // Clear - String overload
    
    // Test 2: What about null?
    // config.oauth("name", null) {}  // Would this compile?
}
EOF
python3 << 'PYEOF'
# Since we can't actually compile Kotlin, let's reason through the JVM signatures
# and what Java would see

api_lines = [
    "oauth (Lio/ktor/server/auth/AuthenticationConfig;Ljava/lang/String;Lio/ktor/server/auth/OpenIdConfiguration;Lkotlin/jvm/functions/Function1;)V",
    "oauth (Lio/ktor/server/auth/AuthenticationConfig;Ljava/lang/String;Ljava/lang/String;Lio/ktor/server/auth/OpenIdConfiguration;Lkotlin/jvm/functions/Function1;)V",
    "oauth (Lio/ktor/server/auth/AuthenticationConfig;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V",
    "oauth (Lio/ktor/server/auth/AuthenticationConfig;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V",
]

print("JVM Signatures for oauth:")
print("=" * 80)
for i, sig in enumerate(api_lines, 1):
    # Parse parameter count
    params = sig.count("L")
    print(f"{i}. Parameters: {params}")
    print(f"   {sig}")
    print()

print("\nAnalysis:")
print("-" * 80)
print("In Java, a call oauth(config, \"name\", null, function) has 4 arguments")
print("\nCan match:")
print("  - Signature 1 (4 params): oauth(..., String, OpenIdConfiguration, Function1)")
print("  - Signature 3 (4 params): oauth(..., String, String, Function1)")
print("\nBoth accept null as 3rd parameter:")
print("  - OpenIdConfiguration (nullable)")
print("  - String (nullable)")
print("\n⚠️  This IS ambiguous in Java!")
PYEOF

Repository: ktorio/ktor

Length of output: 1215


Java overload ambiguity with 4-argument oauth calls.

The 4-parameter overloads oauth(AuthenticationConfig, String, OpenIdConfiguration, Function1) and oauth(AuthenticationConfig, String, String, Function1) create ambiguity when called from Java with a null third argument—the compiler cannot determine which overload to invoke since both accept null. Consider adding @JvmName annotations to disambiguate, e.g., @JvmName("oauthWithOpenId") on the OpenIdConfiguration overload.

🤖 Prompt for AI Agents
In `@ktor-server/ktor-server-plugins/ktor-server-auth/api/ktor-server-auth.api`
around lines 488 - 493, The two 4-arg Kotlin overloads
oauth(AuthenticationConfig, String, OpenIdConfiguration, Function1) and
oauth(AuthenticationConfig, String, String, Function1) cause Java-call ambiguity
when the third parameter is null; annotate the Kotlin overloads with distinct
JVM names (e.g., add `@JvmName`("oauthWithOpenId") to the OpenIdConfiguration
variant and `@JvmName`("oauthWithClientId") to the String-variant) so the
generated methods have unique JVM signatures and Java callers can unambiguously
invoke the desired overload; apply the annotations to the corresponding oauth
functions (and their default dispatchers if necessary) so the compiled API
exposes disambiguated method names.

public static synthetic fun oauth$default (Lio/ktor/server/auth/AuthenticationConfig;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
public static synthetic fun oauth$default (Lio/ktor/server/auth/AuthenticationConfig;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
public static final fun staticSettings (Lio/ktor/server/auth/OAuthAuthenticationProvider;)Lio/ktor/server/auth/OAuthServerSettings;
Expand Down Expand Up @@ -542,14 +546,29 @@ public final class io/ktor/server/auth/OAuthVersion : java/lang/Enum {

public final class io/ktor/server/auth/OpenIdConfiguration {
public static final field Companion Lio/ktor/server/auth/OpenIdConfiguration$Companion;
public fun <init> (Ljava/lang/String;Ljava/lang/String;)V
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Ljava/lang/String;
public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lio/ktor/server/auth/OpenIdConfiguration;
public static synthetic fun copy$default (Lio/ktor/server/auth/OpenIdConfiguration;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/ktor/server/auth/OpenIdConfiguration;
public final fun component3 ()Ljava/lang/String;
public final fun component4 ()Ljava/lang/String;
public final fun component5 ()Ljava/lang/String;
public final fun component6 ()Ljava/util/List;
public final fun component7 ()Ljava/util/List;
public final fun component8 ()Ljava/util/List;
public final fun component9 ()Ljava/util/List;
public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;)Lio/ktor/server/auth/OpenIdConfiguration;
public static synthetic fun copy$default (Lio/ktor/server/auth/OpenIdConfiguration;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;ILjava/lang/Object;)Lio/ktor/server/auth/OpenIdConfiguration;
public fun equals (Ljava/lang/Object;)Z
public final fun getAuthorizationEndpoint ()Ljava/lang/String;
public final fun getClaimsSupported ()Ljava/util/List;
public final fun getIssuer ()Ljava/lang/String;
public final fun getJwksUri ()Ljava/lang/String;
public final fun getResponseTypesSupported ()Ljava/util/List;
public final fun getScopesSupported ()Ljava/util/List;
public final fun getTokenEndpoint ()Ljava/lang/String;
public final fun getTokenEndpointAuthMethodsSupported ()Ljava/util/List;
public final fun getUserInfoEndpoint ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
Expand All @@ -574,9 +593,6 @@ public final class io/ktor/server/auth/OpenIdConfigurationKt {
}

public final class io/ktor/server/auth/OpenIdOAuthConfig {
public field clientId Ljava/lang/String;
public field clientSecret Ljava/lang/String;
public field urlProvider Lkotlin/jvm/functions/Function3;
public fun <init> (Lio/ktor/server/auth/OpenIdConfiguration;)V
public final fun getAccessTokenRequiresBasicAuth ()Z
public final fun getClient ()Lio/ktor/client/HttpClient;
Expand Down
Loading