Skip to content

Commit

Permalink
Support reauth with biometric in latte
Browse files Browse the repository at this point in the history
  • Loading branch information
tung2744 committed Aug 30, 2023
1 parent e16a9f4 commit e78031f
Show file tree
Hide file tree
Showing 10 changed files with 169 additions and 20 deletions.
2 changes: 1 addition & 1 deletion sdk/src/main/java/com/oursky/authgear/AuthgearException.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ internal fun wrapException(e: Exception): Exception {

// CancelException
if (e is BiometricPromptAuthenticationException) {
if (e.errorCode == BiometricPrompt.ERROR_CANCELED || e.errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON || e.errorCode == BiometricPrompt.ERROR_USER_CANCELED) {
if (isBiometricCancelError(e.errorCode)) {
return CancelException(e)
}
}
Expand Down
7 changes: 7 additions & 0 deletions sdk/src/main/java/com/oursky/authgear/Biometric.kt
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,10 @@ internal fun buildPromptInfo(
}
return builder.build()
}

internal fun isBiometricCancelError(errorCode: Int): Boolean {
if (errorCode == BiometricPrompt.ERROR_CANCELED || errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON || errorCode == BiometricPrompt.ERROR_USER_CANCELED) {
return true
}
return false
}
12 changes: 12 additions & 0 deletions sdk/src/main/java/com/oursky/authgear/latte/BiometricOptions.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.oursky.authgear.latte

import androidx.fragment.app.FragmentActivity

data class BiometricOptions constructor(
var activity: FragmentActivity,
var title: String,
var subtitle: String? = null,
var description: String? = null,
var negativeButtonText: String? = null,
var allowedAuthenticators: Int = androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
)
5 changes: 5 additions & 0 deletions sdk/src/main/java/com/oursky/authgear/latte/Capability.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.oursky.authgear.latte

enum class Capability(val raw: String) {
BIOMETRIC("biometric")
}
127 changes: 118 additions & 9 deletions sdk/src/main/java/com/oursky/authgear/latte/Latte.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.Uri
import android.os.Handler
import android.os.Looper
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.fragment.app.Fragment
import com.oursky.authgear.*
import com.oursky.authgear.data.HttpClient
import com.oursky.authgear.latte.fragment.LatteFragment
import com.oursky.authgear.latte.fragment.LatteFragmentListener
import com.oursky.authgear.net.toQueryParameter
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.BufferOverflow
Expand Down Expand Up @@ -57,9 +62,26 @@ class Latte(
}

private suspend fun waitForResult(fragment: LatteFragment): LatteResult {
return waitForResult(fragment, null) { result, resumeWith ->
resumeWith(Result.success(result))
}
}

private suspend fun <T> waitForResult(
fragment: LatteFragment,
listener: LatteFragmentListener<T>?,
callback: (LatteResult, (Result<T>) -> Unit) -> Unit
): T {
val application = authgear.core.application
val result: LatteResult = suspendCoroutine<LatteResult> { k ->
val result: T = suspendCoroutine<T> { k ->
var isResumed = false
val resumeWith = fun(result: Result<T>) {
if (isResumed) {
return
}
isResumed = true
k.resumeWith(result)
}
val intentFilter = IntentFilter(fragment.latteID)
val br = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
Expand All @@ -81,12 +103,18 @@ class Latte(
if (isResumed) {
return
}
isResumed = true
val resultStr = intent.getStringExtra(LatteFragment.INTENT_KEY_RESULT) ?: return
val result = Json.decodeFromString<LatteResult>(resultStr)
k.resumeWith(Result.success(result))
try {
callback(result, resumeWith)
} catch (e: Throwable) {
resumeWith(Result.failure(e))
}
application.unregisterReceiver(this)
}
LatteFragment.BroadcastType.REAUTH_WITH_BIOMETRIC.name -> {
listener?.onReauthWithBiometric(resumeWith)
}
}
}
}
Expand Down Expand Up @@ -121,7 +149,7 @@ class Latte(
coroutineScope: CoroutineScope,
options: ReauthenticateOptions
): Pair<Fragment, LatteHandle<Boolean>> {
val request = authgear.createReauthenticateRequest(makeAuthgearReauthenticateOptions(options))
val request = authgear.createReauthenticateRequest(makeAuthgearReauthenticateOptions(context, options))
val fragment = LatteFragment.makeWithPreCreatedWebView(
context = context,
id = makeID(),
Expand All @@ -131,10 +159,77 @@ class Latte(
)
fragment.waitWebViewToLoad()

val listener = object : LatteFragmentListener<Boolean> {
override fun onReauthWithBiometric(resumeWith: (Result<Boolean>) -> Unit) {
val biometricOptions = options.biometricOptions ?: return
val builder = BiometricPrompt.PromptInfo.Builder()
.setTitle(biometricOptions.title)
.setAllowedAuthenticators(biometricOptions.allowedAuthenticators)
val subtitle = biometricOptions.subtitle
if (subtitle != null) {
builder.setSubtitle(subtitle)
}
val description = biometricOptions.description
if (description != null) {
builder.setSubtitle(description)
}
val negativeButtonText = biometricOptions.negativeButtonText
if (negativeButtonText != null) {
builder.setNegativeButtonText(negativeButtonText)
}
val promptInfo = builder.build()
val prompt =
BiometricPrompt(
biometricOptions.activity,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationFailed() {
// This callback will be invoked EVERY time the recognition failed.
// So while the prompt is still opened, this callback can be called repetitively.
// Finally, either onAuthenticationError or onAuthenticationSucceeded will be called.
// So this callback is not important to the developer.
}

override fun onAuthenticationError(
errorCode: Int,
errString: CharSequence
) {
if (isBiometricCancelError(errorCode)) {
return
}
resumeWith(
Result.failure(
wrapException(BiometricPromptAuthenticationException(
errorCode
))
)
)
}

override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
resumeWith(Result.success(true))
}
})

val handler = Handler(Looper.getMainLooper())
handler.post {
prompt.authenticate(promptInfo)
}
}
}

val d = coroutineScope.async {
val result = waitForResult(fragment)
authgear.finishAuthentication(result.getOrThrow().toString(), request)
true
val result = waitForResult<Boolean>(fragment, listener) { r, resumeWith ->
val uri = r.getOrThrow()
coroutineScope.launch {
try {
authgear.finishAuthentication(uri.toString(), request)
resumeWith(Result.success(true))
} catch (e: Throwable) {
resumeWith(Result.failure(e))
}
}
}
result
}

val handle = LatteHandle(fragment.latteID, d)
Expand Down Expand Up @@ -391,8 +486,22 @@ class Latte(
)
}

private suspend fun makeAuthgearReauthenticateOptions(latteOptions: ReauthenticateOptions): com.oursky.authgear.ReauthentcateOptions {
val finalXState = makeXStateWithSecrets(latteOptions.xState, latteOptions.xSecrets)
private suspend fun makeAuthgearReauthenticateOptions(context: Context, latteOptions: ReauthenticateOptions): com.oursky.authgear.ReauthentcateOptions {
val reauthXState = HashMap(latteOptions.xState)
reauthXState["user_initiate"] = "reauth"
val capabilities = mutableListOf<Capability>()

val biometricOptions = latteOptions.biometricOptions
if (biometricOptions != null) {
val result = BiometricManager.from(context).canAuthenticate(biometricOptions.allowedAuthenticators)
if (result == BiometricManager.BIOMETRIC_SUCCESS) {
capabilities.add(Capability.BIOMETRIC)
}
}

reauthXState["capabilities"] = capabilities.joinToString(separator = ",") { it.raw }

val finalXState = makeXStateWithSecrets(reauthXState, latteOptions.xSecrets)

return ReauthentcateOptions(
xState = finalXState.toQueryParameter(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,6 @@ package com.oursky.authgear.latte
data class ReauthenticateOptions @JvmOverloads constructor(
var xSecrets: Map<String, String> = mapOf(),
var xState: Map<String, String> = mapOf(),
var uiLocales: List<String>? = null
) {
init {
val newXState = HashMap(xState)
newXState["user_initiate"] = "reauth"
xState = newXState
}
}
var uiLocales: List<String>? = null,
var biometricOptions: BiometricOptions? = null
)
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ package com.oursky.authgear.latte
internal sealed class WebViewEvent {
object OpenEmailClient : WebViewEvent()
object OpenSMSClient : WebViewEvent()
object ReauthWithBiometric : WebViewEvent()
data class Tracking(val event: LatteTrackingEvent) : WebViewEvent()
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ internal class WebViewJSInterface(private val webView: WebView) {
OPEN_EMAIL_CLIENT("openEmailClient"),
OPEN_SMS_CLIENT("openSMSClient"),
TRACKING("tracking"),
READY("ready")
READY("ready"),
REAUTH_WITH_BIOMETRIC("reauthWithBiometric")
}

@JavascriptInterface
Expand All @@ -49,6 +50,9 @@ internal class WebViewJSInterface(private val webView: WebView) {
BuiltInEvent.READY -> {
this.webView.listener?.onReady(this.webView)
}
BuiltInEvent.REAUTH_WITH_BIOMETRIC -> {
this.webView.listener?.onEvent(WebViewEvent.ReauthWithBiometric)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ internal class LatteFragment() : Fragment() {
COMPLETE,
OPEN_EMAIL_CLIENT,
OPEN_SMS_CLIENT,
TRACKING
TRACKING,
REAUTH_WITH_BIOMETRIC
}

val latteID: String
Expand Down Expand Up @@ -94,6 +95,9 @@ internal class LatteFragment() : Fragment() {
is WebViewEvent.Tracking -> {
fragment.broadcastTrackingIntent(event.event)
}
is WebViewEvent.ReauthWithBiometric -> {
fragment.broadcastOnReauthWithBiometricIntent()
}
}
}

Expand Down Expand Up @@ -162,6 +166,13 @@ internal class LatteFragment() : Fragment() {
ctx.sendOrderedBroadcast(broadcastIntent, null)
}

private fun broadcastOnReauthWithBiometricIntent() {
val ctx = context ?: return
val broadcastIntent = Intent(latteID)
broadcastIntent.putExtra(INTENT_KEY_TYPE, BroadcastType.REAUTH_WITH_BIOMETRIC.toString())
ctx.sendOrderedBroadcast(broadcastIntent, null)
}

private fun broadcastTrackingIntent(event: LatteTrackingEvent) {
val ctx = context ?: return
val broadcastIntent = Intent(latteID)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.oursky.authgear.latte.fragment

interface LatteFragmentListener<T> {
fun onReauthWithBiometric(resumeWith: (Result<T>) -> Unit) {}
}

0 comments on commit e78031f

Please sign in to comment.