-
Notifications
You must be signed in to change notification settings - Fork 238
feat: Add resumeSession() to recover Web Auth logins after Android process death #1566
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5127fff
2ca50d4
443d5ba
c679a26
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,7 +3,10 @@ package com.auth0.react | |
| import android.app.Activity | ||
| import android.content.Intent | ||
| import android.os.Build | ||
| import android.os.Handler | ||
| import android.os.Looper | ||
| import androidx.fragment.app.FragmentActivity | ||
| import androidx.lifecycle.LifecycleOwner | ||
| import com.auth0.android.Auth0 | ||
| import com.auth0.android.authentication.AuthenticationAPIClient | ||
| import com.auth0.android.authentication.AuthenticationException | ||
|
|
@@ -38,6 +41,11 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0 | |
|
|
||
| companion object { | ||
| const val NAME = "A0Auth0" | ||
|
|
||
| // Grace window (ms) for an in-flight restored token exchange to complete before | ||
| // resumeWebAuthSession resolves null. | ||
| private const val RESUME_SESSION_GRACE_MS = 5000L | ||
|
|
||
| private const val CREDENTIAL_MANAGER_ERROR_CODE = "CREDENTIAL_MANAGER_ERROR" | ||
| private const val BIOMETRICS_AUTHENTICATION_ERROR_CODE = "BIOMETRICS_CONFIGURATION_ERROR" | ||
|
|
||
|
|
@@ -166,19 +174,86 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0 | |
| } | ||
|
|
||
| builder.withParameters(cleanedParameters) | ||
| builder.start(reactContext.currentActivity as Activity, | ||
| object : com.auth0.android.callback.Callback<Credentials, AuthenticationException> { | ||
|
|
||
| val activity = reactContext.currentActivity | ||
| if (activity == null) { | ||
| promise.reject("a0.activity_not_available", "Current Activity is not available") | ||
| webAuthPromise = null | ||
| return | ||
| } | ||
| // start() registers a LifecycleObserver internally (Auth0.Android 3.19.0+ for | ||
| // process-death recovery), which must happen on the main thread. | ||
| UiThreadUtil.runOnUiThread { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Whats the need to explicitly put it to uiThread here ? Isn't this callback executed in UI thread ? Would there be any issues if this is not added ?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. as |
||
| builder.start(activity, | ||
| object : com.auth0.android.callback.Callback<Credentials, AuthenticationException> { | ||
| override fun onSuccess(result: Credentials) { | ||
| val map = CredentialsParser.toMap(result) | ||
| promise.resolve(map) | ||
| webAuthPromise = null | ||
| } | ||
|
|
||
| override fun onFailure(error: AuthenticationException) { | ||
| handleError(error, promise) | ||
| webAuthPromise = null | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| @ReactMethod | ||
| override fun resumeWebAuthSession(promise: Promise) { | ||
| val activity = reactContext.currentActivity | ||
| if (activity !is LifecycleOwner) { | ||
| // No lifecycle owner to register against; nothing to recover. | ||
| promise.resolve(null) | ||
| return | ||
| } | ||
| val lifecycleOwner = activity as LifecycleOwner | ||
|
|
||
| UiThreadUtil.runOnUiThread { | ||
| val resolved = java.util.concurrent.atomic.AtomicBoolean(false) | ||
| // Holds the pending grace-window timeout so a callback that settles the promise | ||
| // first can cancel it, releasing the retained promise/activity references instead | ||
| // of leaking them until the delay elapses. | ||
| val timeoutHandler = Handler(Looper.getMainLooper()) | ||
| val loginCallback = object : | ||
| com.auth0.android.callback.Callback<Credentials, AuthenticationException> { | ||
| override fun onSuccess(result: Credentials) { | ||
| val map = CredentialsParser.toMap(result) | ||
| promise.resolve(map) | ||
| webAuthPromise = null | ||
| if (resolved.compareAndSet(false, true)) { | ||
| timeoutHandler.removeCallbacksAndMessages(null) | ||
| promise.resolve(CredentialsParser.toMap(result)) | ||
| } | ||
| } | ||
|
|
||
| override fun onFailure(error: AuthenticationException) { | ||
| handleError(error, promise) | ||
| webAuthPromise = null | ||
| if (resolved.compareAndSet(false, true)) { | ||
| timeoutHandler.removeCallbacksAndMessages(null) | ||
| handleError(error, promise) | ||
| } | ||
| } | ||
| }) | ||
| } | ||
| // Logout recovery is out of scope; provide a no-op logout callback. | ||
| val logoutCallback = object : | ||
| com.auth0.android.callback.Callback<Void?, AuthenticationException> { | ||
| override fun onSuccess(result: Void?) {} | ||
| override fun onFailure(error: AuthenticationException) {} | ||
| } | ||
|
|
||
| // Registering against an already-RESUMED owner synchronously replays onResume, | ||
| // draining any result buffered after process-death recovery. | ||
| WebAuthProvider.registerCallbacks(lifecycleOwner, loginCallback, logoutCallback) | ||
|
|
||
| // Safety net for the rare case where the restored token exchange is still in | ||
| // flight: give it a short grace window, then resolve null if nothing arrived. | ||
| // The login callback cancels this timeout if it settles first. | ||
| if (!resolved.get()) { | ||
| timeoutHandler.postDelayed({ | ||
| if (resolved.compareAndSet(false, true)) { | ||
| promise.resolve(null) | ||
| } | ||
| }, RESUME_SESSION_GRACE_MS) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @ReactMethod | ||
|
|
@@ -392,16 +467,25 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0 | |
| ) | ||
| } | ||
|
|
||
| builder.start(reactContext.currentActivity as FragmentActivity, | ||
| object : com.auth0.android.callback.Callback<Void?, AuthenticationException> { | ||
| override fun onSuccess(result: Void?) { | ||
| promise.resolve(true) | ||
| } | ||
| val activity = reactContext.currentActivity | ||
| if (activity !is FragmentActivity) { | ||
| promise.reject("a0.activity_not_available", "Current Activity is not a FragmentActivity") | ||
| return | ||
| } | ||
| // start() registers a LifecycleObserver internally (Auth0.Android 3.19.0+), | ||
| // which must happen on the main thread. | ||
| UiThreadUtil.runOnUiThread { | ||
| builder.start(activity, | ||
| object : com.auth0.android.callback.Callback<Void?, AuthenticationException> { | ||
| override fun onSuccess(result: Void?) { | ||
| promise.resolve(true) | ||
| } | ||
|
|
||
| override fun onFailure(e: AuthenticationException) { | ||
| handleError(e, promise) | ||
| } | ||
| }) | ||
| override fun onFailure(e: AuthenticationException) { | ||
| handleError(e, promise) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| @ReactMethod | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
where is UiThreadUtil located? is it auth0.android api or inbuilt api from android SDK
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
its from
com.facebook.react.bridge.UiThreadUtila react native built-in library