Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ import com.google.gson.Gson
import okhttp3.HttpUrl.Companion.toHttpUrl

/**
*
*
* API client for Auth0's embedded authentication API.
*
* ```
Expand All @@ -34,14 +32,15 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
*/
public class EmbeddedAuthClient(private val auth0: Auth0) {

/** Used for both `/e/authorize` and the `/oauth/token` exchange. */
private val factory: RequestFactory<EmbeddedAuthException> =
RequestFactory(auth0.networkingClient, embeddedAuthErrorAdapter())

private val gson: Gson = GsonProvider.gson

private val clientId: String
get() = auth0.clientId

@Volatile
private var transactionState: EmbeddedAuthState? = null

Expand All @@ -50,8 +49,8 @@ public class EmbeddedAuthClient(private val auth0: Auth0) {
*
* This call never resolves successfully: the server always answers with a continuation, so the
* request completes through [EmbeddedAuthException]. Inspect
* [EmbeddedAuthException.isInsufficientAuthorization] and [EmbeddedAuthException.nextActions] to
* learn which step to call next. A terminal error is reported on the same failure channel.
* [EmbeddedAuthError.InsufficientAuthorization.nextActions] on [EmbeddedAuthException.error]
* to learn which step to call next. A terminal error is reported on the same failure channel.
*
* @param connection name of the connection to authenticate against.
* @param scope space-separated scopes to request. Must include `openid` for the terminal token
Expand Down Expand Up @@ -82,7 +81,7 @@ public class EmbeddedAuthClient(private val auth0: Auth0) {
* Continues the flow by submitting an identifier of the given [type].
*
* This call never resolves successfully; it completes through [EmbeddedAuthException] whose
* [EmbeddedAuthException.nextActions] carry the next step to call.
* [EmbeddedAuthError.InsufficientAuthorization.nextActions] carry the next step to call.
*/
public fun identify(
identifier: String,
Expand All @@ -96,7 +95,7 @@ public class EmbeddedAuthClient(private val auth0: Auth0) {
* Continues the flow by requesting an email challenge for the authenticator at [index].
*
* This call never resolves successfully; it completes through [EmbeddedAuthException] whose
* [EmbeddedAuthException.nextActions] carry the next step to call.
* [EmbeddedAuthError.InsufficientAuthorization.nextActions] carry the next step to call.
*/
@JvmOverloads
public fun challengeEmail(index: Int = 0): Request<Void?, EmbeddedAuthException> =
Expand All @@ -108,8 +107,8 @@ public class EmbeddedAuthClient(private val auth0: Auth0) {
* Verifies a one-time [code] of the given [type]. This is the terminal step of the flow.
*
* On success, it yields the [Credentials]. If the server requires further steps the request
* completes through [EmbeddedAuthException] instead, with the next step on
* [EmbeddedAuthException.nextActions].
* completes through [EmbeddedAuthException] instead, with
* [EmbeddedAuthError.InsufficientAuthorization.nextActions] on [EmbeddedAuthException.error].
*/
@JvmOverloads
public fun verifyOtp(
Expand Down Expand Up @@ -150,7 +149,8 @@ public class EmbeddedAuthClient(private val auth0: Auth0) {
private fun <T> noActiveSession(): Request<T, EmbeddedAuthException> = FailedRequest<T>(
EmbeddedAuthException(
NO_ACTIVE_SESSION_ERROR,
"No embedded authentication flow is in progress. Call authorize() first."
"No embedded authentication flow is in progress. Call authorize() first.",
error = EmbeddedAuthError.NoActiveSession
)
)

Expand Down Expand Up @@ -191,12 +191,14 @@ public class EmbeddedAuthClient(private val auth0: Auth0) {
}

private fun updateSessionFromFailure(error: EmbeddedAuthException) {
when {
error.isInsufficientAuthorization && error.authSession != null ->
transactionState = EmbeddedAuthState(error.authSession)
if (error.statusCode in 500..599) return

error.isAccessDenied || error.isTooManyAttempts || error.isTooManyLogins ->
transactionState = null
when (error.error) {
is EmbeddedAuthError.InsufficientAuthorization ->
error.authSession?.let { transactionState = EmbeddedAuthState(it) }

is EmbeddedAuthError.Network -> Unit
else -> transactionState = null
Comment thread
pmathew92 marked this conversation as resolved.
}
}

Expand All @@ -214,6 +216,8 @@ public class EmbeddedAuthClient(private val auth0: Auth0) {
private const val AUTH_SESSION_KEY = "auth_session"
private const val ACTION_KEY = "action"
private const val EMAIL_KEY = "email"

@Suppress("unused")
Comment thread
pmathew92 marked this conversation as resolved.
private const val PHONE_KEY = "phone"
private const val OTP_KEY = "otp"
private const val INDEX_KEY = "index"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.auth0.android.embedded

import com.auth0.android.embedded.authorize.NextAction

/**
* The typed classification of an [EmbeddedAuthException].
*
*/
public sealed interface EmbeddedAuthError {
Comment thread
NandanPrabhu marked this conversation as resolved.

/**
* The flow is not complete and can continue: act on one of [nextActions].
* Corresponds to `code == "insufficient_authorization"`. [reason] reports why the
* previous step's input was rejected, when the server says so.
*/
public data class InsufficientAuthorization(
Comment thread
pmathew92 marked this conversation as resolved.
public val nextActions: List<NextAction>,
public val reason: Reason
) : EmbeddedAuthError {

public enum class Reason {
/** a plain step-forward continuation. */

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"invalid_identifier_or_code" "invalid_code" "authorization_pending" "slow_down" "invalid_identifier_or_password" enums that can come under insufficient authorization. do we need add all these?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not now. We can add when those scenarios come in the next milestones

NONE,

/** The submitted one-time code was invalid. */
INVALID_CODE,

/** The identifier or the code was invalid; the server does not say which. */
INVALID_IDENTIFIER_OR_CODE
}
}

/** Terminal: too many wrong OTP submissions. Restart the flow. */
public data object TooManyWrongOtpAttempts : EmbeddedAuthError

/** Terminal: the OTP challenge expired before it was verified. Restart the flow. */
public data object ChallengeExpired : EmbeddedAuthError

/**
* Terminal: access denied for a reason not modelled as its own case.
* Read [EmbeddedAuthException.description] for specifics.
*/
public data object AccessDenied : EmbeddedAuthError

/** Terminal: rate-limited by attack protection . */
public data object TooManyAttempts : EmbeddedAuthError

/** Terminal: rate-limited by same-user-login protection. */
public data object TooManyLogins : EmbeddedAuthError

/**
* Terminal: the grant being redeemed is no longer valid. Start a new flow with
* [EmbeddedAuthClient.authorize].
*/
public data object SessionExpired : EmbeddedAuthError

/**
* Terminal: the request was malformed (`code == "invalid_request"`).Read [EmbeddedAuthException.description] for
* specifics, fix the request, and start a new flow with [EmbeddedAuthClient.authorize].
*/
public data object InvalidRequest : EmbeddedAuthError

/** The request never reached the server (`cause` is a network error). Retry the step. */
public data object Network : EmbeddedAuthError

/** Client-side guard: no flow is in progress. Call [EmbeddedAuthClient.authorize] first. */
public data object NoActiveSession : EmbeddedAuthError

/**
* An error this version of the SDK does not classify. Read [EmbeddedAuthException.code] / [EmbeddedAuthException.description] /
* [EmbeddedAuthException.statusCode] to diagnose.
*/
public data object Unknown : EmbeddedAuthError
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package com.auth0.android.embedded

import com.auth0.android.Auth0Exception
import com.auth0.android.NetworkErrorException
import com.auth0.android.embedded.authorize.NextAction

/**
* Represents an error raised by Auth0's embedded authentication API.
*
* Branch on [error] with an exhaustive `when` to handle each case; read [code], [description],
* and [statusCode] for diagnostics or to present specific messages.
*/
public class EmbeddedAuthException internal constructor(

Expand All @@ -16,57 +17,10 @@ public class EmbeddedAuthException internal constructor(
/** HTTP status code of the response, or `0` when no response was received. */
public val statusCode: Int = 0,

/** When the attempt is not finished, the menu of actions the server will accept next. */
public val nextActions: List<NextAction> = emptyList(),
/** The typed classification of this error. */
public val error: EmbeddedAuthError,
Comment thread
pmathew92 marked this conversation as resolved.

internal val authSession: String? = null,

cause: Throwable? = null
) : Auth0Exception(description, cause) {

public val isNetworkError: Boolean
get() = cause is NetworkErrorException

/** The attempt isn't done: the server returned a continuation. Read [nextActions] for what to call next. */
public val isInsufficientAuthorization: Boolean
get() = code == INSUFFICIENT_AUTHORIZATION

/** Recoverable: the submitted code or identifier was wrong — let the user retry with [nextActions]. */
public val isInvalidCode: Boolean
Comment thread
pmathew92 marked this conversation as resolved.
get() = code == INSUFFICIENT_AUTHORIZATION &&
description in INVALID_CODE_DESCRIPTIONS

/** Terminal: the attempt was denied and must not be retried. */
public val isAccessDenied: Boolean
get() = code == ACCESS_DENIED

/** Terminal: the challenge was denied after too many wrong one-time-code attempts. */
public val isTooManyWrongOtpAttempts: Boolean
get() = code == ACCESS_DENIED && description == TOO_MANY_WRONG_OTP_ATTEMPTS

/** Terminal: the challenge expired before it was verified. */
public val isChallengeExpired: Boolean
get() = code == ACCESS_DENIED && description == CHALLENGE_EXPIRED

/** Terminal: too many failed verification attempts. */
public val isTooManyAttempts: Boolean
get() = statusCode == TOO_MANY_REQUESTS_STATUS &&
code == TOO_MANY_REQUESTS && description == TOO_MANY_ATTEMPTS

/** Terminal: too many login attempts. */
public val isTooManyLogins: Boolean
get() = statusCode == TOO_MANY_REQUESTS_STATUS &&
code == TOO_MANY_REQUESTS && description == TOO_MANY_LOGINS

private companion object {
private const val INSUFFICIENT_AUTHORIZATION = "insufficient_authorization"
private const val ACCESS_DENIED = "access_denied"
private const val TOO_MANY_WRONG_OTP_ATTEMPTS = "too_many_wrong_otp_attempts"
private const val CHALLENGE_EXPIRED = "challenge_expired"
private val INVALID_CODE_DESCRIPTIONS = setOf("invalid_code", "invalid_identifier_or_code")
private const val TOO_MANY_REQUESTS = "too_many_requests"
private const val TOO_MANY_ATTEMPTS = "too_many_attempts"
private const val TOO_MANY_LOGINS = "too_many_logins"
private const val TOO_MANY_REQUESTS_STATUS = 429
}
}
) : Auth0Exception(description, cause)
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.auth0.android.embedded.authorize

import com.auth0.android.Auth0Exception
import com.auth0.android.NetworkErrorException
import com.auth0.android.embedded.EmbeddedAuthError
import com.auth0.android.embedded.EmbeddedAuthException
import com.auth0.android.request.ErrorAdapter
import com.auth0.android.request.JsonAdapter
Expand All @@ -20,11 +21,26 @@ private const val AUTH_SESSION_KEY = "auth_session"
private const val DEFAULT_DESCRIPTION =
"An error occurred when trying to authenticate with the server."

private const val INSUFFICIENT_AUTHORIZATION = "insufficient_authorization"
private const val ACCESS_DENIED = "access_denied"
private const val TOO_MANY_WRONG_OTP_ATTEMPTS = "too_many_wrong_otp_attempts"
private const val CHALLENGE_EXPIRED = "challenge_expired"
private const val TOO_MANY_REQUESTS = "too_many_requests"
private const val TOO_MANY_ATTEMPTS = "too_many_attempts"
private const val TOO_MANY_LOGINS = "too_many_logins"
private const val INVALID_CODE = "invalid_code"
private const val INVALID_IDENTIFIER_OR_CODE = "invalid_identifier_or_code"
private const val INVALID_GRANT = "invalid_grant"
private const val INVALID_REQUEST = "invalid_request"
private const val TOO_MANY_REQUESTS_STATUS = 429

/** Parses the `200` body of `/e/authorize` into the code to exchange for tokens. */
internal fun authorizeCodeAdapter(gson: Gson): JsonAdapter<AuthorizeCode> =
GsonAdapter(AuthorizeCode::class.java, gson)

/** Translates every embedded-authentication error response into an [EmbeddedAuthException]. */
/**
* Translates every embedded-authentication error response into an [EmbeddedAuthException].
*/
internal fun embeddedAuthErrorAdapter(): ErrorAdapter<EmbeddedAuthException> {
val mapAdapter = forMap(GsonProvider.gson)
return object : ErrorAdapter<EmbeddedAuthException> {
Expand All @@ -37,11 +53,13 @@ internal fun embeddedAuthErrorAdapter(): ErrorAdapter<EmbeddedAuthException> {
return if (bodyText.isBlank()) EmbeddedAuthException(
Auth0Exception.EMPTY_BODY_ERROR,
Auth0Exception.EMPTY_RESPONSE_BODY_DESCRIPTION,
statusCode
statusCode,
error = EmbeddedAuthError.Unknown
) else EmbeddedAuthException(
Auth0Exception.NON_JSON_ERROR,
bodyText,
statusCode
statusCode,
error = EmbeddedAuthError.Unknown
)
}

Expand All @@ -51,25 +69,63 @@ internal fun embeddedAuthErrorAdapter(): ErrorAdapter<EmbeddedAuthException> {
reader: Reader
): EmbeddedAuthException {
val values = mapAdapter.fromJson(reader)
val code = values[ERROR_KEY] as? String ?: Auth0Exception.UNKNOWN_ERROR
val description = values[ERROR_DESCRIPTION_KEY] as? String ?: DEFAULT_DESCRIPTION
val authSession = values[AUTH_SESSION_KEY] as? String
@Suppress("UNCHECKED_CAST")
val nextRaw = values[NEXT_KEY] as? List<Map<String, Any>> ?: emptyList()

val error: EmbeddedAuthError = when {
code == INSUFFICIENT_AUTHORIZATION ->
EmbeddedAuthError.InsufficientAuthorization(
nextRaw.toNextActions(),
insufficientAuthorizationReason(description)
)
code == ACCESS_DENIED && description == TOO_MANY_WRONG_OTP_ATTEMPTS ->
EmbeddedAuthError.TooManyWrongOtpAttempts
code == ACCESS_DENIED && description == CHALLENGE_EXPIRED ->
EmbeddedAuthError.ChallengeExpired
code == ACCESS_DENIED ->
EmbeddedAuthError.AccessDenied
statusCode == TOO_MANY_REQUESTS_STATUS && code == TOO_MANY_REQUESTS && description == TOO_MANY_ATTEMPTS ->
EmbeddedAuthError.TooManyAttempts
statusCode == TOO_MANY_REQUESTS_STATUS && code == TOO_MANY_REQUESTS && description == TOO_MANY_LOGINS ->
EmbeddedAuthError.TooManyLogins
code == INVALID_GRANT ->
EmbeddedAuthError.SessionExpired
code == INVALID_REQUEST ->
EmbeddedAuthError.InvalidRequest
else ->
EmbeddedAuthError.Unknown
}

return EmbeddedAuthException(
values[ERROR_KEY] as? String ?: Auth0Exception.UNKNOWN_ERROR,
values[ERROR_DESCRIPTION_KEY] as? String ?: DEFAULT_DESCRIPTION,
code,
description,
statusCode,
nextActions = nextRaw.toNextActions(),
authSession = values[AUTH_SESSION_KEY] as? String
error = error,
authSession = authSession
)
}

fun insufficientAuthorizationReason(description: String):
EmbeddedAuthError.InsufficientAuthorization.Reason = when (description) {
INVALID_CODE -> EmbeddedAuthError.InsufficientAuthorization.Reason.INVALID_CODE
INVALID_IDENTIFIER_OR_CODE ->
EmbeddedAuthError.InsufficientAuthorization.Reason.INVALID_IDENTIFIER_OR_CODE
else -> EmbeddedAuthError.InsufficientAuthorization.Reason.NONE
}

override fun fromException(cause: Throwable): EmbeddedAuthException {
return if (isNetworkError(cause)) EmbeddedAuthException(
Auth0Exception.UNKNOWN_ERROR,
"Failed to execute the network request",
error = EmbeddedAuthError.Network,
cause = NetworkErrorException(cause)
) else EmbeddedAuthException(
Auth0Exception.UNKNOWN_ERROR,
DEFAULT_DESCRIPTION,
error = EmbeddedAuthError.Unknown,
cause = Auth0Exception(DEFAULT_DESCRIPTION, cause)
)
}
Expand Down
Loading
Loading