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
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.13'
s.dependency 'Auth0', '2.14'

install_modules_dependencies(s)
end
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -818,4 +818,4 @@ This project is licensed under the MIT license. See the <a href="https://github.
[license-image]: https://img.shields.io/npm/l/react-native-auth0.svg?style=flat-square
[license-url]: #license
[downloads-image]: https://img.shields.io/npm/dm/react-native-auth0.svg?style=flat-square
[downloads-url]: https://npmjs.org/package/react-native-auth0
[downloads-url]: https://npmjs.org/package/react-native-auth0
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.8.0'
implementation 'com.auth0.android:auth0:3.10.0'
}

if (isNewArchitectureEnabled()) {
Expand Down
131 changes: 130 additions & 1 deletion android/src/main/java/com/auth0/react/A0Auth0Module.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import android.app.Activity
import android.content.Intent
import androidx.fragment.app.FragmentActivity
import com.auth0.android.Auth0
import com.auth0.android.authentication.AuthenticationAPIClient
import com.auth0.android.authentication.AuthenticationException
import com.auth0.android.authentication.storage.CredentialsManagerException
import com.auth0.android.authentication.storage.LocalAuthenticationOptions
import com.auth0.android.authentication.storage.SecureCredentialsManager
import com.auth0.android.authentication.storage.SharedPreferencesStorage
import com.auth0.android.dpop.DPoP
import com.auth0.android.dpop.DPoPException
import com.auth0.android.provider.WebAuthProvider
import com.auth0.android.result.Credentials
import com.facebook.react.bridge.ActivityEventListener
Expand All @@ -17,6 +20,7 @@ import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.UiThreadUtil
import com.facebook.react.bridge.WritableNativeMap
import java.net.MalformedURLException
import java.net.URL

Expand All @@ -26,6 +30,20 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0
const val NAME = "A0Auth0"
private const val CREDENTIAL_MANAGER_ERROR_CODE = "CREDENTIAL_MANAGER_ERROR"
private const val BIOMETRICS_AUTHENTICATION_ERROR_CODE = "BIOMETRICS_CONFIGURATION_ERROR"

// DPoP-specific error codes
private const val DPOP_ERROR_CODE = "DPOP_ERROR"
private const val DPOP_KEY_GENERATION_FAILED_CODE = "DPOP_KEY_GENERATION_FAILED"
private const val DPOP_KEY_STORAGE_FAILED_CODE = "DPOP_KEY_STORAGE_FAILED"
private const val DPOP_KEY_RETRIEVAL_FAILED_CODE = "DPOP_KEY_RETRIEVAL_FAILED"
private const val DPOP_KEYSTORE_ERROR_CODE = "DPOP_KEYSTORE_ERROR"
private const val DPOP_CRYPTO_ERROR_CODE = "DPOP_CRYPTO_ERROR"
private const val DPOP_GENERATION_FAILED_CODE = "DPOP_GENERATION_FAILED"
Copy link
Contributor

Choose a reason for hiding this comment

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

DPOP_GENERATION_FAILED_CODE do you need a specific code for this. DPoP generation will always fail due to one of the other codes added

Copy link
Contributor Author

Choose a reason for hiding this comment

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

its an generic error for both android and iOS in case of anything unexpected happen in DPoP flow.
Let me know if you think I should remove it completely

private const val DPOP_PROOF_FAILED_CODE = "DPOP_PROOF_FAILED"
private const val DPOP_NONCE_MISMATCH_CODE = "DPOP_NONCE_MISMATCH"
private const val DPOP_INVALID_TOKEN_TYPE_CODE = "DPOP_INVALID_TOKEN_TYPE"
private const val DPOP_MISSING_PARAMETER_CODE = "DPOP_MISSING_PARAMETER"
private const val DPOP_CLEAR_KEY_FAILED_CODE = "DPOP_CLEAR_KEY_FAILED"
}

private val errorCodeMap = mapOf(
Expand Down Expand Up @@ -65,6 +83,8 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0
CredentialsManagerException.API_ERROR to "API_ERROR",
CredentialsManagerException.NO_NETWORK to "NO_NETWORK"
)
// DPoP enabled by default
private var useDPoP: Boolean = true

private var auth0: Auth0? = null
private lateinit var secureCredentialsManager: SecureCredentialsManager
Expand Down Expand Up @@ -92,6 +112,9 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0
additionalParameters: ReadableMap?,
promise: Promise
) {
if(this.useDPoP) {
WebAuthProvider.useDPoP(reactContext)
}
webAuthPromise = promise
val cleanedParameters = mutableMapOf<String, String>()

Expand Down Expand Up @@ -143,9 +166,16 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0
clientId: String,
domain: String,
localAuthenticationOptions: ReadableMap?,
useDPoP: Boolean?,
promise: Promise
) {
this.useDPoP = useDPoP ?: true
auth0 = Auth0.getInstance(clientId, domain)

val authAPI = AuthenticationAPIClient(auth0!!)
if (this.useDPoP) {
authAPI.useDPoP(reactContext)
}

localAuthenticationOptions?.let { options ->
val activity = reactContext.currentActivity
Expand Down Expand Up @@ -241,6 +271,17 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0
@ReactMethod
override fun clearCredentials(promise: Promise) {
secureCredentialsManager.clearCredentials()

// Also clear DPoP key if DPoP is enabled
if (useDPoP) {
try {
DPoP.clearKeyPair()
} catch (e: Exception) {
// Log error but don't fail the operation
android.util.Log.w(NAME, "Failed to clear DPoP key", e)
}
}

promise.resolve(true)
}

Expand Down Expand Up @@ -277,6 +318,79 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0
})
}

@ReactMethod
override fun getDPoPHeaders(url: String, method: String, accessToken: String, tokenType: String, nonce: String?, promise: Promise) {
try {
// Validate parameters
if (url.isEmpty()) {
promise.reject(
DPOP_MISSING_PARAMETER_CODE,
"URL parameter is required for DPoP header generation"
)
return
}

if (method.isEmpty()) {
promise.reject(
DPOP_MISSING_PARAMETER_CODE,
"HTTP method parameter is required for DPoP header generation"
)
return
}

if (accessToken.isEmpty()) {
promise.reject(
DPOP_MISSING_PARAMETER_CODE,
"Access token parameter is required for DPoP header generation"
)
return
}

// Check if token type is DPoP
if (!tokenType.equals("DPoP", ignoreCase = true)) {
// If not DPoP, return Bearer token format
val headers = WritableNativeMap()
headers.putString("Authorization", "Bearer $accessToken")
promise.resolve(headers)
return
}

val headerData = if (nonce != null && nonce.isNotEmpty()) {
DPoP.getHeaderData(method, url, accessToken, tokenType, nonce)
} else {
DPoP.getHeaderData(method, url, accessToken, tokenType)
}
val map = WritableNativeMap()
map.putString("Authorization", headerData.authorizationHeader)
headerData.dpopProof?.let { map.putString("DPoP", it) }
promise.resolve(map)
} catch (e: DPoPException) {
handleDPoPError(e, promise)
} catch (e: Exception) {
promise.reject(
DPOP_GENERATION_FAILED_CODE,
"Failed to generate DPoP headers: ${e.message}",
e
)
}
}

@ReactMethod
override fun clearDPoPKey(promise: Promise) {
try {
DPoP.clearKeyPair()
promise.resolve(null)
} catch (e: DPoPException) {
handleDPoPError(e, promise)
} catch (e: Exception) {
promise.reject(
DPOP_CLEAR_KEY_FAILED_CODE,
"Failed to clear DPoP key: ${e.message}",
e
)
}
}

override fun onActivityResult(activity: Activity, requestCode: Int, resultCode: Int, data: Intent?) {
// No-op
}
Expand Down Expand Up @@ -313,6 +427,21 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0
return errorCodeMap[e] ?: CREDENTIAL_MANAGER_ERROR_CODE
}

private fun handleDPoPError(error: DPoPException, promise: Promise) {
val errorCode = when (error) {
DPoPException.KEY_GENERATION_ERROR -> DPOP_KEY_GENERATION_FAILED_CODE
DPoPException.KEY_STORE_ERROR -> DPOP_KEYSTORE_ERROR_CODE
DPoPException.KEY_PAIR_NOT_FOUND -> DPOP_KEY_RETRIEVAL_FAILED_CODE
DPoPException.SIGNING_ERROR -> DPOP_PROOF_FAILED_CODE
DPoPException.MALFORMED_URL -> DPOP_MISSING_PARAMETER_CODE
DPoPException.UNSUPPORTED_ERROR -> DPOP_ERROR_CODE
DPoPException.UNKNOWN_ERROR -> DPOP_GENERATION_FAILED_CODE
else -> DPOP_GENERATION_FAILED_CODE
Copy link

Copilot AI Oct 23, 2025

Choose a reason for hiding this comment

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

This when expression matches DPoPException instances as if they were enum values, but the code suggests DPoPException is likely a sealed class or enum. Without exhaustive matching (removing the 'else' branch), new DPoPException types added in future SDK versions could silently fall through to the default case. Consider making this exhaustive or adding a comment explaining why the else branch is needed.

Suggested change
else -> DPOP_GENERATION_FAILED_CODE

Copilot uses AI. Check for mistakes.

}

promise.reject(errorCode, error.message ?: "DPoP operation failed", error)
}

private fun handleError(error: AuthenticationException, promise: Promise) {
when {
error.isBrowserAppNotAvailable -> {
Expand Down Expand Up @@ -340,4 +469,4 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0
error
)
}
}
}
9 changes: 9 additions & 0 deletions android/src/main/oldarch/com/auth0/react/A0Auth0Spec.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ abstract class A0Auth0Spec(context: ReactApplicationContext) : ReactContextBaseJ
clientId: String,
domain: String,
localAuthenticationOptions: ReadableMap?,
useDPoP: Boolean?,
promise: Promise
)

Expand Down Expand Up @@ -81,4 +82,12 @@ abstract class A0Auth0Spec(context: ReactApplicationContext) : ReactContextBaseJ
@ReactMethod
@DoNotStrip
abstract fun cancelWebAuth(promise: Promise)

@ReactMethod
@DoNotStrip
abstract fun getDPoPHeaders(url: String, method: String, accessToken: String, tokenType: String, nonce: String?, promise: Promise)

@ReactMethod
@DoNotStrip
abstract fun clearDPoPKey(promise: Promise)
}
20 changes: 10 additions & 10 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
PODS:
- A0Auth0 (5.0.1):
- Auth0 (= 2.13)
- Auth0 (= 2.14)
- boost
- DoubleConversion
- fast_float
Expand Down Expand Up @@ -28,7 +28,7 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- Auth0 (2.13.0):
- Auth0 (2.14.0):
- JWTDecode (= 3.3.0)
- SimpleKeychain (= 1.3.0)
- boost (1.84.0)
Expand Down Expand Up @@ -2441,7 +2441,7 @@ PODS:
- React-perflogger (= 0.82.0)
- React-utils (= 0.82.0)
- SocketRocket
- RNGestureHandler (2.28.0):
- RNGestureHandler (2.29.0):
- boost
- DoubleConversion
- fast_float
Expand Down Expand Up @@ -2469,7 +2469,7 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- RNScreens (4.15.4):
- RNScreens (4.18.0):
- boost
- DoubleConversion
- fast_float
Expand All @@ -2496,10 +2496,10 @@ PODS:
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- RNScreens/common (= 4.15.4)
- RNScreens/common (= 4.18.0)
- SocketRocket
- Yoga
- RNScreens/common (4.15.4):
- RNScreens/common (4.18.0):
- boost
- DoubleConversion
- fast_float
Expand Down Expand Up @@ -2778,8 +2778,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/yoga"

SPEC CHECKSUMS:
A0Auth0: ccddcfa49a643921b58197d8b4bd649d83b72987
Auth0: 8deb8df56dd91516403ec474d968fb9f79189b93
A0Auth0: ae12e22692f0a545862faee8a811e466ef12cc55
Auth0: 022dda235af8a664a4faf9e7b60b063b5bc08373
boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90
DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb
fast_float: b32c788ed9c6a8c584d114d0047beda9664e7cc6
Expand Down Expand Up @@ -2854,8 +2854,8 @@ SPEC CHECKSUMS:
ReactAppDependencyProvider: c5c4f5280e4ae0f9f4a739c64c4260fe0b3edaf1
ReactCodegen: 374f1c9242fbdd673b460d358b33860c0cc9d926
ReactCommon: 25c7f94aee74ddd93a8287756a8ac0830a309544
RNGestureHandler: f1dd7f92a0faa2868a919ab53bb9d66eb4ebfcf5
RNScreens: db22525a8ed56bb87ab038b8f03a050bf40e6ed8
RNGestureHandler: 6859520a21304f0bedf0643d0cf0beade47c83f2
RNScreens: d821082c6dd1cb397cc0c98b026eeafaa68be479
SimpleKeychain: 9c0f3ca8458fed74e01db864d181c5cbe278603e
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
Yoga: edeb9900b9e5bb5b27b9a6a2d5914e4fe4033c1b
Expand Down
21 changes: 17 additions & 4 deletions ios/A0Auth0.mm
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,10 @@ - (dispatch_queue_t)methodQueue
RCT_EXPORT_METHOD(initializeAuth0WithConfiguration:(NSString *)clientId
domain:(NSString *)domain
localAuthenticationOptions:(NSDictionary * _Nullable)localAuthenticationOptions
useDPoP:(NSNumber *)useDPoP
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) {
[self tryAndInitializeNativeBridge:clientId domain:domain withLocalAuthenticationOptions:localAuthenticationOptions resolve:resolve reject:reject];
reject:(RCTPromiseRejectBlock)reject) {
[self tryAndInitializeNativeBridge:clientId domain:domain withLocalAuthenticationOptions:localAuthenticationOptions useDPoP:useDPoP resolve:resolve reject:reject];
}


Expand Down Expand Up @@ -134,6 +135,17 @@ - (dispatch_queue_t)methodQueue
[self.nativeBridge webAuthLogoutWithScheme:scheme federated:federated redirectUri:redirectUri resolve:resolve reject:reject];
}

RCT_EXPORT_METHOD(clearDPoPKey:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
[self.nativeBridge clearDPoPKeyWithResolve:resolve reject:reject];
}


RCT_EXPORT_METHOD(getDPoPHeaders:(NSString *)url method:(NSString *)method accessToken:(NSString *)accessToken tokenType:(NSString *)tokenType nonce:(NSString *)nonce resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
[self.nativeBridge getDPoPHeadersWithUrl:url method:method accessToken:accessToken tokenType:tokenType nonce:nonce resolve:resolve reject:reject];
}






Expand All @@ -154,8 +166,9 @@ - (BOOL)checkHasValidNativeBridgeInstance:(NSString*) clientId domain:(NSString
return valid;
}

- (void)tryAndInitializeNativeBridge:(NSString *)clientId domain:(NSString *)domain withLocalAuthenticationOptions:(NSDictionary*) options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
NativeBridge *bridge = [[NativeBridge alloc] initWithClientId:clientId domain:domain localAuthenticationOptions:options resolve:resolve reject:reject];
- (void)tryAndInitializeNativeBridge:(NSString *)clientId domain:(NSString *)domain withLocalAuthenticationOptions:(NSDictionary*) options useDPoP:(NSNumber *)useDPoP resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
BOOL useDPoPBool = [useDPoP boolValue];
NativeBridge *bridge = [[NativeBridge alloc] initWithClientId:clientId domain:domain localAuthenticationOptions:options useDPoP:useDPoPBool resolve:resolve reject:reject];
self.nativeBridge = bridge;
}
#ifdef RCT_NEW_ARCH_ENABLED
Expand Down
Loading