From baea6c4849d185c57d0e193b11ae4b2596d671dc Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Mon, 10 Feb 2025 15:24:55 +0100 Subject: [PATCH 1/5] Unify http client test resource classes --- .../MatrixClientServerApiHttpClientTest.kt | 51 ++++--------------- 1 file changed, 10 insertions(+), 41 deletions(-) diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/MatrixClientServerApiHttpClientTest.kt b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/MatrixClientServerApiHttpClientTest.kt index 867e0edec..fdfc7b258 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/MatrixClientServerApiHttpClientTest.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/MatrixClientServerApiHttpClientTest.kt @@ -58,17 +58,7 @@ class MatrixClientServerApiHttpClientTest { data class PostPathWithoutAuth( @SerialName("pathParam") val pathParam: String, @SerialName("requestParam") val requestParam: String, - ) : MatrixEndpoint { - @Serializable - data class Request( - val includeDino: Boolean - ) - - @Serializable - data class Response( - val status: String - ) - } + ) : MatrixEndpoint @Serializable @Resource("/path/{pathParam}") @@ -77,17 +67,7 @@ class MatrixClientServerApiHttpClientTest { data class PostPathWithOptionalAuth( @SerialName("pathParam") val pathParam: String, @SerialName("requestParam") val requestParam: String, - ) : MatrixEndpoint { - @Serializable - data class Request( - val includeDino: Boolean - ) - - @Serializable - data class Response( - val status: String - ) - } + ) : MatrixEndpoint @Serializable @Resource("/path/{pathParam}") @@ -96,18 +76,7 @@ class MatrixClientServerApiHttpClientTest { data class PostPathWithUIA( @SerialName("pathParam") val pathParam: String, @SerialName("requestParam") val requestParam: String, - ) : MatrixUIAEndpoint { - @Serializable - data class Request( - val includeDino: Boolean - ) - - @Serializable - data class Response( - val status: String - ) - } - + ) : MatrixUIAEndpoint @Test fun itShouldHaveAuthenticationTokenIncludedAndDoNormalRequest() = runTest { @@ -423,8 +392,8 @@ class MatrixClientServerApiHttpClientTest { eventContentSerializerMappings = mappings, ) - cut.uiaRequest(PostPathWithUIA("1", "2"), PostPathWithUIA.Request(true)) - .getOrThrow() shouldBe UIA.Success(PostPathWithUIA.Response("ok")) + cut.uiaRequest(PostPathWithUIA("1", "2"), PostPath.Request(true)) + .getOrThrow() shouldBe UIA.Success(PostPath.Response("ok")) } @Test @@ -446,7 +415,7 @@ class MatrixClientServerApiHttpClientTest { ) val error = shouldThrow { - cut.uiaRequest(PostPathWithUIA("1", "2"), PostPathWithUIA.Request(true)).getOrThrow() + cut.uiaRequest(PostPathWithUIA("1", "2"), PostPath.Request(true)).getOrThrow() } error.statusCode shouldBe HttpStatusCode.NotFound assertEquals( @@ -479,7 +448,7 @@ class MatrixClientServerApiHttpClientTest { eventContentSerializerMappings = mappings, ) - val error = cut.uiaRequest(PostPathWithUIA("1", "2"), PostPathWithUIA.Request(true)).getOrThrow() + val error = cut.uiaRequest(PostPathWithUIA("1", "2"), PostPath.Request(true)).getOrThrow() .shouldBeInstanceOf>() assertEquals( ErrorResponse.UnknownToken::class, @@ -512,7 +481,7 @@ class MatrixClientServerApiHttpClientTest { eventContentSerializerMappings = mappings, ) - val error = cut.uiaRequest(PostPathWithUIA("1", "2"), PostPathWithUIA.Request(true)).getOrThrow() + val error = cut.uiaRequest(PostPathWithUIA("1", "2"), PostPath.Request(true)).getOrThrow() .shouldBeInstanceOf>() assertEquals( ErrorResponse.UserLocked::class, @@ -600,7 +569,7 @@ class MatrixClientServerApiHttpClientTest { eventContentSerializerMappings = mappings, ) - val result = cut.uiaRequest(PostPathWithUIA("1", "2"), PostPathWithUIA.Request(true)).getOrThrow() + val result = cut.uiaRequest(PostPathWithUIA("1", "2"), PostPath.Request(true)).getOrThrow() result.shouldBeInstanceOf>() result.state shouldBe UIAState( completed = listOf(), @@ -810,7 +779,7 @@ class MatrixClientServerApiHttpClientTest { ) val result1 = cut.uiaRequest( PostPathWithUIA("1", "2"), - PostPathWithUIA.Request(true) + PostPath.Request(true) ).getOrThrow() result1.shouldBeInstanceOf>() result1.state shouldBe expectedUIAState -- GitLab From d1ffd8154edf69eae44dc5e789b035894bd8dc5d Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Mon, 10 Feb 2025 16:27:38 +0100 Subject: [PATCH 2/5] Add documentation for authrequired enum --- .../net/folivo/trixnity/core/MatrixEndpoint.kt | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/MatrixEndpoint.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/MatrixEndpoint.kt index 83f499550..505c5c7ee 100644 --- a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/MatrixEndpoint.kt +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/MatrixEndpoint.kt @@ -53,7 +53,21 @@ annotation class Auth(val required: AuthRequired) annotation class ForceJson enum class AuthRequired { - YES, OPTIONAL, NO; + /** + * Client: Send token if available + * Server: Always require token, error if none included + */ + YES, + /** + * Client: Send token if available + * Server: Use token if included + */ + OPTIONAL, + /** + * Client: Send token if requested by server + * Server: Ignore token, even if included + */ + NO; companion object { val attributeKey = AttributeKey("matrixEndpointAuthenticationRequired") -- GitLab From ce0e316b98f8b7df471924a53db297378b338894 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Mon, 10 Feb 2025 16:28:40 +0100 Subject: [PATCH 3/5] Add test case for AuthRequired.NO --- .../MatrixClientServerApiHttpClientTest.kt | 50 ++++++++++++++++--- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/MatrixClientServerApiHttpClientTest.kt b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/MatrixClientServerApiHttpClientTest.kt index fdfc7b258..8ce6febd4 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/MatrixClientServerApiHttpClientTest.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/MatrixClientServerApiHttpClientTest.kt @@ -103,7 +103,7 @@ class MatrixClientServerApiHttpClientTest { ) cut.request(PostPath("1", "2"), PostPath.Request(true)).getOrThrow() shouldBe - PostPath.Response("ok") + PostPath.Response("ok") } @Test @@ -126,7 +126,41 @@ class MatrixClientServerApiHttpClientTest { ) cut.request(PostPathWithoutAuth("1", "2"), PostPath.Request(true)).getOrThrow() shouldBe - PostPath.Response("ok") + PostPath.Response("ok") + } + + @Test + fun itShouldRetryWithToken() = runTest { + val testTokenStore = ClassicMatrixAuthProvider.BearerTokensStore.InMemory() + + val cut = MatrixClientServerApiHttpClient( + baseUrl = Url("https://matrix.host"), + authProvider = MatrixAuthProvider.classic(testTokenStore), + httpClientEngine = scopedMockEngine(false) { + addHandler { request -> + if (request.headers[HttpHeaders.Authorization] == "Bearer access") { + respond( + """{"status":"ok"}""", + HttpStatusCode.OK, + headersOf(HttpHeaders.ContentType, Application.Json.toString()) + ) + } else { + respond( + """{"status":"not ok"}""", + HttpStatusCode.Unauthorized, + headersOf(HttpHeaders.ContentType, Application.Json.toString(),) + ) + } + } + }, + json = json, + eventContentSerializerMappings = mappings, + ) + + testTokenStore.bearerTokens = ClassicMatrixAuthProvider.BearerTokens("access", null) + + cut.request(PostPathWithoutAuth("1", "2"), PostPath.Request(true)) + .getOrThrow() shouldBe PostPath.Response("ok") } @Test @@ -159,12 +193,12 @@ class MatrixClientServerApiHttpClientTest { ) cut.request(PostPathWithOptionalAuth("1", "2"), PostPath.Request(true)).getOrThrow() shouldBe - PostPath.Response("ok") + PostPath.Response("ok") testTokenStore.bearerTokens = ClassicMatrixAuthProvider.BearerTokens("access", null) cut.request(PostPathWithOptionalAuth("1", "2"), PostPath.Request(true)).getOrThrow() shouldBe - PostPath.Response("ok") + PostPath.Response("ok") } @Test @@ -230,7 +264,7 @@ class MatrixClientServerApiHttpClientTest { ) cut.request(PostPath("1", "2"), PostPath.Request(true)).getOrThrow() shouldBe - PostPath.Response("ok") + PostPath.Response("ok") refreshCalled shouldBe true onLogout shouldBe null } @@ -602,7 +636,7 @@ class MatrixClientServerApiHttpClientTest { ) result.authenticate(AuthenticationRequest.Password(IdentifierType.User("username"), "password")) result.getFallbackUrl(AuthenticationType.Password).toString() shouldBe - "https://matrix.host/_matrix/client/v3/auth/m.login.password/fallback/web?session=session1" + "https://matrix.host/_matrix/client/v3/auth/m.login.password/fallback/web?session=session1" } @Test @@ -785,14 +819,14 @@ class MatrixClientServerApiHttpClientTest { result1.state shouldBe expectedUIAState result1.errorResponse shouldBe ErrorResponse.NotFound("") result1.getFallbackUrl(AuthenticationType.Password).toString() shouldBe - "https://matrix.host/_matrix/client/v3/auth/m.login.password/fallback/web?session=session1" + "https://matrix.host/_matrix/client/v3/auth/m.login.password/fallback/web?session=session1" val result2 = result1.authenticate(AuthenticationRequest.Password(IdentifierType.User("username"), "password")) .getOrThrow() result2.shouldBeInstanceOf>() result2.state shouldBe expectedUIAState result2.errorResponse shouldBe ErrorResponse.NotFound("") result2.getFallbackUrl(AuthenticationType.Password).toString() shouldBe - "https://matrix.host/_matrix/client/v3/auth/m.login.password/fallback/web?session=session1" + "https://matrix.host/_matrix/client/v3/auth/m.login.password/fallback/web?session=session1" result2.authenticate(AuthenticationRequest.Password(IdentifierType.User("username"), "password")) .getOrThrow() requestCount shouldBe 3 -- GitLab From eda0a33df30d86747c6ea540a9c94614a44a3401 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Mon, 10 Feb 2025 16:31:44 +0100 Subject: [PATCH 4/5] Add AuthRequired.NEVER option --- .../client/ClassicAuthProviderFactory.kt | 1 + .../client/MatrixClientServerApiHttpClient.kt | 13 ++++++ .../MatrixClientServerApiHttpClientTest.kt | 41 +++++++++++++++++++ .../model/authentication/Register.kt | 2 +- .../server/MatrixAccessTokenAuth.kt | 1 + .../folivo/trixnity/core/MatrixEndpoint.kt | 7 +++- .../server/MatrixSignatureAuth.kt | 3 +- 7 files changed, 65 insertions(+), 3 deletions(-) diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/ClassicAuthProviderFactory.kt b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/ClassicAuthProviderFactory.kt index 8b49bf38f..25778279b 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/ClassicAuthProviderFactory.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/ClassicAuthProviderFactory.kt @@ -115,6 +115,7 @@ class ClassicMatrixAuthProvider( AuthRequired.YES -> true AuthRequired.OPTIONAL -> true AuthRequired.NO -> false + AuthRequired.NEVER -> false else -> false } diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/MatrixClientServerApiHttpClient.kt b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/MatrixClientServerApiHttpClient.kt index 6d311550e..da02305e4 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/MatrixClientServerApiHttpClient.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/MatrixClientServerApiHttpClient.kt @@ -5,6 +5,7 @@ import io.ktor.client.* import io.ktor.client.call.* import io.ktor.client.engine.* import io.ktor.client.plugins.* +import io.ktor.client.plugins.api.* import io.ktor.client.plugins.auth.* import io.ktor.client.plugins.resources.* import io.ktor.client.request.* @@ -35,6 +36,17 @@ interface MatrixAuthProvider : AuthProvider { companion object } +/** + * Plugin to fully disable auth for requests with AuthRequired set to NEVER + */ +private val SkipAuthenticationIfDisabled: ClientPlugin = createClientPlugin("SkipAuthenticationIfDisabled") { + onRequest { request, _ -> + if (request.attributes.getOrNull(AuthRequired.attributeKey) == AuthRequired.NEVER) { + request.attributes.put(AuthCircuitBreaker, Unit) + } + } +} + class MatrixClientServerApiHttpClient( val baseUrl: Url? = null, authProvider: MatrixAuthProvider = MatrixAuthProvider.classicInMemory(), @@ -51,6 +63,7 @@ class MatrixClientServerApiHttpClient( install(DefaultRequest) { if (baseUrl != null) url.takeFrom(baseUrl) } + install(SkipAuthenticationIfDisabled) install(Auth) { providers.add(authProvider) } diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/MatrixClientServerApiHttpClientTest.kt b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/MatrixClientServerApiHttpClientTest.kt index 8ce6febd4..4248d45ff 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/MatrixClientServerApiHttpClientTest.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/MatrixClientServerApiHttpClientTest.kt @@ -21,6 +21,7 @@ import net.folivo.trixnity.clientserverapi.model.uia.UIAState import net.folivo.trixnity.core.* import net.folivo.trixnity.core.HttpMethod import net.folivo.trixnity.core.HttpMethodType.POST +import net.folivo.trixnity.core.MatrixServerException import net.folivo.trixnity.core.serialization.createDefaultEventContentSerializerMappings import net.folivo.trixnity.core.serialization.createMatrixEventJson import net.folivo.trixnity.testutils.scopedMockEngine @@ -69,6 +70,15 @@ class MatrixClientServerApiHttpClientTest { @SerialName("requestParam") val requestParam: String, ) : MatrixEndpoint + @Serializable + @Resource("/path/{pathParam}") + @HttpMethod(POST) + @Auth(AuthRequired.NEVER) + data class PostPathWithDisabledAuth( + @SerialName("pathParam") val pathParam: String, + @SerialName("requestParam") val requestParam: String, + ) : MatrixEndpoint + @Serializable @Resource("/path/{pathParam}") @HttpMethod(POST) @@ -129,6 +139,37 @@ class MatrixClientServerApiHttpClientTest { PostPath.Response("ok") } + @Test + fun itShouldNotRetryWithTokenOnNever() = runTest { + val testTokenStore = ClassicMatrixAuthProvider.BearerTokensStore.InMemory() + + val cut = MatrixClientServerApiHttpClient( + baseUrl = Url("https://matrix.host"), + authProvider = MatrixAuthProvider.classic(testTokenStore), + httpClientEngine = scopedMockEngine(false) { + addHandler { request -> + request.headers[HttpHeaders.Authorization] shouldBe null + respond( + """{"status":"ok"}""", + HttpStatusCode.Unauthorized, + headersOf(HttpHeaders.ContentType, Application.Json.toString(),) + ) + } + }, + json = json, + eventContentSerializerMappings = mappings, + ) + + testTokenStore.bearerTokens = ClassicMatrixAuthProvider.BearerTokens("access", null) + + cut.request(PostPathWithDisabledAuth("1", "2"), PostPath.Request(true)) + .exceptionOrNull() shouldBe + MatrixServerException( + HttpStatusCode.Unauthorized, + ErrorResponse.CustomErrorResponse("UNKNOWN", error="""{"status":"ok"}""") + ) + } + @Test fun itShouldRetryWithToken() = runTest { val testTokenStore = ClassicMatrixAuthProvider.BearerTokensStore.InMemory() diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/authentication/Register.kt b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/authentication/Register.kt index 38b0a28ee..8d0040dc6 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/authentication/Register.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/authentication/Register.kt @@ -16,7 +16,7 @@ import net.folivo.trixnity.core.model.UserId @Serializable @Resource("/_matrix/client/v3/register") @HttpMethod(POST) -@Auth(AuthRequired.NO) +@Auth(AuthRequired.NEVER) data class Register( @SerialName("kind") val kind: AccountType? = null, ) : MatrixUIAEndpoint { diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-server/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/server/MatrixAccessTokenAuth.kt b/trixnity-clientserverapi/trixnity-clientserverapi-server/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/server/MatrixAccessTokenAuth.kt index 9eb219de8..ad9696113 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-server/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/server/MatrixAccessTokenAuth.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-server/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/server/MatrixAccessTokenAuth.kt @@ -94,6 +94,7 @@ fun AuthenticationConfig.matrixAccessTokenAuth( !it.request.queryParameters.contains("access_token") AuthRequired.NO -> true + AuthRequired.NEVER -> true null -> false } } diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/MatrixEndpoint.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/MatrixEndpoint.kt index 505c5c7ee..5469ac5f3 100644 --- a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/MatrixEndpoint.kt +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/MatrixEndpoint.kt @@ -67,7 +67,12 @@ enum class AuthRequired { * Client: Send token if requested by server * Server: Ignore token, even if included */ - NO; + NO, + /** + * Client: Never send token + * Server: Ignore token, even if included + */ + NEVER; companion object { val attributeKey = AttributeKey("matrixEndpointAuthenticationRequired") diff --git a/trixnity-serverserverapi/trixnity-serverserverapi-server/src/commonMain/kotlin/net/folivo/trixnity/serverserverapi/server/MatrixSignatureAuth.kt b/trixnity-serverserverapi/trixnity-serverserverapi-server/src/commonMain/kotlin/net/folivo/trixnity/serverserverapi/server/MatrixSignatureAuth.kt index b15c11f7f..e0717273f 100644 --- a/trixnity-serverserverapi/trixnity-serverserverapi-server/src/commonMain/kotlin/net/folivo/trixnity/serverserverapi/server/MatrixSignatureAuth.kt +++ b/trixnity-serverserverapi/trixnity-serverserverapi-server/src/commonMain/kotlin/net/folivo/trixnity/serverserverapi/server/MatrixSignatureAuth.kt @@ -120,7 +120,8 @@ fun AuthenticationConfig.matrixSignatureAuth( .apply(configure) .apply { skipWhen { - it.attributes.getOrNull(AuthRequired.attributeKey) == AuthRequired.NO + val authRequired = it.attributes.getOrNull(AuthRequired.attributeKey) + authRequired == AuthRequired.NO || authRequired == AuthRequired.NEVER } }) register(provider) -- GitLab From 9b5cb5bc56a10ba5ef2327a9a9a4c2d04cf7637e Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Mon, 10 Feb 2025 16:35:20 +0100 Subject: [PATCH 5/5] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 854078c5f..d297e0b90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Send access token to endpoints when server wants to +- Fix error enumerating account registration flows ### Security -- GitLab