diff --git a/descopesdk/src/main/java/com/descope/internal/http/DescopeClient.kt b/descopesdk/src/main/java/com/descope/internal/http/DescopeClient.kt index 96c502f8..779ffe90 100644 --- a/descopesdk/src/main/java/com/descope/internal/http/DescopeClient.kt +++ b/descopesdk/src/main/java/com/descope/internal/http/DescopeClient.kt @@ -5,6 +5,7 @@ import com.descope.sdk.DescopeSdk import com.descope.types.DeliveryMethod import com.descope.types.OAuthProvider import com.descope.types.SignUpDetails +import java.net.HttpCookie internal class DescopeClient(internal val config: DescopeConfig) : HttpClient(config.baseUrl, config.logger) { @@ -357,4 +358,4 @@ private fun DeliveryMethod.route() = this.name.lowercase() // Utilities -private val emptyResponse: (String) -> Unit = {} +private val emptyResponse: (String, List) -> Unit = {_, _ ->} diff --git a/descopesdk/src/main/java/com/descope/internal/http/HttpClient.kt b/descopesdk/src/main/java/com/descope/internal/http/HttpClient.kt index 4721a4a2..655ee3ee 100644 --- a/descopesdk/src/main/java/com/descope/internal/http/HttpClient.kt +++ b/descopesdk/src/main/java/com/descope/internal/http/HttpClient.kt @@ -10,6 +10,7 @@ import com.descope.types.DescopeException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.json.JSONObject +import java.net.HttpCookie import java.net.URL import javax.net.ssl.HttpsURLConnection @@ -22,14 +23,14 @@ internal open class HttpClient( suspend fun get( route: String, - decoder: (String) -> T, + decoder: (String, List) -> T, headers: Map = emptyMap(), params: Map = emptyMap(), ) = call(route, "GET", decoder, headers = headers, params = params) suspend fun post( route: String, - decoder: (String) -> T, + decoder: (String, List) -> T, body: Map = emptyMap(), headers: Map = emptyMap(), params: Map = emptyMap(), @@ -40,7 +41,7 @@ internal open class HttpClient( open val basePath = "/" open val defaultHeaders: Map = emptyMap() - + open fun exceptionFromResponse(response: String): Exception? = null // Internal @@ -48,7 +49,7 @@ internal open class HttpClient( private suspend fun call( route: String, method: String, - decoder: (String) -> T, + decoder: (String, List) -> T, body: Map? = null, headers: Map, params: Map, @@ -83,7 +84,7 @@ internal open class HttpClient( if (responseCode == HttpsURLConnection.HTTP_OK) { val response = connection.inputStream.bufferedReader().use { it.readText() } logger?.log(Debug, "Received response body", response) - decoder(response) + decoder(response, connection.cookies) } else { val response = connection.errorStream.bufferedReader().use { it.readText() } exceptionFromResponse(response)?.run { @@ -117,3 +118,16 @@ internal open class HttpClient( return URL(urlString) } } + +private val HttpsURLConnection.cookies: List + get() { + val cookies = mutableListOf() + headerFields.keys.find { it?.lowercase() == "set-cookie" }?.let { key -> + headerFields[key]?.forEach { + try { + cookies.addAll(HttpCookie.parse(it)) + } catch (ignored: Exception) {} + } + } + return cookies.toList() + } diff --git a/descopesdk/src/main/java/com/descope/internal/http/Responses.kt b/descopesdk/src/main/java/com/descope/internal/http/Responses.kt index c4876056..10212111 100644 --- a/descopesdk/src/main/java/com/descope/internal/http/Responses.kt +++ b/descopesdk/src/main/java/com/descope/internal/http/Responses.kt @@ -5,18 +5,33 @@ import com.descope.internal.others.secToMs import com.descope.internal.others.stringOrEmptyAsNull import com.descope.internal.others.toStringList import org.json.JSONObject +import java.net.HttpCookie + +private const val SESSION_COOKIE_NAME = "DS" +private const val REFRESH_COOKIE_NAME = "DSR" internal data class JwtServerResponse( - val sessionJwt: String, + val sessionJwt: String?, val refreshJwt: String?, val user: UserResponse?, val firstSeen: Boolean, ) { companion object { - fun fromJson(json: String) = JSONObject(json).run { + fun fromJson(json: String, cookies: List) = JSONObject(json).run { + var sessionJwt: String? = null + var refreshJwt: String? = null + + // check cookies for tokens + cookies.forEach { + when (it.name) { + SESSION_COOKIE_NAME -> sessionJwt = it.value + REFRESH_COOKIE_NAME -> refreshJwt = it.value + } + } + JwtServerResponse( - sessionJwt = getString("sessionJwt"), - refreshJwt = stringOrEmptyAsNull("refreshJwt"), + sessionJwt = stringOrEmptyAsNull("sessionJwt") ?: sessionJwt, + refreshJwt = stringOrEmptyAsNull("refreshJwt") ?: refreshJwt, user = optJSONObject("user")?.run { UserResponse.fromJson(this) }, firstSeen = optBoolean("firstSeen"), ) @@ -37,7 +52,8 @@ internal data class UserResponse( val customAttributes: Map, ) { companion object { - fun fromJson(json: String) = fromJson(JSONObject(json)) + @Suppress("UNUSED_PARAMETER") + fun fromJson(json: String, cookies: List) = fromJson(JSONObject(json)) fun fromJson(json: JSONObject) = json.run { UserResponse( @@ -61,7 +77,8 @@ internal data class MaskedAddressServerResponse( val maskedPhone: String? = null, ) { companion object { - fun fromJson(json: String) = JSONObject(json).run { + @Suppress("UNUSED_PARAMETER") + fun fromJson(json: String, cookies: List) = JSONObject(json).run { MaskedAddressServerResponse( maskedEmail = stringOrEmptyAsNull("maskedEmail"), maskedPhone = stringOrEmptyAsNull("maskedPhone"), @@ -78,7 +95,8 @@ internal data class PasswordPolicyServerResponse( val nonAlphanumeric: Boolean, ) { companion object { - fun fromJson(json: String) = JSONObject(json).run { + @Suppress("UNUSED_PARAMETER") + fun fromJson(json: String, cookies: List) = JSONObject(json).run { PasswordPolicyServerResponse( minLength = getInt("minLength"), lowercase = optBoolean("lowercase"), @@ -96,7 +114,8 @@ internal data class EnchantedLinkServerResponse( val maskedEmail: String, ) { companion object { - fun fromJson(json: String) = JSONObject(json).run { + @Suppress("UNUSED_PARAMETER") + fun fromJson(json: String, cookies: List) = JSONObject(json).run { EnchantedLinkServerResponse( linkId = getString("linkId"), pendingRef = getString("pendingRef"), @@ -112,7 +131,8 @@ internal data class TotpServerResponse( val key: String, ) { companion object { - fun fromJson(json: String) = JSONObject(json).run { + @Suppress("UNUSED_PARAMETER") + fun fromJson(json: String, cookies: List) = JSONObject(json).run { TotpServerResponse( provisioningUrl = getString("provisioningUrl"), image = getString("image").toByteArray(), @@ -146,7 +166,8 @@ internal data class OAuthServerResponse( val url: String, ) { companion object { - fun fromJson(json: String) = JSONObject(json).run { + @Suppress("UNUSED_PARAMETER") + fun fromJson(json: String, cookies: List) = JSONObject(json).run { OAuthServerResponse( url = getString("url") ) @@ -158,7 +179,8 @@ internal data class SsoServerResponse( val url: String, ) { companion object { - fun fromJson(json: String) = JSONObject(json).run { + @Suppress("UNUSED_PARAMETER") + fun fromJson(json: String, cookies: List) = JSONObject(json).run { SsoServerResponse( url = getString("url") ) diff --git a/descopesdk/src/main/java/com/descope/internal/routes/Shared.kt b/descopesdk/src/main/java/com/descope/internal/routes/Shared.kt index a887373b..39083217 100644 --- a/descopesdk/src/main/java/com/descope/internal/routes/Shared.kt +++ b/descopesdk/src/main/java/com/descope/internal/routes/Shared.kt @@ -40,6 +40,7 @@ internal fun UserResponse.convert(): DescopeUser = DescopeUser( ) internal fun JwtServerResponse.convert(): AuthenticationResponse { + val sessionJwt = sessionJwt ?: throw DescopeException.decodeError.with(message = "Missing session JWT") val refreshJwt = refreshJwt ?: throw DescopeException.decodeError.with(message = "Missing refresh JWT") val user = user ?: throw DescopeException.decodeError.with(message = "Missing user details") return AuthenticationResponse( @@ -50,10 +51,13 @@ internal fun JwtServerResponse.convert(): AuthenticationResponse { ) } -internal fun JwtServerResponse.toRefreshResponse(): RefreshResponse = RefreshResponse( - refreshToken = refreshJwt?.run { Token(this) }, - sessionToken = Token(sessionJwt), -) +internal fun JwtServerResponse.toRefreshResponse(): RefreshResponse { + val sessionJwt = sessionJwt ?: throw DescopeException.decodeError.with(message = "Missing session JWT") + return RefreshResponse( + refreshToken = refreshJwt?.run { Token(this) }, + sessionToken = Token(sessionJwt), + ) +} internal fun MaskedAddressServerResponse.convert(method: DeliveryMethod) = when (method) { DeliveryMethod.Email -> maskedEmail ?: throw DescopeException.decodeError.with(message = "masked email not received")