Skip to content

Commit

Permalink
Add get tenants API
Browse files Browse the repository at this point in the history
  • Loading branch information
itaihanski committed Nov 21, 2024
1 parent 141ab88 commit 25dfbc7
Show file tree
Hide file tree
Showing 8 changed files with 225 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,16 @@ internal open class DescopeClient(internal val config: DescopeConfig) : HttpClie
headers = authorization(refreshJwt),
)

suspend fun tenants(dct: Boolean, tenantIds: List<String>, refreshJwt: String): TenantsResponse = post(
route = "auth/me/tenants",
decoder = TenantsResponse::fromJson,
headers = authorization(refreshJwt),
body = mapOf(
"dct" to dct,
"ids" to tenantIds,
)
)

suspend fun refresh(refreshJwt: String): JwtServerResponse = post(
route = "auth/refresh",
decoder = JwtServerResponse::fromJson,
Expand Down
25 changes: 25 additions & 0 deletions descopesdk/src/main/java/com/descope/internal/http/Responses.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.descope.internal.http
import com.descope.internal.others.optionalMap
import com.descope.internal.others.secToMs
import com.descope.internal.others.stringOrEmptyAsNull
import com.descope.internal.others.toObjectList
import com.descope.internal.others.toStringList
import org.json.JSONObject
import java.net.HttpCookie
Expand Down Expand Up @@ -78,6 +79,30 @@ internal data class UserResponse(
}
}

internal data class TenantsResponse(
val tenants: List<Tenant>,
) {

data class Tenant(
val tenantId: String,
val name: String,
val customAttributes: Map<String, Any>,
)

companion object {
@Suppress("UNUSED_PARAMETER")
fun fromJson(json: String, cookies: List<HttpCookie>) = JSONObject(json).run {
TenantsResponse(tenants = getJSONArray("tenants").toObjectList().map {
Tenant(
tenantId = it.getString("id"),
name = it.getString("name"),
customAttributes = it.optionalMap("customAttributes"),
)
})
}
}
}

internal data class MaskedAddressServerResponse(
val maskedEmail: String? = null,
val maskedPhone: String? = null,
Expand Down
8 changes: 8 additions & 0 deletions descopesdk/src/main/java/com/descope/internal/others/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ internal fun JSONArray.toStringList(): List<String> {
return list
}

internal fun JSONArray.toObjectList(): List<JSONObject> {
val list = mutableListOf<JSONObject>()
for (i in 0 until length()) {
list.add(getJSONObject(i))
}
return list
}

internal fun List<*>.toJsonArray(): JSONArray = JSONArray().apply {
this@toJsonArray.forEach {
when {
Expand Down
19 changes: 19 additions & 0 deletions descopesdk/src/main/java/com/descope/internal/routes/Auth.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.descope.internal.routes

import com.descope.internal.http.DescopeClient
import com.descope.internal.http.TenantsResponse
import com.descope.sdk.DescopeAuth
import com.descope.types.DescopeTenant
import com.descope.types.DescopeUser
import com.descope.types.RevokeType
import com.descope.types.RefreshResponse
Expand All @@ -16,6 +18,13 @@ internal class Auth(private val client: DescopeClient) : DescopeAuth {
me(refreshJwt)
}

override suspend fun tenants(dct: Boolean, tenantIds: List<String>, refreshJwt: String): List<DescopeTenant> =
client.tenants(dct, tenantIds, refreshJwt).convert()

override fun tenants(dct: Boolean, tenantIds: List<String>, refreshJwt: String, callback: (Result<List<DescopeTenant>>) -> Unit) = wrapCoroutine(callback) {
tenants(dct, tenantIds, refreshJwt)
}

override suspend fun refreshSession(refreshJwt: String): RefreshResponse =
client.refresh(refreshJwt).toRefreshResponse()

Expand All @@ -42,3 +51,13 @@ internal class Auth(private val client: DescopeClient) : DescopeAuth {
revokeSessions(RevokeType.CurrentSession, refreshJwt, callback)

}

internal fun TenantsResponse.convert(): List<DescopeTenant> {
return tenants.map {
DescopeTenant(
tenantId = it.tenantId,
name = it.name,
customAttributes = it.customAttributes,
)
}
}
17 changes: 17 additions & 0 deletions descopesdk/src/main/java/com/descope/sdk/Routes.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import androidx.browser.customtabs.CustomTabsIntent
import com.descope.session.DescopeSession
import com.descope.types.AuthenticationResponse
import com.descope.types.DeliveryMethod
import com.descope.types.DescopeTenant
import com.descope.types.DescopeUser
import com.descope.types.EnchantedLinkResponse
import com.descope.types.RevokeType
Expand Down Expand Up @@ -39,6 +40,22 @@ interface DescopeAuth {
/** @see me */
fun me(refreshJwt: String, callback: (Result<DescopeUser>) -> Unit)

/**
* Returns the current session user tenants.
*
* @param dct Set this to `true` and leave [tenantIds] empty to request the current
* tenant for the user as set in the `dct` claim. This will fail if a tenant
* hasn't already been selected.
* @param tenantIds Provide a non-empty array of tenant IDs and set `dct` to `false`
* to request a specific list of tenants for the user.
* @param refreshJwt The refreshJwt from an active [DescopeSession].
* @return A list of one or more [DescopeTenant] values.
*/
suspend fun tenants(dct: Boolean, tenantIds: List<String>, refreshJwt: String): List<DescopeTenant>

/** @see tenants */
fun tenants(dct: Boolean, tenantIds: List<String>, refreshJwt: String, callback: (Result<List<DescopeTenant>>) -> Unit)

/**
* Refreshes a [DescopeSession].
*
Expand Down
21 changes: 21 additions & 0 deletions descopesdk/src/main/java/com/descope/types/Tenant.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.descope.types

import com.descope.sdk.DescopeAuth

/**
* The [DescopeTenant] class represents a tenant in Descope.
*
* You can retrieve the tenants for a user after authentication by calling [DescopeAuth.tenants].
*
* @property tenantId The unique identifier for the user in the project.
* This is either an automatically generated value or a custom value that was set
* when the tenant was created.
* @property name The name of the tenant.
* @property customAttributes A mapping of any custom attributes associated with this tenant. The custom attributes
* are managed via the Descope console.
*/
data class DescopeTenant(
val tenantId: String,
val name: String,
val customAttributes: Map<String, Any>,
)
117 changes: 117 additions & 0 deletions descopesdk/src/test/java/com/descope/internal/routes/AuthTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package com.descope.internal.routes

import com.descope.internal.http.TenantsResponse
import com.descope.internal.http.UserResponse
import com.descope.types.RevokeType
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Test

class AuthTest {
@Test
fun me() = runTest {
val client = MockClient()
val auth = Auth(client)
client.assert = { route: String, _: Map<String, Any?>, headers: Map<String, String>, _: Map<String, String?> ->
assertEquals("auth/me", route)
val authorizationHeader = headers["Authorization"]
assertNotNull(authorizationHeader)
assertTrue(authorizationHeader!!.contains("refreshJwt"))
}
client.response = UserResponse(
userId = "userId",
loginIds = listOf("loginId"),
name = "name",
picture = null,
email = null,
verifiedEmail = false,
phone = null,
verifiedPhone = false,
createdTime = 0L,
customAttributes = emptyMap(),
givenName = null,
middleName = null,
familyName = null,
)
val response = auth.me("refreshJwt")
assertEquals("name", response.name)
assertEquals(1, client.calls)
}

@Test
fun tenants() = runTest {
val client = MockClient()
val auth = Auth(client)
client.assert = { route: String, body: Map<String, Any?>, headers: Map<String, String>, _: Map<String, String?> ->
assertEquals("auth/me/tenants", route)
val authorizationHeader = headers["Authorization"]
assertNotNull(authorizationHeader)
assertTrue(authorizationHeader!!.contains("refreshJwt"))
assertEquals(true, body["dct"])
}
client.response = TenantsResponse(
tenants = listOf(
TenantsResponse.Tenant("id1", "t1", emptyMap()),
TenantsResponse.Tenant("id2", "t2", mapOf("a" to "b", "c" to "d"))
),
)
val response = auth.tenants(true, emptyList(), "refreshJwt")
assertEquals(2, response.size)
assertEquals("id1", response[0].tenantId)
assertEquals("t1", response[0].name)
assertEquals("id2", response[1].tenantId)
assertEquals("t2", response[1].name)
assertEquals(2, response[1].customAttributes.size)
assertEquals(1, client.calls)
}

@Test
fun refreshSession() = runTest {
val client = MockClient()
val auth = Auth(client)
client.assert = { route: String, _: Map<String, Any?>, headers: Map<String, String>, _: Map<String, String?> ->
assertEquals("auth/refresh", route)
val authorizationHeader = headers["Authorization"]
assertNotNull(authorizationHeader)
assertTrue(authorizationHeader!!.contains("refreshJwt"))
}
client.response = mockJwtResponse
val response = auth.refreshSession("refreshJwt")
assertEquals(jwt, response.sessionToken.jwt)
assertEquals(jwt, response.refreshToken!!.jwt)
assertEquals(1, client.calls)
}

@Test
fun revokeSession_current() = runTest {
val client = MockClient()
val auth = Auth(client)
client.assert = { route: String, _: Map<String, Any?>, headers: Map<String, String>, _: Map<String, String?> ->
assertEquals("auth/logout", route)
val authorizationHeader = headers["Authorization"]
assertNotNull(authorizationHeader)
assertTrue(authorizationHeader!!.contains("refreshJwt"))
}
client.response = Unit
auth.revokeSessions(RevokeType.CurrentSession, "refreshJwt")
assertEquals(1, client.calls)
}

@Test
fun revokeSession_all() = runTest {
val client = MockClient()
val auth = Auth(client)
client.assert = { route: String, _: Map<String, Any?>, headers: Map<String, String>, _: Map<String, String?> ->
assertEquals("auth/logoutall", route)
val authorizationHeader = headers["Authorization"]
assertNotNull(authorizationHeader)
assertTrue(authorizationHeader!!.contains("refreshJwt"))
}
client.response = Unit
auth.revokeSessions(RevokeType.AllSessions, "refreshJwt")
assertEquals(1, client.calls)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ internal class MockClient : DescopeClient(DescopeConfig("p1")) {
response?.run { return this as T }
throw Exception("Test did not configure response")
}

override suspend fun <T> get(route: String, decoder: (String, List<HttpCookie>) -> T, headers: Map<String, String>, params: Map<String, String?>): T {
calls += 1
assert?.invoke(route, emptyMap(), headers, params)
error?.run { throw this }
response?.run { return this as T }
throw Exception("Test did not configure response")
}
}

internal fun SignUpDetails.validate(body: Map<String, Any?>) {
Expand Down

0 comments on commit 25dfbc7

Please sign in to comment.