From 79644273d137130ad3e62cf61b59692857b0443f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Coleksii-minaiev=E2=80=9D?= <“oleksii.minaiev@postindustria.com”> Date: Wed, 13 Nov 2024 11:04:30 +0200 Subject: [PATCH] FR-18338.3 Fixes after marge with master --- .../android/AuthenticationActivity.kt | 3 +- .../java/com/frontegg/android/FronteggAuth.kt | 38 ++++- .../android/services/FronteggAuthService.kt | 150 +++++++++++++++++- 3 files changed, 179 insertions(+), 12 deletions(-) diff --git a/android/src/main/java/com/frontegg/android/AuthenticationActivity.kt b/android/src/main/java/com/frontegg/android/AuthenticationActivity.kt index 4d37bbb..5eedf3c 100644 --- a/android/src/main/java/com/frontegg/android/AuthenticationActivity.kt +++ b/android/src/main/java/com/frontegg/android/AuthenticationActivity.kt @@ -11,6 +11,7 @@ import androidx.browser.customtabs.CustomTabsIntent import com.frontegg.android.services.FronteggAuthService import com.frontegg.android.services.FronteggInnerStorage import com.frontegg.android.EmbeddedAuthActivity.Companion +import com.frontegg.android.services.FronteggAppService import com.frontegg.android.utils.AuthorizeUrlGenerator class AuthenticationActivity : Activity() { @@ -88,7 +89,7 @@ class AuthenticationActivity : Activity() { * when using external browser login */ private fun invokeAuthFinishedCallback() { - if (FronteggApp.getInstance().isEmbeddedMode) { + if (FronteggAuth.instance.isEmbeddedMode) { return } onAuthFinishedCallback?.invoke() diff --git a/android/src/main/java/com/frontegg/android/FronteggAuth.kt b/android/src/main/java/com/frontegg/android/FronteggAuth.kt index c7427d6..2cc44b2 100644 --- a/android/src/main/java/com/frontegg/android/FronteggAuth.kt +++ b/android/src/main/java/com/frontegg/android/FronteggAuth.kt @@ -55,7 +55,11 @@ interface FronteggAuth { * @param activity is the activity of application; * @param loginHint is a value that auto filled to login field at Frontegg LoginBox. */ - fun login(activity: Activity, loginHint: String? = null) + fun login( + activity: Activity, + loginHint: String? = null, + callback: (() -> Unit)? = null + ) /** * Logout user. Clear data about the user. Could make a network request. @@ -63,7 +67,9 @@ interface FronteggAuth { * should be false after calling the [logout] method and [callback] was triggered. * @param callback call after the logout process is finished. */ - fun logout(callback: () -> Unit = {}) + fun logout( + callback: () -> Unit = {}, + ) /** * Direct Login Action process. Support only on Embedded Mode([EmbeddedAuthActivity] is enabled). @@ -86,7 +92,10 @@ interface FronteggAuth { * @param tenantId is available user tenants. Can extract them from [User].tenants.tenantId; * @param callback call after the tenant switching process is finished. */ - fun switchTenant(tenantId: String, callback: (Boolean) -> Unit = {}) + fun switchTenant( + tenantId: String, + callback: (Boolean) -> Unit = {}, + ) /** * Refresh token if needed. @@ -94,6 +103,29 @@ interface FronteggAuth { */ fun refreshTokenIfNeeded(): Boolean + /** + * Login with passkeys + * @param activity is the activity of application; + * @param callback call after the registration is finished or was intercepted by Exception. + * [error] not null if some Exception happened during logging. + */ + fun loginWithPasskeys( + activity: Activity, + callback: ((error: Exception?) -> Unit)? = null, + ) + + /** + * Register passkeys. + * @param activity is the activity of application; + * @param callback call after the registration is finished or was intercepted by Exception. + * [error] not null if some Exception happened during registration. + */ + fun registerPasskeys( + activity: Activity, + callback: ((error: Exception?) -> Unit)? = null, + ) + + companion object { /** diff --git a/android/src/main/java/com/frontegg/android/services/FronteggAuthService.kt b/android/src/main/java/com/frontegg/android/services/FronteggAuthService.kt index fe5a2fc..f8d4472 100644 --- a/android/src/main/java/com/frontegg/android/services/FronteggAuthService.kt +++ b/android/src/main/java/com/frontegg/android/services/FronteggAuthService.kt @@ -4,13 +4,20 @@ import android.annotation.SuppressLint import android.app.Activity import android.os.Handler import android.os.Looper +import android.util.Base64 import android.util.Log import android.webkit.CookieManager import android.webkit.WebView +import androidx.credentials.PublicKeyCredential import com.frontegg.android.AuthenticationActivity import com.frontegg.android.EmbeddedAuthActivity import com.frontegg.android.FronteggApp import com.frontegg.android.FronteggAuth +import com.frontegg.android.embedded.CredentialManagerHandler +import com.frontegg.android.exceptions.FailedToAuthenticateException +import com.frontegg.android.exceptions.MfaRequiredException +import com.frontegg.android.exceptions.WebAuthnAlreadyRegisteredInLocalDeviceException +import com.frontegg.android.exceptions.isWebAuthnRegisteredBeforeException import com.frontegg.android.models.User import com.frontegg.android.regions.RegionConfig import com.frontegg.android.utils.AuthorizeUrlGenerator @@ -23,7 +30,10 @@ import io.reactivex.rxjava3.core.Observable import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.json.JSONObject @OptIn(DelicateCoroutinesApi::class) @SuppressLint("CheckResult") @@ -90,15 +100,21 @@ class FronteggAuthService( } - override fun login(activity: Activity, loginHint: String?) { + override fun login( + activity: Activity, + loginHint: String?, + callback: (() -> Unit)?, + ) { if (isEmbeddedMode) { - EmbeddedAuthActivity.authenticate(activity, loginHint) + EmbeddedAuthActivity.authenticate(activity, loginHint, callback) } else { - AuthenticationActivity.authenticate(activity, loginHint) + AuthenticationActivity.authenticate(activity, loginHint, callback) } } - override fun logout(callback: () -> Unit) { + override fun logout( + callback: () -> Unit, + ) { isLoading.value = true refreshTokenManager.cancelLastTimer() @@ -127,7 +143,6 @@ class FronteggAuthService( } } - } @@ -145,7 +160,10 @@ class FronteggAuthService( } - override fun switchTenant(tenantId: String, callback: (Boolean) -> Unit) { + override fun switchTenant( + tenantId: String, + callback: (Boolean) -> Unit, + ) { Log.d(TAG, "switchTenant()") GlobalScope.launch(Dispatchers.IO) { val handler = Handler(Looper.getMainLooper()) @@ -183,6 +201,124 @@ class FronteggAuthService( } } + override fun loginWithPasskeys( + activity: Activity, + callback: ((error: Exception?) -> Unit)?, + ) { + + val passkeyManager = CredentialManagerHandler(activity) + val scope = MainScope() + scope.launch { + try { + + val webAuthnPreloginRequest = withContext(Dispatchers.IO) { api.webAuthnPrelogin() } + + val result = passkeyManager.getPasskey(webAuthnPreloginRequest.jsonChallenge) + + val challengeResponse = + (result.credential as PublicKeyCredential).authenticationResponseJson + + isLoading.value = true + val webAuthnPostLoginResponse = withContext(Dispatchers.IO) { + api.webAuthnPostlogin(webAuthnPreloginRequest.cookie, challengeResponse) + } + + Log.i(TAG, "Login with Passkeys succeeded, exchanging oauth token") + withContext(Dispatchers.IO) { + setCredentials( + webAuthnPostLoginResponse.access_token, + webAuthnPostLoginResponse.refresh_token + ) + } + + callback?.invoke(null) + } catch (e: Exception) { + Log.e(TAG, "failed to login with passkeys", e) + if (e is MfaRequiredException) { + startMultiFactorAuthenticator(activity, callback, e.mfaRequestData) + } else { + callback?.invoke(e) + } + } + } + } + + override fun registerPasskeys( + activity: Activity, + callback: ((error: Exception?) -> Unit)?, + ) { + + val passkeyManager = CredentialManagerHandler(activity) + val scope = MainScope() + scope.launch { + try { + + val webAuthnRegsiterRequest = withContext(Dispatchers.IO) { + api.getWebAuthnRegisterChallenge() + } + + val result = passkeyManager.createPasskey(webAuthnRegsiterRequest.jsonChallenge) + + val challengeResponse = result.registrationResponseJson + + withContext(Dispatchers.IO) { + api.verifyWebAuthnDevice(webAuthnRegsiterRequest.cookie, challengeResponse) + } + + callback?.invoke(null) + } catch (e: Exception) { + Log.e(TAG, "Failed to login with passkeys", e) + if (isWebAuthnRegisteredBeforeException(e)) { + callback?.invoke(WebAuthnAlreadyRegisteredInLocalDeviceException()) + } else { + callback?.invoke(e) + } + } + } + } + + private fun startMultiFactorAuthenticator( + activity: Activity, + callback: ((error: Exception?) -> Unit)?, + mfaRequestData: String + ) { + + val authCallback: () -> Unit = { + if (this.isAuthenticated.value) { + callback?.invoke(null) + } else { + val error = FailedToAuthenticateException(error = "Failed to authenticate with MFA") + callback?.invoke(error) + } + } + + val multiFactorStateBase64 = + Base64.encodeToString(mfaRequestData.toByteArray(Charsets.UTF_8), Base64.NO_WRAP) + val directLogin = mapOf( + "type" to "direct", + "data" to "$baseUrl/oauth/account/mfa-mobile-authenticator?state=$multiFactorStateBase64", + "additionalQueryParams" to mapOf( + "prompt" to "consent" + ) + ) + val jsonData = JSONObject(directLogin).toString().toByteArray(Charsets.UTF_8) + val loginDirectAction = Base64.encodeToString(jsonData, Base64.NO_WRAP) + + if (isEmbeddedMode) { + EmbeddedAuthActivity.authenticateWithMultiFactor( + activity, + loginDirectAction, + authCallback + ) + } else { + AuthenticationActivity.authenticateWithMultiFactor( + activity, + loginDirectAction, + authCallback + ) + } + + } fun reinitWithRegion() { this.initializeSubscriptions() @@ -230,7 +366,6 @@ class FronteggAuthService( activity: Activity? = null, callback: (() -> Unit)? = null, ): Boolean { - val codeVerifier = credentialManager.getCodeVerifier() val redirectUrl = Constants.oauthCallbackUrl(baseUrl) @@ -325,7 +460,6 @@ class FronteggAuthService( return } - refreshTokenManager.cancelLastTimer() if (accessToken == null) {