Skip to content

Commit

Permalink
Add DescopeLogger and DescopeException
Browse files Browse the repository at this point in the history
  • Loading branch information
itaihanski committed Nov 16, 2023
1 parent e454f69 commit 7f566cf
Show file tree
Hide file tree
Showing 13 changed files with 383 additions and 76 deletions.
10 changes: 0 additions & 10 deletions descopesdk/src/main/java/com/descope/Descope.kt
Original file line number Diff line number Diff line change
Expand Up @@ -123,13 +123,3 @@ object Descope {
DescopeSdk(config = config)
}
}

// SDK information

/** The Descope SDK name */
val Descope.name: String
get() = "DescopeAndroid"

/** The Descope SDK version */
val Descope.version: String
get() = "0.9.3"
30 changes: 30 additions & 0 deletions descopesdk/src/main/java/com/descope/internal/http/ClientErrors.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.descope.internal.http

import com.descope.internal.others.toMap
import com.descope.internal.others.with
import com.descope.types.DescopeException
import org.json.JSONObject

internal fun parseServerError(response: String): DescopeException? = try {
val map = JSONObject(response).toMap()
val code = map["errorCode"] as? String ?: throw Exception("errorCode is required")
val desc = map["errorDescription"] as? String ?: "Descope server error"
val message = map["errorMessage"] as? String
DescopeException(code = code, desc = desc, message = message)
} catch (_: Exception) {
null
}

internal fun exceptionFromResponseCode(code: Int): DescopeException? {
val desc = when (code) {
in 200..299 -> null
400 -> "The request was invalid"
401 -> "The request was unauthorized"
403 -> "The request was forbidden"
404 -> "The resource was not found"
500, 503 -> "The server failed with status code $code"
in 500..<600 -> "The server was unreachable"
else -> "The server returned status code $code"
}
return desc?.run { DescopeException.httpError.with(desc = this) }
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
package com.descope.internal.http

import com.descope.Descope
import com.descope.sdk.DescopeConfig
import com.descope.sdk.DescopeSdk
import com.descope.types.DeliveryMethod
import com.descope.types.OAuthProvider
import com.descope.types.SignUpDetails
import com.descope.version

internal class DescopeClient(private val config: DescopeConfig) : HttpClient(config.baseUrl) {
internal class DescopeClient(internal val config: DescopeConfig) : HttpClient(config.baseUrl, config.logger) {

// OTP

Expand Down Expand Up @@ -261,7 +260,7 @@ internal class DescopeClient(private val config: DescopeConfig) : HttpClient(con
// OAuth

suspend fun oauthStart(provider: OAuthProvider, redirectUrl: String?): OAuthServerResponse = post(
route="auth/oauth/authorize",
route = "auth/oauth/authorize",
decoder = OAuthServerResponse::fromJson,
params = mapOf(
"provider" to provider.name,
Expand All @@ -270,7 +269,7 @@ internal class DescopeClient(private val config: DescopeConfig) : HttpClient(con
)

suspend fun oauthExchange(code: String): JwtServerResponse = post(
route="auth/oauth/exchange",
route = "auth/oauth/exchange",
decoder = JwtServerResponse::fromJson,
body = mapOf(
"code" to code,
Expand All @@ -280,7 +279,7 @@ internal class DescopeClient(private val config: DescopeConfig) : HttpClient(con
// SSO

suspend fun ssoStart(emailOrTenantId: String, redirectUrl: String?): SsoServerResponse = post(
route="auth/saml/authorize",
route = "auth/saml/authorize",
decoder = SsoServerResponse::fromJson,
params = mapOf(
"tenant" to emailOrTenantId,
Expand All @@ -289,7 +288,7 @@ internal class DescopeClient(private val config: DescopeConfig) : HttpClient(con
)

suspend fun ssoExchange(code: String): JwtServerResponse = post(
route="auth/saml/exchange",
route = "auth/saml/exchange",
decoder = JwtServerResponse::fromJson,
body = mapOf(
"code" to code,
Expand Down Expand Up @@ -334,9 +333,11 @@ internal class DescopeClient(private val config: DescopeConfig) : HttpClient(con
override val defaultHeaders: Map<String, String> = mapOf(
"Authorization" to "Bearer ${config.projectId}",
"x-descope-sdk-name" to "android",
"x-descope-sdk-version" to Descope.version,
"x-descope-sdk-version" to DescopeSdk.version,
)

override fun exceptionFromResponse(response: String): Exception? = parseServerError(response)

// Internal

private fun authorization(value: String) = mapOf(
Expand Down
30 changes: 27 additions & 3 deletions descopesdk/src/main/java/com/descope/internal/http/HttpClient.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
package com.descope.internal.http

import android.net.Uri
import com.descope.internal.others.with
import com.descope.sdk.DescopeLogger
import com.descope.sdk.DescopeLogger.Level.Debug
import com.descope.sdk.DescopeLogger.Level.Error
import com.descope.sdk.DescopeLogger.Level.Info
import com.descope.types.DescopeException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONObject
Expand All @@ -9,6 +15,7 @@ import javax.net.ssl.HttpsURLConnection

internal open class HttpClient(
private val baseUrl: String,
private val logger: DescopeLogger?,
) {

// Convenience functions
Expand All @@ -33,6 +40,8 @@ internal open class HttpClient(
open val basePath = "/"

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

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

// Internal

Expand All @@ -45,6 +54,7 @@ internal open class HttpClient(
params: Map<String, String?>,
): T = withContext(Dispatchers.IO) {
val url = makeUrl(route, params)
logger?.log(Info, "Starting network call", url)

val connection = url.openConnection() as HttpsURLConnection
try {
Expand All @@ -55,12 +65,13 @@ internal open class HttpClient(

// Send body if needed
body?.run {
logger?.log(Debug, "Sending request body", this)
connection.setRequestProperty("Content-Type", "application/json")
connection.doOutput = true
// Send the request
connection.outputStream.bufferedWriter().use {
it.write(JSONObject().apply {
filterValues { value -> value != null }
filterValues { value -> value != null }
.forEach { entry -> put(entry.key, entry.value) }
}.toString())
it.flush()
Expand All @@ -71,12 +82,25 @@ internal open class HttpClient(
val responseCode = connection.responseCode
if (responseCode == HttpsURLConnection.HTTP_OK) {
val response = connection.inputStream.bufferedReader().use { it.readText() }
logger?.log(Debug, "Received response body", response)
decoder(response)
} else {
// TODO: handle error responses and network errors
throw Exception("Network error")
val response = connection.errorStream.bufferedReader().use { it.readText() }
exceptionFromResponse(response)?.run {
logger?.log(Info, "Network call failed with server error", url, responseCode, this)
throw this
}
logger?.log(Info, "Network call failed with server error", url, responseCode)
throw exceptionFromResponseCode(responseCode) ?: Exception("Network error")
}
} catch (e: Exception) {
if (e !is DescopeException) {
logger?.log(Error, "Network call failed with network error", url, e)
throw DescopeException.networkError.with(cause = e)
}
throw e
} finally {
logger?.log(Info, "Network call finished", url)
connection.disconnect()
}
}
Expand Down
10 changes: 10 additions & 0 deletions descopesdk/src/main/java/com/descope/internal/others/Errors.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.descope.internal.others

import com.descope.types.DescopeException

internal fun DescopeException.with(desc: String? = null, message: String? = null, cause: Throwable? = null) = DescopeException(
code = code,
desc = desc ?: this.desc,
message = message ?: this.message,
cause = cause ?: this.cause,
)
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,19 @@ package com.descope.internal.routes
import com.descope.internal.http.DescopeClient
import com.descope.internal.http.EnchantedLinkServerResponse
import com.descope.sdk.DescopeEnchantedLink
import com.descope.sdk.DescopeLogger
import com.descope.sdk.DescopeLogger.Level.Error
import com.descope.sdk.DescopeLogger.Level.Info
import com.descope.types.AuthenticationResponse
import com.descope.types.DescopeException
import com.descope.types.EnchantedLinkResponse
import com.descope.types.Result
import com.descope.types.SignUpDetails
import kotlinx.coroutines.delay

private const val defaultPollDuration: Long = 2 /* mins */ * 60 /* secs */ * 1000 /* ms */
private const val DEFAULT_POLL_DURATION: Long = 2 /* mins */ * 60 /* secs */ * 1000 /* ms */

internal class EnchantedLink(private val client: DescopeClient) : DescopeEnchantedLink {
internal class EnchantedLink(override val client: DescopeClient) : Route, DescopeEnchantedLink {

override suspend fun signUp(loginId: String, details: SignUpDetails?, uri: String?): EnchantedLinkResponse =
client.enchantedLinkSignUp(loginId, details, uri).convert()
Expand Down Expand Up @@ -49,20 +53,27 @@ internal class EnchantedLink(private val client: DescopeClient) : DescopeEnchant
}

override suspend fun pollForSession(pendingRef: String, timeoutMilliseconds: Long?): AuthenticationResponse {
val pollingEndsAt = System.currentTimeMillis() + (timeoutMilliseconds ?: defaultPollDuration)
val pollingEndsAt = System.currentTimeMillis() + (timeoutMilliseconds ?: DEFAULT_POLL_DURATION)
log(Info, "Polling for enchanted link", timeoutMilliseconds ?: DEFAULT_POLL_DURATION)
// use repeat to ensure we always check at least once
while (true) {
// check for the session once, any errors not specifically handled
// below are intentionally let through to the calling code
try {
return checkForSession(pendingRef)
val response = checkForSession(pendingRef)
log(Info, "Enchanted link authentication succeeded")
return response
} catch (e: Exception) {
// sleep for a second before checking again
log(Info, "Waiting for enchanted link")
delay(1000L)
// if the timer's expired then we throw as specific
// client side error that can be handled appropriately
// by the calling code
if (pollingEndsAt - System.currentTimeMillis() <= 0L) throw Exception("Enchanted link polling expired") // TODO: Make the enchantedLinkExpired exception
if (pollingEndsAt - System.currentTimeMillis() <= 0L) {
log(Error, "Timed out while polling for enchanted link")
throw DescopeException.enchantedLinkExpired
}
}
}
}
Expand Down
16 changes: 11 additions & 5 deletions descopesdk/src/main/java/com/descope/internal/routes/Flow.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ import android.content.Context
import android.net.Uri
import androidx.browser.customtabs.CustomTabsIntent
import com.descope.internal.http.DescopeClient
import com.descope.internal.others.with
import com.descope.sdk.DescopeFlow
import com.descope.sdk.DescopeLogger
import com.descope.sdk.DescopeLogger.Level.Info
import com.descope.types.AuthenticationResponse
import com.descope.types.DescopeException
import com.descope.types.Result
import java.security.MessageDigest
import kotlin.io.encoding.Base64
Expand All @@ -14,8 +18,8 @@ import kotlin.random.Random

@OptIn(ExperimentalEncodingApi::class)
internal class Flow(
private val client: DescopeClient
) : DescopeFlow {
override val client: DescopeClient
) : Route, DescopeFlow {

override var currentRunner: DescopeFlow.Runner? = null
private set
Expand All @@ -34,6 +38,7 @@ internal class Flow(
private lateinit var codeVerifier: String

override fun start(context: Context) {
log(Info, "Starting flow authentication", flowUrl)
// create some random bytes
val randomBytes = ByteArray(32)
Random.nextBytes(randomBytes)
Expand Down Expand Up @@ -72,10 +77,11 @@ internal class Flow(

override suspend fun exchange(incomingUri: Uri): AuthenticationResponse {
// make sure start has been called
if (!this::codeVerifier.isInitialized) throw Exception("`start(context)` must be called before exchange")
if (!this::codeVerifier.isInitialized) throw DescopeException.flowFailed.with(desc = "`start(context)` must be called before exchange")

// get the `code` url param from the incoming uri and exchange it
val authorizationCode = incomingUri.getQueryParameter("code") ?: throw Exception("No code parameter on incoming URI")
val authorizationCode = incomingUri.getQueryParameter("code") ?: throw DescopeException.flowFailed.with(desc = "No code parameter on incoming URI")
log(Info, "Exchanging flow authorization code for session", authorizationCode)
if (currentRunner === this) currentRunner = null
return client.flowExchange(authorizationCode, codeVerifier).convert()
}
Expand All @@ -85,7 +91,7 @@ internal class Flow(
}

}

}

private fun launchUri(context: Context, uri: Uri) {
Expand Down
20 changes: 16 additions & 4 deletions descopesdk/src/main/java/com/descope/internal/routes/Shared.kt
Original file line number Diff line number Diff line change
@@ -1,19 +1,31 @@
package com.descope.internal.routes

import android.net.Uri
import com.descope.internal.http.DescopeClient
import com.descope.internal.http.JwtServerResponse
import com.descope.internal.http.MaskedAddressServerResponse
import com.descope.internal.http.UserResponse
import com.descope.internal.others.with
import com.descope.sdk.DescopeLogger
import com.descope.session.Token
import com.descope.types.AuthenticationResponse
import com.descope.types.DeliveryMethod
import com.descope.types.DescopeException
import com.descope.types.DescopeUser
import com.descope.types.RefreshResponse
import com.descope.types.Result
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch

internal interface Route {
val client: DescopeClient

fun log(level: DescopeLogger.Level, message: String, vararg values: Any) {
client.config.logger?.log(level, message, *values)
}
}

internal fun UserResponse.convert(): DescopeUser = DescopeUser(
userId = userId,
loginIds = loginIds,
Expand All @@ -28,8 +40,8 @@ internal fun UserResponse.convert(): DescopeUser = DescopeUser(
)

internal fun JwtServerResponse.convert(): AuthenticationResponse {
val refreshJwt = refreshJwt ?: throw Exception("Missing refresh JWT") // TODO replace with DescopeError
val user = user ?: throw Exception("Missing user details") // TODO replace with DescopeError
val refreshJwt = refreshJwt ?: throw DescopeException.decodeError.with(message = "Missing refresh JWT")
val user = user ?: throw DescopeException.decodeError.with(message = "Missing user details")
return AuthenticationResponse(
refreshToken = Token(refreshJwt),
sessionToken = Token(sessionJwt),
Expand All @@ -44,8 +56,8 @@ internal fun JwtServerResponse.toRefreshResponse(): RefreshResponse = RefreshRes
)

internal fun MaskedAddressServerResponse.convert(method: DeliveryMethod) = when (method) {
DeliveryMethod.Email -> maskedEmail ?: throw Exception("masked email not received")
DeliveryMethod.Sms, DeliveryMethod.Whatsapp -> maskedPhone ?: throw Exception("masked phone not received")
DeliveryMethod.Email -> maskedEmail ?: throw DescopeException.decodeError.with(message = "masked email not received")
DeliveryMethod.Sms, DeliveryMethod.Whatsapp -> maskedPhone ?: throw DescopeException.decodeError.with(message = "masked phone not received")
}

@Suppress("OPT_IN_USAGE")
Expand Down
Loading

0 comments on commit 7f566cf

Please sign in to comment.