Skip to content

Commit 09aeddb

Browse files
committed
add test for invalid credentials scenario; refactor authentication server setup into reusable methods
1 parent a1d18ec commit 09aeddb

File tree

1 file changed

+48
-46
lines changed

1 file changed

+48
-46
lines changed

integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/AbstractAuthenticationTest.kt

Lines changed: 48 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package io.modelcontextprotocol.kotlin.sdk.integration
22

33
import io.kotest.matchers.shouldBe
44
import io.ktor.client.HttpClient
5+
import io.ktor.client.request.basicAuth
56
import io.ktor.client.request.get
67
import io.ktor.http.HttpStatusCode
78
import io.ktor.serialization.kotlinx.json.json
@@ -49,6 +50,8 @@ abstract class AbstractAuthenticationTest {
4950

5051
protected val validUser: String = "user-${UUID.randomUUID().toString().take(8)}"
5152
protected val validPassword: String = UUID.randomUUID().toString()
53+
protected val invalidUser: String = "user-${UUID.randomUUID().toString().take(8)}"
54+
protected val invalidPassword: String = UUID.randomUUID().toString()
5255

5356
/**
5457
* Installs Ktor plugins required by the transport under test.
@@ -72,27 +75,7 @@ abstract class AbstractAuthenticationTest {
7275

7376
@Test
7477
fun `mcp behind basic auth rejects unauthenticated requests with 401`(): Unit = runBlocking {
75-
val server = embeddedServer(ServerCIO, host = HOST, port = 0) {
76-
configurePlugins()
77-
install(Authentication) {
78-
basic(AUTH_REALM) {
79-
validate { credentials ->
80-
if (credentials.name == validUser && credentials.password == validPassword) {
81-
UserIdPrincipal(credentials.name)
82-
} else {
83-
null
84-
}
85-
}
86-
}
87-
}
88-
routing {
89-
authenticate(AUTH_REALM) {
90-
registerMcpServer {
91-
createMcpServer { principal<UserIdPrincipal>()?.name }
92-
}
93-
}
94-
}
95-
}.startSuspend(wait = false)
78+
val server = startAuthenticatedServer()
9679

9780
val httpClient = HttpClient(ClientCIO)
9881
try {
@@ -103,33 +86,26 @@ abstract class AbstractAuthenticationTest {
10386
}
10487
}
10588

89+
@Test
90+
fun `mcp rejects requests with invalid credentials with 401`(): Unit = runBlocking {
91+
val server = startAuthenticatedServer()
92+
93+
val httpClient = HttpClient(ClientCIO) {
94+
expectSuccess = false
95+
}
96+
try {
97+
httpClient.get("http://$HOST:${server.actualPort()}") {
98+
basicAuth(invalidUser, invalidPassword)
99+
}.status shouldBe HttpStatusCode.Unauthorized
100+
} finally {
101+
httpClient.close()
102+
server.stopSuspend(500, 1000)
103+
}
104+
}
105+
106106
@Test
107107
fun `authenticated mcp client can read resource scoped to principal`(): Unit = runBlocking {
108-
val server = embeddedServer(ServerCIO, host = HOST, port = 0) {
109-
configurePlugins()
110-
install(Authentication) {
111-
basic(AUTH_REALM) {
112-
validate { credentials ->
113-
if (credentials.name == validUser && credentials.password == validPassword) {
114-
UserIdPrincipal(credentials.name)
115-
} else {
116-
null
117-
}
118-
}
119-
}
120-
}
121-
routing {
122-
authenticate(AUTH_REALM) {
123-
registerMcpServer {
124-
// `this` is the ApplicationCall at connection time.
125-
// The lambda passed to createMcpServer captures this call;
126-
// principal<T>() is safe to call from resource handlers because
127-
// the call's authentication context remains valid for the session lifetime.
128-
createMcpServer { principal<UserIdPrincipal>()?.name }
129-
}
130-
}
131-
}
132-
}.startSuspend(wait = false)
108+
val server = startAuthenticatedServer()
133109

134110
val baseUrl = "http://$HOST:${server.actualPort()}"
135111
var mcpClient: Client? = null
@@ -154,6 +130,32 @@ abstract class AbstractAuthenticationTest {
154130
}
155131
}
156132

133+
private suspend fun startAuthenticatedServer() = embeddedServer(ServerCIO, host = HOST, port = 0) {
134+
configurePlugins()
135+
installBasicAuth()
136+
routing {
137+
authenticate(AUTH_REALM) {
138+
registerMcpServer {
139+
createMcpServer { principal<UserIdPrincipal>()?.name }
140+
}
141+
}
142+
}
143+
}.startSuspend(wait = false)
144+
145+
private fun Application.installBasicAuth() {
146+
install(Authentication) {
147+
basic(AUTH_REALM) {
148+
validate { credentials ->
149+
if (credentials.name == validUser && credentials.password == validPassword) {
150+
UserIdPrincipal(credentials.name)
151+
} else {
152+
null
153+
}
154+
}
155+
}
156+
}
157+
}
158+
157159
protected fun createMcpServer(principalProvider: () -> String?): Server = Server(
158160
serverInfo = Implementation(name = "test-server", version = "1.0.0"),
159161
options = ServerOptions(

0 commit comments

Comments
 (0)