Skip to content

Commit 0ba802f

Browse files
umair-ablyclaude
andcommitted
Run AuthReauthTest on the server UTS leg via a claim-bearing JWT
The UTS spec authenticates this test with a JWT; the Java port had substituted a native TokenRequest for convenience. Restoring the JWT (AblyJwt: HS256 via JDK crypto, no external library) lets the test carry the signed x-ably-clientType=server claim on the server leg — the only server-side declaration realtime accepts on token auth — so it now runs on every leg instead of being skipped. Verified against sandbox: the claim-bearing JWT connects and re-authenticates where the bare agent flag was rejected with 40167. assumeSideSupportsTokenAuth stays for the native-token tests (TokenRequestTest): the native token format cannot carry the claim yet. Also drops DR/ticket numbers from code comments in this PR's files; the behavior is described in place instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent d4177a7 commit 0ba802f

4 files changed

Lines changed: 72 additions & 20 deletions

File tree

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package io.ably.lib.uts.infra.integration
2+
3+
import java.util.Base64
4+
import javax.crypto.Mac
5+
import javax.crypto.spec.SecretKeySpec
6+
7+
/**
8+
* Minimal HS256 Ably JWT signer, built on JDK crypto only (no external JWT library).
9+
*
10+
* Exists for tests that need token claims the native Ably token format cannot carry —
11+
* notably `x-ably-clientType=server`: on token auth the realtime service accepts a
12+
* server-side declaration only from that signed claim (a `-server` agent entry alone is
13+
* rejected with error 40167), and the native token format cannot carry the claim yet. So a
14+
* JWT is the one way a token-authenticated client can declare the server side, and JWT-based
15+
* tests can run on the server UTS leg while native-token tests remain skipped (see
16+
* assumeSideSupportsTokenAuth).
17+
*/
18+
object AblyJwt {
19+
/**
20+
* Signs a JWT with the given Ably API key (`keyName:keySecret`), valid for [ttlSeconds],
21+
* with wildcard capability, and the optional Ably claims.
22+
*/
23+
fun sign(
24+
keyStr: String,
25+
clientId: String? = null,
26+
clientType: String? = null,
27+
ttlSeconds: Long = 3600,
28+
): String {
29+
val keyName = keyStr.substringBefore(':')
30+
val keySecret = keyStr.substringAfter(':')
31+
val now = System.currentTimeMillis() / 1000
32+
val header = """{"typ":"JWT","alg":"HS256","kid":"$keyName"}"""
33+
val claims = buildString {
34+
append("""{"iat":$now,"exp":${now + ttlSeconds},"x-ably-capability":"{\"*\":[\"*\"]}"""")
35+
if (clientId != null) append(""","x-ably-clientId":"$clientId"""")
36+
if (clientType != null) append(""","x-ably-clientType":"$clientType"""")
37+
append("}")
38+
}
39+
val enc = Base64.getUrlEncoder().withoutPadding()
40+
val signingInput = enc.encodeToString(header.toByteArray(Charsets.UTF_8)) + "." +
41+
enc.encodeToString(claims.toByteArray(Charsets.UTF_8))
42+
val mac = Mac.getInstance("HmacSHA256").apply {
43+
init(SecretKeySpec(keySecret.toByteArray(Charsets.UTF_8), "HmacSHA256"))
44+
}
45+
val signature = enc.encodeToString(mac.doFinal(signingInput.toByteArray(Charsets.UTF_8)))
46+
return "$signingInput.$signature"
47+
}
48+
}

‎uts/src/test/kotlin/io/ably/lib/uts/infra/unit/ClientFactories.kt‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,21 +38,21 @@ class ClientOptionsBuilder : DebugOptions("appId.keyId:keySecret") {
3838
val utsSide: String = System.getProperty("uts.side").let { if (it.isNullOrEmpty()) "core" else it }
3939

4040
/**
41-
* Call at the top of any test whose client authenticates with a token.
41+
* Call at the top of any test whose client authenticates with a native Ably token.
4242
*
4343
* Realtime rejects a token-authenticated connection that declares the server side through the
4444
* agent entry alone: error 40167, "a connection or request may only declare itself as a server
45-
* via a signed x-ably-clientType token claim". The signed-claim mechanism is PDR-091's deferred
46-
* decision D2 and nothing in the test infrastructure can mint such a claim yet, so until D2
47-
* lands, token-auth conformance runs on the core leg only and is skipped (not failed) on the
48-
* server leg. Key-auth tests are unaffected — on API-key auth the agent entry is the accepted
49-
* declaration.
45+
* via a signed x-ably-clientType token claim". The native Ably token format cannot carry that
46+
* claim yet, so until the platform supports it, native-token conformance runs on the core leg
47+
* only and is skipped (not failed) on the server leg. Key-auth tests are unaffected — on
48+
* API-key auth the agent entry is the accepted declaration — and JWT-based tests can carry the
49+
* claim already (see AblyJwt), so they run on every leg instead of calling this.
5050
*/
5151
fun assumeSideSupportsTokenAuth() = org.junit.jupiter.api.Assumptions.assumeTrue(
5252
utsSide == "core",
53-
"token-auth conformance is skipped on the '$utsSide' leg: realtime requires a signed " +
53+
"native-token conformance is skipped on the '$utsSide' leg: realtime requires a signed " +
5454
"x-ably-clientType token claim (40167) to declare the server side on token auth, " +
55-
"which the test infrastructure cannot mint until PDR-091 D2 lands",
55+
"and the native token format cannot carry that claim yet",
5656
)
5757

5858
fun TestRealtimeClient(block: ClientOptionsBuilder.() -> Unit): AblyRealtime {

‎uts/src/test/kotlin/io/ably/lib/uts/integration/proxy/realtime/AuthReauthTest.kt‎

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
package io.ably.lib.uts.integration.proxy.realtime
22

33
import io.ably.lib.realtime.ConnectionState
4-
import io.ably.lib.rest.AblyRest
54
import io.ably.lib.rest.Auth
65
import io.ably.lib.uts.infra.awaitState
6+
import io.ably.lib.uts.infra.integration.AblyJwt
77
import io.ably.lib.uts.infra.integration.SandboxApp
88
import io.ably.lib.uts.infra.integration.proxy.ProxyManager
99
import io.ably.lib.uts.infra.integration.proxy.ProxySession
1010
import io.ably.lib.uts.infra.integration.proxy.connectThroughProxy
1111
import io.ably.lib.uts.infra.pollUntil
12-
import io.ably.lib.uts.infra.unit.assumeSideSupportsTokenAuth
1312
import io.ably.lib.uts.infra.unit.TestRealtimeClient
13+
import io.ably.lib.uts.infra.unit.utsSide
1414
import kotlinx.coroutines.runBlocking
1515
import kotlinx.coroutines.test.runTest
1616
import org.junit.jupiter.api.AfterAll
@@ -56,19 +56,23 @@ class AuthReauthTest {
5656
*/
5757
@Test
5858
fun `RTN22, RTC8a - server-initiated re-authentication`() = runTest {
59-
assumeSideSupportsTokenAuth()
6059
// No proxy rules: the AUTH injection is triggered imperatively after the SDK connects.
6160
val session = ProxySession.create(rules = emptyList())
6261

6362
// Re-authentication is observed via an authCallback. The spec generates a JWT from the
64-
// sandbox key parts; the idiomatic ably-java equivalent is a locally-signed TokenRequest
65-
// produced from the same key — no external JWT library required. The realtime client then
66-
// exchanges it for a token (through the proxy), satisfying RTC8a.
67-
val tokenSigner = AblyRest(app.defaultKey)
63+
// sandbox key parts, and so does this test (AblyJwt: HS256 via JDK crypto, no external
64+
// library). A JWT rather than a native TokenRequest is load-bearing on the server UTS
65+
// leg: a token-authenticated client may declare the server side only via the signed
66+
// x-ably-clientType claim, which the native token format cannot carry yet — so the JWT
67+
// carries the claim on the server leg, and this test runs on every leg.
6868
val authCallbackCount = AtomicInteger(0)
6969
val authCallback = Auth.TokenCallback { params ->
7070
authCallbackCount.incrementAndGet()
71-
tokenSigner.auth.createTokenRequest(params, null)
71+
AblyJwt.sign(
72+
app.defaultKey,
73+
clientId = params.clientId,
74+
clientType = if (utsSide == "server") "server" else null,
75+
)
7276
}
7377

7478
// Keep the JSON protocol (ClientOptionsBuilder default): the proxy injects/inspects frames
@@ -130,13 +134,12 @@ class AuthReauthTest {
130134
"Expected at least one client-to-server AUTH frame carrying auth details",
131135
)
132136
} finally {
133-
// Nest teardown so session/tokenSigner are always cleaned up even if close-wait times out.
137+
// Nest teardown so the session is always cleaned up even if close-wait times out.
134138
try {
135139
client.close()
136140
awaitState(client, ConnectionState.closed, 10.seconds)
137141
} finally {
138142
session.close()
139-
runCatching { tokenSigner.close() }
140143
}
141144
}
142145
}

‎uts/src/test/kotlin/io/ably/lib/uts/unit/SideModesTest.kt‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ package io.ably.lib.uts.unit
1111
* quietly constructs plain core clients, turning the server CI leg into a duplicate of the
1212
* core leg.
1313
*
14-
* The side entry is a versionless flag — a bare token, per ably/ably-common#361 — so the
15-
* assertions also fail if a `/version` form regresses. Mirrors ably-js's side_modes.test.ts.
14+
* The side entry is registered in the ably-common agents registry as a versionless flag — a
15+
* bare token — so the assertions also fail if a `/version` form regresses. Mirrors ably-js's
16+
* side_modes.test.ts.
1617
*/
1718

1819
import io.ably.lib.rest.AblyBase

0 commit comments

Comments
 (0)