From b4083eb862d924bdc34151bb65ac10e998455c9f Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 15 Jul 2026 16:34:36 -0700 Subject: [PATCH] feat(api): support organization-scoped sign-in tickets --- .../main/kotlin/com/clerk/api/auth/Auth.kt | 15 ++--- .../test/java/com/clerk/api/auth/AuthTest.kt | 20 +++++++ .../incoming/ClientSyncingMiddlewareTest.kt | 56 +++++++++++++++++++ 3 files changed, 82 insertions(+), 9 deletions(-) diff --git a/source/api/src/main/kotlin/com/clerk/api/auth/Auth.kt b/source/api/src/main/kotlin/com/clerk/api/auth/Auth.kt index f860aea75..35ab25a29 100644 --- a/source/api/src/main/kotlin/com/clerk/api/auth/Auth.kt +++ b/source/api/src/main/kotlin/com/clerk/api/auth/Auth.kt @@ -357,7 +357,11 @@ class Auth internal constructor() { } /** - * Signs in with a ticket. + * Signs in with a ticket generated by the Backend API. + * + * When the ticket is scoped to an organization, the resulting session is activated for that + * organization. The updated session, including [Session.lastActiveOrganizationId], is available + * through [Clerk.sessionFlow] when this method returns successfully. * * @param ticket The authentication ticket. * @return A [ClerkResult] containing the [SignIn] object on success, or a [ClerkErrorResponse] on @@ -369,14 +373,7 @@ class Auth internal constructor() { * ``` */ suspend fun signInWithTicket(ticket: String): ClerkResult { - val params = - mapOf( - "strategy" to com.clerk.api.Constants.Strategy.TICKET, - "ticket" to ticket, - "locale" to Clerk.locale.value.orEmpty(), - ) - - val result = ClerkApi.signIn.createSignIn(params) + val result = SignIn.create(SignIn.CreateParams.Strategy.Ticket(ticket)) result.onFailure { emitAuthError(it) } return result } diff --git a/source/api/src/test/java/com/clerk/api/auth/AuthTest.kt b/source/api/src/test/java/com/clerk/api/auth/AuthTest.kt index 7c1ce12e3..32817b6f5 100644 --- a/source/api/src/test/java/com/clerk/api/auth/AuthTest.kt +++ b/source/api/src/test/java/com/clerk/api/auth/AuthTest.kt @@ -214,6 +214,26 @@ class AuthTest { coVerify(exactly = 0) { signInApi.prepareSignInFirstFactor(any(), any()) } } + @Test + fun `signInWithTicket creates sign in with ticket strategy`() = runTest { + val signInApi = mockk() + val createdSignIn = SignIn(id = "sign_in_123") + val createParams = slot>() + mockkObject(ClerkApi) + every { ClerkApi.signIn } returns signInApi + coEvery { signInApi.createSignIn(capture(createParams)) } returns + ClerkResult.success(createdSignIn) + + val result = Auth().signInWithTicket("ticket_123") + + assertTrue(result is ClerkResult.Success) + assertSame(createdSignIn, (result as ClerkResult.Success).value) + assertEquals("ticket", createParams.captured["strategy"]) + assertEquals("ticket_123", createParams.captured["ticket"]) + assertTrue(createParams.captured.containsKey("locale")) + coVerify(exactly = 1) { signInApi.createSignIn(any()) } + } + @Test fun `sessions exposes all sessions on the current client`() { val firstSession = testSession("sess_1") diff --git a/source/api/src/test/java/com/clerk/api/network/middleware/incoming/ClientSyncingMiddlewareTest.kt b/source/api/src/test/java/com/clerk/api/network/middleware/incoming/ClientSyncingMiddlewareTest.kt index afa5b3a4b..a889e4985 100644 --- a/source/api/src/test/java/com/clerk/api/network/middleware/incoming/ClientSyncingMiddlewareTest.kt +++ b/source/api/src/test/java/com/clerk/api/network/middleware/incoming/ClientSyncingMiddlewareTest.kt @@ -180,6 +180,62 @@ class ClientSyncingMiddlewareTest { assertEquals(listOf(session), Clerk.sessionsFlow.value) } + @Test + fun `ticket sign in syncs session with active organization`() { + val middleware = ClientSyncingMiddleware(json = ClerkApi.json) + val request = + Request.Builder() + .url("https://api.clerk.com/v1/client/sign_ins") + .post( + "strategy=ticket&ticket=ticket_123" + .toRequestBody("application/x-www-form-urlencoded".toMediaType()) + ) + .build() + val response = + Response.Builder() + .request(request) + .protocol(Protocol.HTTP_1_1) + .code(200) + .message("OK") + .body( + """ + { + "response": { + "id": "sign_in_123", + "status": "complete", + "created_session_id": "sess_123" + }, + "client": { + "id": "client_123", + "sessions": [ + { + "id": "sess_123", + "status": "active", + "expire_at": 10000, + "last_active_at": 1000, + "last_active_organization_id": "org_123", + "created_at": 1000, + "updated_at": 1000 + } + ], + "last_active_session_id": "sess_123" + } + } + """ + .trimIndent() + .toResponseBody("application/json".toMediaType()) + ) + .build() + val chain = mockk() + every { chain.request() } returns request + every { chain.proceed(request) } returns response + + middleware.intercept(chain) + + assertEquals("sess_123", Clerk.sessionFlow.value?.id) + assertEquals("org_123", Clerk.sessionFlow.value?.lastActiveOrganizationId) + } + @Test fun `intercept hydrates direct client response with server date`() { val middleware = ClientSyncingMiddleware(json = ClerkApi.json)