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
1 change: 1 addition & 0 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Each topic lives in its own file under [`examples/`](examples).
## Embedded Authentication (Beta)

- [Email OTP — first-factor & MFA second-factor](examples/embedded-auth/authorize.md)
- [Username & Password — first-factor](examples/embedded-auth/password-authorize.md)
- [Phone OTP (SMS/Voice) — first-factor & MFA second-factor](examples/embedded-auth/phone-authorize.md)
- [Push Notification Polling (OOB)](examples/embedded-auth/push-oob-polling.md)
- [Recovery Code](examples/embedded-auth/recovery-code.md)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ public class EmbeddedAuthClient(private val auth0: Auth0) {

IdentifierType.PHONE ->
continueStep(EmbeddedCapability.IDENTIFY_PHONE) { addParameter(PHONE_KEY, identifier) }

IdentifierType.USERNAME ->
continueStep(EmbeddedCapability.IDENTIFY_USERNAME) { addParameter(USERNAME_KEY, identifier) }
}

/**
Expand All @@ -119,20 +122,28 @@ public class EmbeddedAuthClient(private val auth0: Auth0) {
public fun verifyOtp(
code: String,
type: OtpType = OtpType.OOB
): Request<Credentials, EmbeddedAuthException> {
val session = transactionState?.authSession ?: return noActiveSession()
val request = factory.post(authorizeUrl, authorizeCodeAdapter(gson))
.addParameters(
mapOf(
AUTH_SESSION_KEY to session,
ACTION_KEY to EmbeddedCapability.VERIFY_OTP.value,
CLIENT_ID_KEY to clientId
)
)
.addParameter(OTP_KEY, code)
.addParameter(TYPE_KEY, type.value)
return advancing(request)
}
): Request<Credentials, EmbeddedAuthException> =
terminalStep(EmbeddedCapability.VERIFY_OTP) {
addParameter(OTP_KEY, code)
addParameter(TYPE_KEY, type.value)
}

/**
* Verifies the user's [password]. Completes the flow when no further factor is required.
*
* On success, it yields the [Credentials]. If the server requires further steps (for example
* a second factor) or rejects the password, the request completes through
* [EmbeddedAuthException] instead, with
* [EmbeddedAuthError.InsufficientAuthorization.nextActions] on [EmbeddedAuthException.error]; a
* wrong password reports [EmbeddedAuthError.InsufficientAuthorization.Reason.INVALID_IDENTIFIER_OR_PASSWORD].
*
* A password found in a data breach completes with the terminal
* [EmbeddedAuthError.PasswordLeaked]
*/
public fun verifyPassword(password: String): Request<Credentials, EmbeddedAuthException> =
terminalStep(EmbeddedCapability.VERIFY_PASSWORD) {
addParameter(PASSWORD_KEY, password)
}

/**
* Continues the flow by requesting a phone challenge for the authenticator at [index].
Expand Down Expand Up @@ -192,18 +203,8 @@ public class EmbeddedAuthClient(private val auth0: Auth0) {
* On rejection or expiry the request completes with a terminal [EmbeddedAuthError] such as
* [EmbeddedAuthError.AuthorizationRejected] or [EmbeddedAuthError.ChallengeExpired].
*/
public fun verifyOob(): Request<Credentials, EmbeddedAuthException> {
val session = transactionState?.authSession ?: return noActiveSession()
val request = factory.post(authorizeUrl, authorizeCodeAdapter(gson))
.addParameters(
mapOf(
AUTH_SESSION_KEY to session,
ACTION_KEY to EmbeddedCapability.VERIFY_OOB.value,
CLIENT_ID_KEY to clientId
)
)
return advancing(request)
}
public fun verifyOob(): Request<Credentials, EmbeddedAuthException> =
terminalStep(EmbeddedCapability.VERIFY_OOB)

/**
* Verifies a recovery [code].
Expand All @@ -229,25 +230,32 @@ public class EmbeddedAuthClient(private val auth0: Auth0) {
*
* On success, it yields the [Credentials].
*/
public fun confirmRecoveryCode(): Request<Credentials, EmbeddedAuthException> {
public fun confirmRecoveryCode(): Request<Credentials, EmbeddedAuthException> =
terminalStep(EmbeddedCapability.CONFIRM_RECOVERY_CODE)

private fun continueStep(
action: EmbeddedCapability,
addPayload: Request<Void?, EmbeddedAuthException>.() -> Unit = {}
): Request<Void?, EmbeddedAuthException> {
val session = transactionState?.authSession ?: return noActiveSession()
val request = factory.post(authorizeUrl, authorizeCodeAdapter(gson))
val request = factory.post(authorizeUrl)
.addParameters(
mapOf(
AUTH_SESSION_KEY to session,
ACTION_KEY to EmbeddedCapability.CONFIRM_RECOVERY_CODE.value,
ACTION_KEY to action.value,
CLIENT_ID_KEY to clientId
)
)
return advancing(request)
request.addPayload()
return stepping(request)
}

private fun continueStep(
private fun terminalStep(
action: EmbeddedCapability,
addPayload: Request<Void?, EmbeddedAuthException>.() -> Unit = {}
): Request<Void?, EmbeddedAuthException> {
addPayload: Request<AuthorizeCode, EmbeddedAuthException>.() -> Unit = {}
): Request<Credentials, EmbeddedAuthException> {
val session = transactionState?.authSession ?: return noActiveSession()
val request = factory.post(authorizeUrl)
val request = factory.post(authorizeUrl, authorizeCodeAdapter(gson))
.addParameters(
mapOf(
AUTH_SESSION_KEY to session,
Expand All @@ -256,7 +264,7 @@ public class EmbeddedAuthClient(private val auth0: Auth0) {
)
)
request.addPayload()
return stepping(request)
return advancing(request)
}

private fun <T> noActiveSession(): Request<T, EmbeddedAuthException> = FailedRequest<T>(
Expand Down Expand Up @@ -330,6 +338,8 @@ public class EmbeddedAuthClient(private val auth0: Auth0) {
private const val ACTION_KEY = "action"
private const val EMAIL_KEY = "email"
private const val PHONE_KEY = "phone"
private const val USERNAME_KEY = "username"
private const val PASSWORD_KEY = "password"
private const val DELIVERY_METHOD_KEY = "delivery_method"
private const val OTP_KEY = "otp"
private const val INDEX_KEY = "index"
Expand All @@ -344,10 +354,12 @@ public class EmbeddedAuthClient(private val auth0: Auth0) {
private val DEFAULT_CAPABILITIES: Set<EmbeddedCapability> = setOf(
EmbeddedCapability.IDENTIFY_EMAIL,
EmbeddedCapability.IDENTIFY_PHONE,
EmbeddedCapability.IDENTIFY_USERNAME,

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.

DEFAULT_CAPABILITIES now includes username and password. Apps that call authorize() with the defaults can start getting IdentifyUsername / VerifyPassword actions, and a when with an else will silently ignore them. Worth a release-note mention.

EmbeddedCapability.CHALLENGE_EMAIL,
EmbeddedCapability.CHALLENGE_PHONE,
EmbeddedCapability.CHALLENGE_PUSH,
EmbeddedCapability.VERIFY_OTP,
EmbeddedCapability.VERIFY_PASSWORD,
EmbeddedCapability.VERIFY_OOB,
EmbeddedCapability.VERIFY_RECOVERY_CODE,
EmbeddedCapability.CONFIRM_RECOVERY_CODE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ public sealed interface EmbeddedAuthError {
/** The identifier or the code was invalid; the server does not say which. */
INVALID_IDENTIFIER_OR_CODE,

/** The username or the password was invalid; the server does not say which. */
INVALID_IDENTIFIER_OR_PASSWORD,

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

Expand Down Expand Up @@ -78,6 +81,15 @@ public sealed interface EmbeddedAuthError {
/** Terminal: additional consent is required before the flow can proceed. */
public data object ConsentRequired : EmbeddedAuthError

/**
* Terminal: breached password detection blocked the login because the password appears in a
* data breach. The user must reset their password before starting a new flow.
*/
public data object PasswordLeaked : EmbeddedAuthError

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.

The SDK design doc has password_leaked as a terminal access_denied. The approved server RFD (decision 4) folds breached passwords into the recoverable invalid_identifier_or_password. Can we confirm with the server team which one ships? If it's the RFD, PasswordLeaked never fires and the docs and sample promise a reset path that doesn't exist.


/** Terminal: no further action is available to continue authorization. Restart the flow. */
public data object NoNextSteps : EmbeddedAuthError

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.

For the release notes: access_denied + no_next_steps used to come back as AccessDenied and now comes back as NoNextSteps. The new subtypes (this and PasswordLeaked) also break exhaustive when without an else. Fine for the beta.


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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,15 @@ private const val TOO_MANY_LOGINS = "too_many_logins"
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 INVALID_GRANT = "invalid_grant"
private const val INVALID_REQUEST = "invalid_request"
private const val TOO_MANY_REQUESTS_STATUS = 429
private const val AUTHORIZATION_REJECTED = "authorization_rejected"
private const val NO_ELIGIBLE_FACTORS = "no_eligible_factors"
private const val CONSENT_REQUIRED = "consent_required"
private const val PASSWORD_LEAKED = "password_leaked"
private const val NO_NEXT_STEPS = "no_next_steps"
private const val AUTHORIZATION_PENDING = "authorization_pending"
private const val SLOW_DOWN = "slow_down"

Expand Down Expand Up @@ -97,6 +100,10 @@ internal fun embeddedAuthErrorAdapter(): ErrorAdapter<EmbeddedAuthException> {
EmbeddedAuthError.NoEligibleFactors
code == ACCESS_DENIED && description == CONSENT_REQUIRED ->
EmbeddedAuthError.ConsentRequired
code == ACCESS_DENIED && description == PASSWORD_LEAKED ->
EmbeddedAuthError.PasswordLeaked
code == ACCESS_DENIED && description == NO_NEXT_STEPS ->
EmbeddedAuthError.NoNextSteps
code == ACCESS_DENIED ->
EmbeddedAuthError.AccessDenied
statusCode == TOO_MANY_REQUESTS_STATUS && code == TOO_MANY_REQUESTS && description == TOO_MANY_ATTEMPTS ->
Expand Down Expand Up @@ -127,6 +134,8 @@ internal fun embeddedAuthErrorAdapter(): ErrorAdapter<EmbeddedAuthException> {
INVALID_CODE -> EmbeddedAuthError.InsufficientAuthorization.Reason.INVALID_CODE
INVALID_IDENTIFIER_OR_CODE ->
EmbeddedAuthError.InsufficientAuthorization.Reason.INVALID_IDENTIFIER_OR_CODE
INVALID_IDENTIFIER_OR_PASSWORD ->
EmbeddedAuthError.InsufficientAuthorization.Reason.INVALID_IDENTIFIER_OR_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
Expand Up @@ -4,10 +4,12 @@ package com.auth0.android.embedded.authorize
public enum class EmbeddedCapability(public val value: String) {
IDENTIFY_EMAIL("action:identify:email:v1"),
IDENTIFY_PHONE("action:identify:phone:v1"),
IDENTIFY_USERNAME("action:identify:username:v1"),
CHALLENGE_EMAIL("action:challenge:email:v1"),
CHALLENGE_PHONE("action:challenge:phone:v1"),
CHALLENGE_PUSH("action:challenge:push:v1"),
VERIFY_OTP("action:verify:otp:v1"),
VERIFY_PASSWORD("action:verify:password:v1"),
VERIFY_OOB("action:verify:oob:v1"),
VERIFY_RECOVERY_CODE("action:verify:recovery-code:v1"),
CONFIRM_RECOVERY_CODE("action:confirm:recovery-code:v1"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ public enum class IdentifierType {
EMAIL,
/** A phone number. */
PHONE,
/** A username. */
USERNAME,
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ public sealed interface NextAction {
override val capability: EmbeddedCapability = EmbeddedCapability.IDENTIFY_PHONE
}

/** Continue by submitting a username. Act on it with [EmbeddedAuthClient.identify]. */
public data object IdentifyUsername : NextAction {
override val capability: EmbeddedCapability = EmbeddedCapability.IDENTIFY_USERNAME
}

/** Continue by verifying the user's password. Act on it with [EmbeddedAuthClient.verifyPassword]. */
public data object VerifyPassword : NextAction {
override val capability: EmbeddedCapability = EmbeddedCapability.VERIFY_PASSWORD
}

/** Continue by requesting an email challenge. Act on it with [EmbeddedAuthClient.challengeEmail]. */
@ConsistentCopyVisibility
public data class ChallengeEmail internal constructor(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ private fun Map<String, Any>.toNextAction(): NextAction? {
return when (EmbeddedCapability.fromValue(raw)) {
EmbeddedCapability.IDENTIFY_EMAIL -> NextAction.IdentifyEmail
EmbeddedCapability.IDENTIFY_PHONE -> NextAction.IdentifyPhone
EmbeddedCapability.IDENTIFY_USERNAME -> NextAction.IdentifyUsername
EmbeddedCapability.VERIFY_PASSWORD -> NextAction.VerifyPassword
EmbeddedCapability.CHALLENGE_EMAIL -> {
val index = (this[INDEX_KEY] as? Number)?.toInt() ?: return dropped(raw, INDEX_KEY)
val identifier = this[IDENTIFIER_KEY] as? String ?: return dropped(raw, IDENTIFIER_KEY)
Expand Down
Loading
Loading