Skip to content
Open
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 @@ -3,13 +3,15 @@ package com.auth0.android.embedded
import com.auth0.android.Auth0
import com.auth0.android.embedded.authorize.AdvancingRequest
import com.auth0.android.embedded.authorize.AuthorizeCode
import com.auth0.android.embedded.authorize.CredentialRequirement
import com.auth0.android.embedded.authorize.EmbeddedAuthState
import com.auth0.android.embedded.authorize.EmbeddedCapability
import com.auth0.android.embedded.authorize.FailedRequest
import com.auth0.android.embedded.authorize.IdentifierType
import com.auth0.android.embedded.authorize.NextAction
import com.auth0.android.embedded.authorize.OtpType
import com.auth0.android.embedded.authorize.PhoneDeliveryMethod
import com.auth0.android.embedded.authorize.SignupPassword
import com.auth0.android.embedded.authorize.StepRequest
import com.auth0.android.embedded.authorize.authorizeCodeAdapter
import com.auth0.android.embedded.authorize.embeddedAuthErrorAdapter
Expand Down Expand Up @@ -251,14 +253,30 @@ public class EmbeddedAuthClient(private val auth0: Auth0) {
* Creates the new user's account and completes the flow. Call it when a
* [NextAction.ConfirmSignup] is offered.
*
* Pass a [password] when [NextAction.ConfirmSignup.password] is not `null`; it must be passed when
* [SignupPassword.required] is `true`. An empty [password] counts as none. It is sent as it is, and the
* SDK doesn't check it against [SignupPassword.complexity], which is advisory.
*
* On success, it yields the [Credentials]. If the server requires further steps the request
* completes through [EmbeddedAuthException] instead, with
* [EmbeddedAuthError.InsufficientAuthorization.nextActions] on [EmbeddedAuthException.error].
*
* The server decides whether the passed password is right or not. A missing
* required password completes with [EmbeddedAuthError.InsufficientAuthorization.Reason.PASSWORD_REQUIRED],
* a password the connection doesn't collect with
* [EmbeddedAuthError.InsufficientAuthorization.Reason.PASSWORD_NOT_ACCEPTED], and a password the
* connection's policy rejects with [EmbeddedAuthError.InsufficientAuthorization.Reason.INVALID_PASSWORD].
* Each keeps the flow open, so call again with the right input.
* If the account can't be created, the request completes with the terminal
* [EmbeddedAuthError.InvalidRequest]; start a new flow with [authorize].
*/
public fun confirmSignup(): Request<Credentials, EmbeddedAuthException> =
terminalStep(EmbeddedCapability.CONFIRM_SIGNUP)
@JvmOverloads
public fun confirmSignup(password: String? = null): Request<Credentials, EmbeddedAuthException> {
val sentPassword = password?.takeIf { it.isNotEmpty() }
return terminalStep(EmbeddedCapability.CONFIRM_SIGNUP) {
if (sentPassword != null) addParameter(PASSWORD_KEY, sentPassword)
}
}

private fun continueStep(
action: EmbeddedCapability,
Expand Down Expand Up @@ -338,18 +356,49 @@ public class EmbeddedAuthClient(private val auth0: Auth0) {
.toString()
}

private fun updateSessionFromFailure(error: EmbeddedAuthException) {
if (error.statusCode in 500..599) return

when (error.error) {
is EmbeddedAuthError.InsufficientAuthorization ->
error.authSession?.let { transactionState = EmbeddedAuthState(it) }

is EmbeddedAuthError.Network -> Unit
else -> transactionState = null
private fun updateSessionFromFailure(error: EmbeddedAuthException): EmbeddedAuthException {
if (error.statusCode in 500..599) return error

return when (val failure = error.error) {
is EmbeddedAuthError.InsufficientAuthorization -> {
val session = error.authSession ?: return error
val announced = failure.nextActions.filterIsInstance<NextAction.Signup>().firstOrNull()?.passwordRequirement
val requirement = when {
failure.reason == EmbeddedAuthError.InsufficientAuthorization.Reason.PASSWORD_REQUIRED ->
CredentialRequirement.REQUIRED

else -> announced ?: transactionState?.passwordRequirement
}
val actions = failure.nextActions.map { action ->
if (action is NextAction.ConfirmSignup) action.withRequired(requirement == CredentialRequirement.REQUIRED) else action
}
transactionState = EmbeddedAuthState(session, requirement)
if (actions == failure.nextActions) error else error.withNextActions(actions, failure.reason)
}

is EmbeddedAuthError.Network -> error
else -> {
transactionState = null
error
}
}
}

private fun NextAction.ConfirmSignup.withRequired(required: Boolean): NextAction.ConfirmSignup =
NextAction.ConfirmSignup(password?.let { SignupPassword(required, it.complexity) })

private fun EmbeddedAuthException.withNextActions(
nextActions: List<NextAction>,
reason: EmbeddedAuthError.InsufficientAuthorization.Reason
): EmbeddedAuthException = EmbeddedAuthException(
code,
description,
statusCode,
error = EmbeddedAuthError.InsufficientAuthorization(nextActions, reason),
authSession = authSession,
cause = cause
)

private companion object {
private const val EMBEDDED_PATH = "e"
private const val AUTHORIZE_PATH = "authorize"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@ public sealed interface EmbeddedAuthError {
/** The username or the password was invalid; the server does not say which. */
INVALID_IDENTIFIER_OR_PASSWORD,

/** A password is needed to create the account and none was sent. Retry with one. */
PASSWORD_REQUIRED,

/** A password was sent but the connection doesn't collect one. Retry without it. */
PASSWORD_NOT_ACCEPTED,

/** The connection's password policy rejected the password. Retry with another one. */
INVALID_PASSWORD,

/** Authorization is pending (e.g., push notification awaiting user approval). Retry after a delay. */
AUTHORIZATION_PENDING,

Expand All @@ -42,6 +51,9 @@ public sealed interface EmbeddedAuthError {
/** Terminal: too many wrong OTP submissions. Restart the flow. */
public data object TooManyWrongOtpAttempts : EmbeddedAuthError

/** Terminal: the limit of verification codes sent to this identifier was reached. Restart the flow. */
public data object TooManyResendAttempts : EmbeddedAuthError

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import com.auth0.android.result.Credentials
internal class AdvancingRequest(
private val authorize: Request<AuthorizeCode, EmbeddedAuthException>,
private val exchange: (authorizationCode: String) -> Request<Credentials, EmbeddedAuthException>,
private val onStepFailure: (EmbeddedAuthException) -> Unit,
private val onStepFailure: (EmbeddedAuthException) -> EmbeddedAuthException,
private val onFlowComplete: () -> Unit
) : Request<Credentials, EmbeddedAuthException> {

Expand All @@ -29,8 +29,7 @@ internal class AdvancingRequest(
}

override fun onFailure(error: EmbeddedAuthException) {
onStepFailure(error)
callback.onFailure(error)
callback.onFailure(onStepFailure(error))
}
})
}
Expand All @@ -40,8 +39,7 @@ internal class AdvancingRequest(
val code = try {
authorize.await()
} catch (error: EmbeddedAuthException) {
onStepFailure(error)
throw error
throw onStepFailure(error)
}
onFlowComplete()
return exchange(code.authorizationCode).await()
Expand All @@ -52,8 +50,7 @@ internal class AdvancingRequest(
val code = try {
authorize.execute()
} catch (error: EmbeddedAuthException) {
onStepFailure(error)
throw error
throw onStepFailure(error)
}
onFlowComplete()
return exchange(code.authorizationCode).execute()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.auth0.android.embedded.authorize

/**
* Whether the connection collects a credential when signing up a new user, as announced on
* [NextAction.Signup].
*/
public enum class CredentialRequirement {
/** The connection doesn't collect this credential. */
NONE,

/** The credential can be set up, or skipped. */
OPTIONAL,

/** The account can't be created without this credential. */
REQUIRED
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ private const val IP_BLOCKED = "ip_blocked"
private const val INVALID_CODE = "invalid_code"
private const val INVALID_IDENTIFIER_OR_CODE = "invalid_identifier_or_code"
private const val INVALID_IDENTIFIER_OR_PASSWORD = "invalid_identifier_or_password"
private const val PASSWORD_REQUIRED = "password_required"
private const val PASSWORD_NOT_ACCEPTED = "password_not_accepted"
private const val INVALID_PASSWORD = "invalid_password"
private const val TOO_MANY_RESEND_ATTEMPTS = "too_many_resend_attempts"
private const val INVALID_GRANT = "invalid_grant"
private const val INVALID_REQUEST = "invalid_request"
private const val TOO_MANY_REQUESTS_STATUS = 429
Expand Down Expand Up @@ -92,6 +96,8 @@ internal fun embeddedAuthErrorAdapter(): ErrorAdapter<EmbeddedAuthException> {
)
code == ACCESS_DENIED && description == TOO_MANY_WRONG_OTP_ATTEMPTS ->
EmbeddedAuthError.TooManyWrongOtpAttempts
code == ACCESS_DENIED && description == TOO_MANY_RESEND_ATTEMPTS ->
EmbeddedAuthError.TooManyResendAttempts
code == ACCESS_DENIED && description == CHALLENGE_EXPIRED ->
EmbeddedAuthError.ChallengeExpired
code == ACCESS_DENIED && description == AUTHORIZATION_REJECTED ->
Expand Down Expand Up @@ -136,6 +142,9 @@ internal fun embeddedAuthErrorAdapter(): ErrorAdapter<EmbeddedAuthException> {
EmbeddedAuthError.InsufficientAuthorization.Reason.INVALID_IDENTIFIER_OR_CODE
INVALID_IDENTIFIER_OR_PASSWORD ->
EmbeddedAuthError.InsufficientAuthorization.Reason.INVALID_IDENTIFIER_OR_PASSWORD
PASSWORD_REQUIRED -> EmbeddedAuthError.InsufficientAuthorization.Reason.PASSWORD_REQUIRED
PASSWORD_NOT_ACCEPTED -> EmbeddedAuthError.InsufficientAuthorization.Reason.PASSWORD_NOT_ACCEPTED
INVALID_PASSWORD -> EmbeddedAuthError.InsufficientAuthorization.Reason.INVALID_PASSWORD
AUTHORIZATION_PENDING ->
EmbeddedAuthError.InsufficientAuthorization.Reason.AUTHORIZATION_PENDING
SLOW_DOWN -> EmbeddedAuthError.InsufficientAuthorization.Reason.SLOW_DOWN
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.auth0.android.embedded.authorize

internal data class EmbeddedAuthState(
val authSession: String
val authSession: String,
val passwordRequirement: CredentialRequirement? = null
)
Original file line number Diff line number Diff line change
Expand Up @@ -91,20 +91,28 @@ public sealed interface NextAction {
/**
* Continue by signing up a new user. Act on it with [EmbeddedAuthClient.signup].
*
* [identifiers] lists the identifiers the connection collects at signup.
* [identifiers] lists the identifiers the connection collects at signup. [passwordRequirement] says
* whether the connection collects a password, which is set up with [EmbeddedAuthClient.confirmSignup].
*/
@ConsistentCopyVisibility
public data class Signup internal constructor(
public val identifiers: List<SignupIdentifier>
public val identifiers: List<SignupIdentifier>,
public val passwordRequirement: CredentialRequirement
) : NextAction {
override val capability: EmbeddedCapability = EmbeddedCapability.SIGNUP
}

/**
* Continue by creating the new user's account once the required identifiers are verified. Act on
* it with [EmbeddedAuthClient.confirmSignup].
*
* [password] is `null` when the connection doesn't collect a password. Otherwise the app should ask
* for one, and must when [SignupPassword.required] is `true`.
*/
public data object ConfirmSignup : NextAction {
@ConsistentCopyVisibility
public data class ConfirmSignup internal constructor(
public val password: SignupPassword?
) : NextAction {
override val capability: EmbeddedCapability = EmbeddedCapability.CONFIRM_SIGNUP
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ private const val IDENTIFIERS_KEY = "identifiers"
private const val EMAIL_KEY = "email"
private const val PHONE_NUMBER_KEY = "phone_number"
private const val REQUIRED_KEY = "required"
private const val AUTHENTICATION_METHODS_KEY = "authentication_methods"
private const val PASSWORD_KEY = "password"
private const val PASSWORD_COMPLEXITY_OPTIONS_KEY = "password_complexity_options"
private const val MIN_LENGTH_KEY = "min_length"
private const val CHARACTER_TYPES_KEY = "character_types"

internal fun List<Map<String, Any>>.toNextActions(): List<NextAction> = mapNotNull { it.toNextAction() }

Expand Down Expand Up @@ -66,9 +71,9 @@ private fun Map<String, Any>.toNextAction(): NextAction? {
EmbeddedCapability.SIGNUP -> {
val identifiers = (this[IDENTIFIERS_KEY] as? Map<*, *>)?.toSignupIdentifiers()
?: return dropped(raw, IDENTIFIERS_KEY)
NextAction.Signup(identifiers)
NextAction.Signup(identifiers, toPasswordRequirement())
}
EmbeddedCapability.CONFIRM_SIGNUP -> NextAction.ConfirmSignup
EmbeddedCapability.CONFIRM_SIGNUP -> NextAction.ConfirmSignup(toSignupPassword())
EmbeddedCapability.UNKNOWN -> NextAction.Unknown(raw)
}
}
Expand All @@ -90,6 +95,55 @@ private fun Map<*, *>.toSignupIdentifiers(): List<SignupIdentifier>? {
return identifiers.ifEmpty { null }
}

/**
* Reads whether the signup action announces a password: `required: true` is [CredentialRequirement.REQUIRED],
* `required: false` is [CredentialRequirement.OPTIONAL], and no password entry is [CredentialRequirement.NONE].
* A password entry without a boolean `required` is logged and treated as [CredentialRequirement.NONE].
*/
private fun Map<String, Any>.toPasswordRequirement(): CredentialRequirement {
val password = (this[AUTHENTICATION_METHODS_KEY] as? Map<*, *>)?.get(PASSWORD_KEY) ?: return CredentialRequirement.NONE
return when ((password as? Map<*, *>)?.get(REQUIRED_KEY) as? Boolean) {
true -> CredentialRequirement.REQUIRED
false -> CredentialRequirement.OPTIONAL
null -> {
Log.w(TAG, "Ignoring signup password: required field \"$REQUIRED_KEY\" was missing or invalid.")
CredentialRequirement.NONE
}
}
}

/**
* Reads the confirm action's `password_complexity_options`, which the server sends exactly when the
* connection collects a password. The result is `null` when they are absent, or when `min_length` is missing
* or not a positive integer. Unknown `character_types` are skipped. [SignupPassword.required] is `false`
* here; the client sets it from the requirement announced on the signup action.
*/
private fun Map<String, Any>.toSignupPassword(): SignupPassword? {
val options = this[PASSWORD_COMPLEXITY_OPTIONS_KEY] as? Map<*, *> ?: return null
val minLength = (options[MIN_LENGTH_KEY] as? Number)?.toWholeInt()?.takeIf { it >= 1 }
if (minLength == null) {
Log.w(TAG, "Ignoring signup password: required field \"$MIN_LENGTH_KEY\" was missing or invalid.")
return null
}
val characterTypes = (options[CHARACTER_TYPES_KEY] as? List<*>)
?.filterIsInstance<String>()
?.mapNotNull(::passwordCharacterType)
?.distinct()
.orEmpty()
return SignupPassword(required = false, complexity = PasswordComplexity(minLength, characterTypes))
}

private fun Number.toWholeInt(): Int? =
toDouble().takeIf { it % 1.0 == 0.0 && it in Int.MIN_VALUE.toDouble()..Int.MAX_VALUE.toDouble() }?.toInt()

private fun passwordCharacterType(value: String): PasswordComplexity.CharacterType? = when (value) {
"lowercase" -> PasswordComplexity.CharacterType.LOWERCASE
"uppercase" -> PasswordComplexity.CharacterType.UPPERCASE
"number" -> PasswordComplexity.CharacterType.NUMBER
"special" -> PasswordComplexity.CharacterType.SPECIAL
else -> null
}

private val SIGNUP_IDENTIFIER_TYPES: List<Pair<String, IdentifierType>> = listOf(
EMAIL_KEY to IdentifierType.EMAIL,
PHONE_NUMBER_KEY to IdentifierType.PHONE
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.auth0.android.embedded.authorize

/**
* Advisory rules for a signup password: the minimum length and the character types that are all required.
*
* The server applies the connection's full password policy, which can include rules not listed here,
* such as requiring only some of the character types. A password that meets these rules can still be
* rejected with [com.auth0.android.embedded.EmbeddedAuthError.InsufficientAuthorization.Reason.INVALID_PASSWORD].
*/
@ConsistentCopyVisibility
public data class PasswordComplexity internal constructor(
/** The minimum length, in UTF-16 code units. */
public val minLength: Int,

/** The character types the password must contain, all of them. Empty when none is required. */
public val characterTypes: List<CharacterType>
) {

/** A kind of character a password can be required to contain. */
public enum class CharacterType {
LOWERCASE,
UPPERCASE,
NUMBER,
SPECIAL
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.auth0.android.embedded.authorize

import com.auth0.android.embedded.EmbeddedAuthClient

/**
* The password a connection collects when signing up a new user, as offered on [NextAction.ConfirmSignup].
*
* Submit it with [EmbeddedAuthClient.confirmSignup].
*/
@ConsistentCopyVisibility
public data class SignupPassword internal constructor(
/** Whether the account can't be created without a password. */
public val required: Boolean,

/** The advisory rules for the password. */
public val complexity: PasswordComplexity
)
Loading
Loading