Skip to content

Commit

Permalink
FR-18338.3 Fixes after marge with master
Browse files Browse the repository at this point in the history
  • Loading branch information
“oleksii-minaiev” committed Nov 13, 2024
1 parent 7281351 commit 7964427
Show file tree
Hide file tree
Showing 3 changed files with 179 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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()
Expand Down
38 changes: 35 additions & 3 deletions android/src/main/java/com/frontegg/android/FronteggAuth.kt
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,21 @@ 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.
* [accessToken], [refreshToken], and [user] should be null, and [isAuthenticated]
* 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).
Expand All @@ -86,14 +92,40 @@ 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.
* @return true if taken was successfully refreshed. False otherwise.
*/
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 {

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -127,7 +143,6 @@ class FronteggAuthService(
}

}

}


Expand All @@ -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())
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -230,7 +366,6 @@ class FronteggAuthService(
activity: Activity? = null,
callback: (() -> Unit)? = null,
): Boolean {

val codeVerifier = credentialManager.getCodeVerifier()
val redirectUrl = Constants.oauthCallbackUrl(baseUrl)

Expand Down Expand Up @@ -325,7 +460,6 @@ class FronteggAuthService(
return
}


refreshTokenManager.cancelLastTimer()

if (accessToken == null) {
Expand Down

0 comments on commit 7964427

Please sign in to comment.