Skip to content

Commit

Permalink
Add support for tokens via cookies (#48)
Browse files Browse the repository at this point in the history
  • Loading branch information
itaihanski authored Nov 22, 2023
1 parent 7728f54 commit cad9fec
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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) {

Expand Down Expand Up @@ -357,4 +358,4 @@ private fun DeliveryMethod.route() = this.name.lowercase()

// Utilities

private val emptyResponse: (String) -> Unit = {}
private val emptyResponse: (String, List<HttpCookie>) -> Unit = {_, _ ->}
24 changes: 19 additions & 5 deletions descopesdk/src/main/java/com/descope/internal/http/HttpClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -22,14 +23,14 @@ internal open class HttpClient(

suspend fun <T> get(
route: String,
decoder: (String) -> T,
decoder: (String, List<HttpCookie>) -> T,
headers: Map<String, String> = emptyMap(),
params: Map<String, String?> = emptyMap(),
) = call(route, "GET", decoder, headers = headers, params = params)

suspend fun <T> post(
route: String,
decoder: (String) -> T,
decoder: (String, List<HttpCookie>) -> T,
body: Map<String, Any?> = emptyMap(),
headers: Map<String, String> = emptyMap(),
params: Map<String, String?> = emptyMap(),
Expand All @@ -40,15 +41,15 @@ internal open class HttpClient(
open val basePath = "/"

open val defaultHeaders: Map<String, String> = emptyMap()

open fun exceptionFromResponse(response: String): Exception? = null

// Internal

private suspend fun <T> call(
route: String,
method: String,
decoder: (String) -> T,
decoder: (String, List<HttpCookie>) -> T,
body: Map<String, Any?>? = null,
headers: Map<String, String>,
params: Map<String, String?>,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -117,3 +118,16 @@ internal open class HttpClient(
return URL(urlString)
}
}

private val HttpsURLConnection.cookies: List<HttpCookie>
get() {
val cookies = mutableListOf<HttpCookie>()
headerFields.keys.find { it?.lowercase() == "set-cookie" }?.let { key ->
headerFields[key]?.forEach {
try {
cookies.addAll(HttpCookie.parse(it))
} catch (ignored: Exception) {}
}
}
return cookies.toList()
}
44 changes: 33 additions & 11 deletions descopesdk/src/main/java/com/descope/internal/http/Responses.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<HttpCookie>) = 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"),
)
Expand All @@ -37,7 +52,8 @@ internal data class UserResponse(
val customAttributes: Map<String, Any>,
) {
companion object {
fun fromJson(json: String) = fromJson(JSONObject(json))
@Suppress("UNUSED_PARAMETER")
fun fromJson(json: String, cookies: List<HttpCookie>) = fromJson(JSONObject(json))

fun fromJson(json: JSONObject) = json.run {
UserResponse(
Expand All @@ -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<HttpCookie>) = JSONObject(json).run {
MaskedAddressServerResponse(
maskedEmail = stringOrEmptyAsNull("maskedEmail"),
maskedPhone = stringOrEmptyAsNull("maskedPhone"),
Expand All @@ -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<HttpCookie>) = JSONObject(json).run {
PasswordPolicyServerResponse(
minLength = getInt("minLength"),
lowercase = optBoolean("lowercase"),
Expand All @@ -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<HttpCookie>) = JSONObject(json).run {
EnchantedLinkServerResponse(
linkId = getString("linkId"),
pendingRef = getString("pendingRef"),
Expand All @@ -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<HttpCookie>) = JSONObject(json).run {
TotpServerResponse(
provisioningUrl = getString("provisioningUrl"),
image = getString("image").toByteArray(),
Expand Down Expand Up @@ -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<HttpCookie>) = JSONObject(json).run {
OAuthServerResponse(
url = getString("url")
)
Expand All @@ -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<HttpCookie>) = JSONObject(json).run {
SsoServerResponse(
url = getString("url")
)
Expand Down
12 changes: 8 additions & 4 deletions descopesdk/src/main/java/com/descope/internal/routes/Shared.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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")
Expand Down

0 comments on commit cad9fec

Please sign in to comment.