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
2 changes: 1 addition & 1 deletion A0Auth0.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Pod::Spec.new do |s|
s.source_files = 'ios/**/*.{h,m,mm,swift}'
s.requires_arc = true

s.dependency 'Auth0', '2.21.2'
s.dependency 'Auth0', '2.23.0'
s.dependency 'SimpleKeychain', '1.3.0'

install_modules_dependencies(s)
Expand Down
50 changes: 50 additions & 0 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- [Using custom scheme for web authentication redirection](#using-custom-scheme-for-web-authentication-redirection)
- [Login using MFA with One Time Password code](#login-using-mfa-with-one-time-password-code)
- [Login with Passwordless](#login-with-passwordless)
- [Login with Passwordless OTP (Database Connections)](#login-with-passwordless-otp-database-connections)
- [Create user in database connection](#create-user-in-database-connection)
- [Using HTTPS callback URLs](#using-https-callback-urls)
- [Using Custom Headers](#using-custom-headers)
Expand Down Expand Up @@ -310,6 +311,55 @@ const subscription = Linking.addEventListener('url', ({ url }) => {
> [!NOTE]
> For native and Expo apps, the **code flow is recommended** because the SDK handles the full exchange for you. Use the magic link flow only if your use case specifically requires magic links, and be aware that you are responsible for handling the deep link and securely storing the returned tokens.

### Login with Passwordless OTP (Database Connections)

> [!NOTE]
> This feature is currently in [Early Access](https://auth0.com/docs/troubleshoot/product-lifecycle/product-release-stages#early-access). Reach out to Auth0 support to have it enabled for your tenant.
>
> Native only (iOS, Android). Calling `auth0.passwordless.*` on the web platform rejects with an `UnsupportedOperation` error.

This flow lets a user authenticate with a one-time code sent to their email or phone against a standard **database** connection (`auth0` strategy) that has `email_otp` or `phone_otp` enabled. It is distinct from [Login with Passwordless](#login-with-passwordless), which targets dedicated `email` / `sms` connections.

It is a two-step, challenge-response flow: request a challenge (which delivers the OTP and returns an opaque `auth_session`), then exchange the challenge and the user-entered code for credentials.

@sanchitmehtagit sanchitmehtagit Jul 2, 2026

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.

lets update EXAMPLES.md with info related to an optional allowSignup parameter (defaults to false) that controls whether a new user is created if one does not yet exist.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

added

#### Email challenge

```js
// 1. Request a challenge — Auth0 emails the OTP and returns the challenge.
const challenge = await auth0.passwordless.challengeWithEmail({
email: 'info@auth0.com',
connection: 'Username-Password-Authentication', // required; must have email_otp enabled
// allowSignup defaults to false
});

// 2. Exchange the challenge and the code the user received for credentials.
const credentials = await auth0.passwordless.loginWithOTP({
challenge,
otp: '123456',
});
```

#### Phone challenge

```js
// 1. Request a challenge — delivered by SMS ('text') or voice call ('voice').
const challenge = await auth0.passwordless.challengeWithPhoneNumber({
phoneNumber: '+15555550123',
connection: 'Username-Password-Authentication', // required; must have phone_otp enabled
deliveryMethod: 'text', // defaults to 'text'
});

// 2. Exchange the challenge and the code for credentials.
const credentials = await auth0.passwordless.loginWithOTP({
challenge,
otp: '123456',
});
```

Both challenge methods accept an optional `allowSignup` parameter (defaults to `false`). When set to `true`, a new user is created for the given email or phone number if one does not already exist on the connection; when `false`, the challenge is rejected for unknown identifiers.

The `challenge` object returned from a challenge call is opaque — pass it as-is to `loginWithOTP`. You can optionally provide `audience` and `scope` to `loginWithOTP`.

### Create user in database connection

```js
Expand Down
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ dependencies {
implementation "com.facebook.react:react-android"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "androidx.browser:browser:1.2.0"
implementation 'com.auth0.android:auth0:3.19.0'
implementation 'com.auth0.android:auth0:3.20.0'
}

if (isNewArchitectureEnabled()) {
Expand Down
34 changes: 34 additions & 0 deletions android/src/main/java/com/auth0/react/A0Auth0Module.kt
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0

private var auth0: Auth0? = null
private var myAccount: MyAccount? = null
private var passwordless: Passwordless? = null
private lateinit var secureCredentialsManager: SecureCredentialsManager
private var webAuthPromise: Promise? = null

Expand Down Expand Up @@ -279,6 +280,7 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0
this.useDPoP = useDPoP ?: true
auth0 = Auth0.getInstance(clientId, domain)
myAccount = MyAccount(auth0!!, this.useDPoP, reactContext)
passwordless = Passwordless(auth0!!, this.useDPoP, reactContext)

val authAPI = AuthenticationAPIClient(auth0!!)
if (this.useDPoP) {
Expand Down Expand Up @@ -774,6 +776,38 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0
}


@ReactMethod
override fun passwordlessChallengeWithEmail(
email: String,
connection: String,
allowSignup: Boolean,
promise: Promise
) {
passwordless!!.challengeWithEmail(email, connection, allowSignup, promise)
}

@ReactMethod
override fun passwordlessChallengeWithPhoneNumber(
phoneNumber: String,
connection: String,
deliveryMethod: String,
Comment thread
pmathew92 marked this conversation as resolved.
allowSignup: Boolean,
promise: Promise
) {
passwordless!!.challengeWithPhoneNumber(phoneNumber, connection, deliveryMethod, allowSignup, promise)
}

@ReactMethod
override fun passwordlessLoginWithOTP(
authSession: String,
otp: String,
audience: String?,
scope: String?,
promise: Promise
) {
passwordless!!.loginWithOTP(authSession, otp, audience, scope, promise)
}

@ReactMethod
override fun passkeyEnrollmentChallenge(
accessToken: String,
Expand Down
97 changes: 97 additions & 0 deletions android/src/main/java/com/auth0/react/Passwordless.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package com.auth0.react

import com.auth0.android.Auth0
import com.auth0.android.authentication.AuthenticationAPIClient
import com.auth0.android.authentication.AuthenticationException
import com.auth0.android.authentication.passwordless.DeliveryMethod
import com.auth0.android.result.Credentials
import com.auth0.android.result.PasswordlessChallenge
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.WritableNativeMap

class Passwordless(
auth0: Auth0,
useDPoP: Boolean,
reactContext: ReactApplicationContext
) {

private val client: AuthenticationAPIClient = AuthenticationAPIClient(auth0).also {
if (useDPoP) {
it.useDPoP(reactContext)
}
}

fun challengeWithEmail(
email: String,
connection: String,
allowSignup: Boolean = false,
promise: Promise
) {
client.passwordlessClient()
.challengeWithEmail(email, connection, allowSignup)
.start(object : com.auth0.android.callback.Callback<PasswordlessChallenge, AuthenticationException> {
override fun onSuccess(result: PasswordlessChallenge) {
val map = WritableNativeMap().apply {
putString("authSession", result.authSession)
}
promise.resolve(map)
}

override fun onFailure(error: AuthenticationException) {
promise.reject("PASSWORDLESS_CHALLENGE_FAILED", error.getDescription(), error)
}
})
}

fun challengeWithPhoneNumber(
phoneNumber: String,
connection: String,
deliveryMethod: String,
allowSignup: Boolean = false,
promise: Promise
) {
val method = if (deliveryMethod == "voice") DeliveryMethod.VOICE else DeliveryMethod.TEXT

client.passwordlessClient()
.challengeWithPhoneNumber(phoneNumber, connection, method, allowSignup)
.start(object : com.auth0.android.callback.Callback<PasswordlessChallenge, AuthenticationException> {
override fun onSuccess(result: PasswordlessChallenge) {
val map = WritableNativeMap().apply {
putString("authSession", result.authSession)
}
promise.resolve(map)
}

override fun onFailure(error: AuthenticationException) {
promise.reject("PASSWORDLESS_CHALLENGE_FAILED", error.getDescription(), error)
}
})
}

fun loginWithOTP(
authSession: String,
otp: String,
audience: String?,
scope: String?,
promise: Promise
) {
val finalScope = if (scope.isNullOrBlank()) "openid profile email" else scope
val finalAudience = audience?.trim()?.ifEmpty { null }

val request = client.passwordlessClient()
.loginWithOTP(PasswordlessChallenge(authSession), otp)
finalAudience?.let { request.setAudience(it) }
request.setScope(finalScope)

request.start(object : com.auth0.android.callback.Callback<Credentials, AuthenticationException> {
override fun onSuccess(result: Credentials) {
promise.resolve(CredentialsParser.toMap(result))
}

override fun onFailure(error: AuthenticationException) {
promise.reject("PASSWORDLESS_LOGIN_FAILED", error.getDescription(), error)
}
})
}
}
10 changes: 5 additions & 5 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
PODS:
- A0Auth0 (5.7.0):
- Auth0 (= 2.21.2)
- A0Auth0 (5.8.0):
- Auth0 (= 2.23.0)
- hermes-engine
- RCTRequired
- RCTTypeSafety
Expand All @@ -23,7 +23,7 @@ PODS:
- ReactNativeDependencies
- SimpleKeychain (= 1.3.0)
- Yoga
- Auth0 (2.21.2):
- Auth0 (2.23.0):
- JWTDecode (= 3.3.0)
- SimpleKeychain (= 1.3.0)
- FBLazyVector (0.86.0)
Expand Down Expand Up @@ -2242,8 +2242,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/yoga"

SPEC CHECKSUMS:
A0Auth0: 3f1b87e65cd0fd4620431b93c8ed2e6eebcdae00
Auth0: e15bc9c1e39a53efc8853d16460b9be409d5346f
A0Auth0: e14195294c028c4ebce21e45a17ee11749e7a943
Auth0: dd46c309079f995e91dfd7af38e8fefd1a43bf4e
FBLazyVector: b3e7ad108f0d882e30445c5527d774e3fd432f3d
hermes-engine: 4ba8c4848d46c6a441e09d1c62f883ba69bc5b76
JWTDecode: 1ca6f765844457d0dd8690436860fecee788f631
Expand Down
Loading
Loading