From acdaa72408691f4d888eca56f9193b0d30193836 Mon Sep 17 00:00:00 2001 From: David Antoon Date: Wed, 2 Oct 2024 23:41:16 +0300 Subject: [PATCH 01/15] add support for passkeys --- android/build.gradle | 25 +- android/consumer-rules.pro | 10 +- android/src/main/AndroidManifest.xml | 2 + .../embedded/CredentialManagerHandler.kt | 69 +++++ .../android/embedded/FronteggWebClient.kt | 5 +- .../android/embedded/FronteggWebView.kt | 19 +- .../android/embedded/PasskeyWebListener.kt | 240 ++++++++++++++++++ app/build.gradle | 5 +- build.gradle | 6 +- embedded/build.gradle | 13 +- embedded/proguard-rules.pro | 2 +- gradle.properties | 1 + gradle/wrapper/gradle-wrapper.properties | 4 +- multi-region/build.gradle | 4 +- 14 files changed, 373 insertions(+), 32 deletions(-) create mode 100644 android/src/main/java/com/frontegg/android/embedded/CredentialManagerHandler.kt create mode 100644 android/src/main/java/com/frontegg/android/embedded/PasskeyWebListener.kt diff --git a/android/build.gradle b/android/build.gradle index eb16cbf..2a01c50 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -12,11 +12,11 @@ version '1.2.25' android { namespace 'com.frontegg.android' - compileSdk 34 + compileSdk 35 defaultConfig { minSdk 26 - targetSdk 34 + targetSdk 35 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" versionName "$version" @@ -35,7 +35,7 @@ android { kotlinOptions { jvmTarget = '1.8' } - buildToolsVersion '30.0.3' + buildToolsVersion '34.0.0' publishing { singleVariant("release") { @@ -51,16 +51,21 @@ dependencies { implementation 'io.reactivex.rxjava3:rxkotlin:3.0.1' implementation 'com.squareup.okhttp3:okhttp:4.12.0' implementation 'com.google.code.gson:gson:2.10.1' - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3" - implementation "androidx.browser:browser:1.8.0" - implementation("androidx.security:security-crypto:1.1.0-alpha06") { + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3' + implementation 'androidx.browser:browser:1.8.0' + implementation ('androidx.security:security-crypto:1.1.0-alpha06') { exclude group: 'com.google.crypto.tink', module: 'tink-android' } - implementation "com.google.crypto.tink:tink-android:1.9.0" + implementation 'com.google.crypto.tink:tink-android:1.9.0' implementation 'com.google.androidbrowserhelper:androidbrowserhelper:2.5.0' - implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" - implementation 'androidx.lifecycle:lifecycle-process:2.6.2' + implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' + implementation 'androidx.webkit:webkit:1.12.1' + implementation 'androidx.lifecycle:lifecycle-process:2.8.6' + implementation 'androidx.credentials:credentials:1.5.0-alpha05' + // optional - needed for credentials support from play services, for devices running + // Android 13 and below. + implementation "androidx.credentials:credentials-play-services-auth:1.5.0-alpha05" } afterEvaluate { diff --git a/android/consumer-rules.pro b/android/consumer-rules.pro index 4689556..d4f8671 100644 --- a/android/consumer-rules.pro +++ b/android/consumer-rules.pro @@ -20,4 +20,12 @@ -keep class com.frontegg.android.models.** { *; } # Retain Tink classes used for shared preferences encryption --keep class com.google.crypto.tink.** { *; } \ No newline at end of file +-keep class com.google.crypto.tink.** { *; } + +-if class androidx.credentials.CredentialManager +-keep class androidx.credentials.playservices.** { + *; +} + +-keep public class android.net.http.SslError +-keep public class android.webkit.WebViewClient diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 1dd27e5..93df43f 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -4,6 +4,8 @@ + + diff --git a/android/src/main/java/com/frontegg/android/embedded/CredentialManagerHandler.kt b/android/src/main/java/com/frontegg/android/embedded/CredentialManagerHandler.kt new file mode 100644 index 0000000..4e70af1 --- /dev/null +++ b/android/src/main/java/com/frontegg/android/embedded/CredentialManagerHandler.kt @@ -0,0 +1,69 @@ +package com.frontegg.android.embedded + + +import android.app.Activity +import android.util.Log +import androidx.credentials.* +import androidx.credentials.exceptions.* +import org.json.JSONObject + +/** + * A class that encapsulates the credential manager object and provides simplified APIs for + * creating and retrieving public key credentials. For other types of credentials follow the + * documentation https://developer.android.com/training/sign-in/passkeys + */ +class CredentialManagerHandler(private val activity: Activity) { + + private val mCredMan = CredentialManager.create(activity.applicationContext) + private val TAG = "CredentialManagerHandler" + + /** + * Encapsulates the create passkey API for credential manager in a less error-prone manner. + * + * @param request a create public key credential request JSON required by [CreatePublicKeyCredentialRequest]. + * @return [CreatePublicKeyCredentialResponse] containing the result of the credential creation. + */ + suspend fun createPasskey(requestStr: String): CreatePublicKeyCredentialResponse { + + val json = JSONObject(requestStr) + val authenticatorSelection = json.getJSONObject("authenticatorSelection"); + authenticatorSelection.put("residentKey", "preferred") + authenticatorSelection.put("userVerification", "required") + authenticatorSelection.put("authenticatorAttachment", "platform") + authenticatorSelection.put("requireResidentKey", false) + json.put("authenticatorSelection", authenticatorSelection) + val request = json.toString() + + val createRequest = CreatePublicKeyCredentialRequest(request, null, true) + try { + return mCredMan.createCredential( + activity, + createRequest + ) as CreatePublicKeyCredentialResponse + } catch (e: CreateCredentialException) { + // For error handling use guidance from https://developer.android.com/training/sign-in/passkeys + Log.i( + TAG, + "Error creating credential: ErrMessage: ${e.errorMessage}, ErrType: ${e.type}" + ) + throw e + } + } + + /** + * Encapsulates the get passkey API for credential manager in a less error-prone manner. + * + * @param request a get public key credential request JSON required by [GetCredentialRequest]. + * @return [GetCredentialResponse] containing the result of the credential retrieval. + */ + suspend fun getPasskey(request: String): GetCredentialResponse { + val getRequest = GetCredentialRequest(listOf(GetPublicKeyCredentialOption(request, null))) + try { + return mCredMan.getCredential(activity, getRequest) + } catch (e: GetCredentialException) { + // For error handling use guidance from https://developer.android.com/training/sign-in/passkeys + Log.i(TAG, "Error retrieving credential: ${e.message}") + throw e + } + } +} diff --git a/android/src/main/java/com/frontegg/android/embedded/FronteggWebClient.kt b/android/src/main/java/com/frontegg/android/embedded/FronteggWebClient.kt index a4d25f5..468ee6d 100644 --- a/android/src/main/java/com/frontegg/android/embedded/FronteggWebClient.kt +++ b/android/src/main/java/com/frontegg/android/embedded/FronteggWebClient.kt @@ -33,7 +33,7 @@ import okhttp3.Request import org.json.JSONObject -class FronteggWebClient(val context: Context) : WebViewClient() { +class FronteggWebClient(val context: Context, val passkeyWebListener: PasskeyWebListener) : WebViewClient() { companion object { private val TAG = FronteggWebClient::class.java.simpleName } @@ -45,6 +45,9 @@ class FronteggWebClient(val context: Context) : WebViewClient() { super.onPageStarted(view, url, favicon) Log.d(TAG, "onPageStarted $url") FronteggAuth.instance.isLoading.value = true + + passkeyWebListener.onPageStarted(); + view?.evaluateJavascript(PasskeyWebListener.INJECTED_VAL, null) } override fun onPageFinished(view: WebView?, url: String?) { diff --git a/android/src/main/java/com/frontegg/android/embedded/FronteggWebView.kt b/android/src/main/java/com/frontegg/android/embedded/FronteggWebView.kt index 2fd933a..e8cc648 100644 --- a/android/src/main/java/com/frontegg/android/embedded/FronteggWebView.kt +++ b/android/src/main/java/com/frontegg/android/embedded/FronteggWebView.kt @@ -1,15 +1,16 @@ package com.frontegg.android.embedded import android.annotation.SuppressLint +import android.app.Activity import android.content.Context import android.content.pm.PackageManager import android.util.AttributeSet import android.webkit.CookieManager import android.webkit.WebView +import androidx.webkit.WebViewCompat +import androidx.webkit.WebViewFeature import com.frontegg.android.FronteggApp -import com.frontegg.android.utils.AuthorizeUrlGenerator -import okhttp3.internal.userAgent -import java.util.* +import kotlinx.coroutines.MainScope open class FronteggWebView : WebView { @@ -60,10 +61,20 @@ open class FronteggWebView : WebView { settings.userAgentString = userAgent } - webViewClient = FronteggWebClient(context) + val scope = MainScope() + val credentialManagerHandler = CredentialManagerHandler(context as Activity) + val passkeyWebListener = PasskeyWebListener(context, scope, credentialManagerHandler) + + webViewClient = FronteggWebClient(context, passkeyWebListener) CookieManager.getInstance().setAcceptThirdPartyCookies(this, true) this.addJavascriptInterface(FronteggNativeBridge(context), "FronteggNativeBridge") + val rules = setOf("*") + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) { + WebViewCompat.addWebMessageListener( + this, PasskeyWebListener.INTERFACE_NAME, rules, passkeyWebListener + ) + } } diff --git a/android/src/main/java/com/frontegg/android/embedded/PasskeyWebListener.kt b/android/src/main/java/com/frontegg/android/embedded/PasskeyWebListener.kt new file mode 100644 index 0000000..8dbb4c4 --- /dev/null +++ b/android/src/main/java/com/frontegg/android/embedded/PasskeyWebListener.kt @@ -0,0 +1,240 @@ +package com.frontegg.android.embedded + +import android.app.Activity +import android.net.Uri +import android.util.Log +import android.webkit.WebView +import android.widget.Toast +import androidx.annotation.UiThread +import androidx.credentials.PublicKeyCredential +import androidx.credentials.exceptions.CreateCredentialException +import androidx.credentials.exceptions.GetCredentialException +import androidx.webkit.JavaScriptReplyProxy +import androidx.webkit.WebMessageCompat +import androidx.webkit.WebViewCompat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.json.JSONArray +import org.json.JSONObject + + +/** +This web listener looks for the 'postMessage()' call on the javascript web code, and when it +receives it, it will handle it in the manner dictated in this local codebase. This allows for +javascript on the web to interact with the local setup on device that contains more complex logic. + +The embedded javascript can be found in CredentialManagerWebView/javascript/encode.js. +It can be modified depending on the use case. If you wish to minify, please use the following command +to call the toptal minifier API. +``` +cat encode.js | grep -v '^let __webauthn_interface__;$' | \ +curl -X POST --data-urlencode input@- \ +https://www.toptal.com/developers/javascript-minifier/api/raw | tr '"' "'" | pbcopy +``` +pbpaste should output the proper minimized code. In linux, you may have to alias as follows: +``` +alias pbcopy='xclip -selection clipboard' +alias pbpaste='xclip -selection clipboard -o' +``` +in your bashrc. + */ +class PasskeyWebListener( + private val activity: Activity, + private val coroutineScope: CoroutineScope, + private val credentialManagerHandler: CredentialManagerHandler +) : WebViewCompat.WebMessageListener { + + /** havePendingRequest is true if there is an outstanding WebAuthn request. There is only ever + one request outstanding at a time.*/ + private var havePendingRequest = false + + /** pendingRequestIsDoomed is true if the WebView has navigated since starting a request. The + fido module cannot be cancelled, but the response will never be delivered in this case.*/ + private var pendingRequestIsDoomed = false + + /** replyChannel is the port that the page is listening for a response on. It + is valid if `havePendingRequest` is true.*/ + private var replyChannel: ReplyChannel? = null + + /** Called by the page when it wants to do a WebAuthn `get` or 'post' request. */ + @UiThread + override fun onPostMessage( + view: WebView, + message: WebMessageCompat, + sourceOrigin: Uri, + isMainFrame: Boolean, + replyProxy: JavaScriptReplyProxy, + ) { + Log.i(TAG, "In Post Message : $message source: $sourceOrigin"); + val messageData = message.data ?: return + onRequest(messageData, sourceOrigin, isMainFrame, JavaScriptReplyChannel(replyProxy)) + } + + private fun onRequest( + msg: String, + sourceOrigin: Uri, + isMainFrame: Boolean, + reply: ReplyChannel, + ) { + msg.let { + val jsonObj = JSONObject(msg); + val type = jsonObj.getString(TYPE_KEY) + val message = jsonObj.getString(REQUEST_KEY) + + if (havePendingRequest) { + postErrorMessage(reply, "request already in progress", type) + return + } + replyChannel = reply + if (!isMainFrame) { + reportFailure("requests from subframes are not supported", type) + return + } + + val originScheme = sourceOrigin.scheme + if (originScheme == null || originScheme.lowercase() != "https") { + reportFailure("WebAuthn not permitted for current URL", type) + return + } + + // Verify that origin belongs to your website, + // it's because the unknown origin may gain credential info. + // TODO: Implement this check +// if (isUnknownOrigin(originScheme)) { +// return +// } + + havePendingRequest = true + pendingRequestIsDoomed = false + + // Let’s use a temporary “replyCurrent” variable to send the data back, while resetting + // the main “replyChannel” variable to null so it’s ready for the next request. + val replyCurrent = replyChannel + if (replyCurrent == null) { + Log.i(TAG, "reply channel was null, cannot continue") + return; + } + + when (type) { + CREATE_UNIQUE_KEY -> + this.coroutineScope.launch { + handleCreateFlow(credentialManagerHandler, message, replyCurrent) + } + GET_UNIQUE_KEY -> this.coroutineScope.launch { + handleGetFlow(credentialManagerHandler, message, replyCurrent) + } + else -> Log.i(TAG, "Incorrect request json") + } + } + } + + // Handles the get flow in a less error-prone way + private suspend fun handleGetFlow( + credentialManagerHandler: CredentialManagerHandler, + message: String, + reply: ReplyChannel, + ) { + try { + havePendingRequest = false + pendingRequestIsDoomed = false + val r = credentialManagerHandler.getPasskey(message) + val successArray = ArrayList(); + successArray.add("success"); + successArray.add(JSONObject( + (r.credential as PublicKeyCredential).authenticationResponseJson)) + successArray.add(GET_UNIQUE_KEY); + reply.send(JSONArray(successArray).toString()) + replyChannel = null // setting initial replyChannel for next request given temp 'reply' + } catch (e: GetCredentialException) { + reportFailure("Error: ${e.errorMessage} w type: ${e.type} w obj: $e", GET_UNIQUE_KEY) + } catch (t: Throwable) { + reportFailure("Error: ${t.message}", GET_UNIQUE_KEY) + } + } + + // handles the create flow in a less error prone way + private suspend fun handleCreateFlow( + credentialManagerHandler: CredentialManagerHandler, + message: String, + reply: ReplyChannel, + ) { + try { + havePendingRequest = false + pendingRequestIsDoomed = false + val response = credentialManagerHandler.createPasskey(message) + val successArray = ArrayList(); + successArray.add("success"); + successArray.add(JSONObject(response.registrationResponseJson)); + successArray.add(CREATE_UNIQUE_KEY); + reply.send(JSONArray(successArray).toString()) + replyChannel = null // setting initial replyChannel for next request given temp 'reply' + } catch (e: CreateCredentialException) { + reportFailure("Error: ${e.errorMessage} w type: ${e.type} w obj: $e", + CREATE_UNIQUE_KEY) + } catch (t: Throwable) { + reportFailure("Error: ${t.message}", CREATE_UNIQUE_KEY) + } + } + + /** Invalidates any current request. */ + fun onPageStarted() { + if (havePendingRequest) { + pendingRequestIsDoomed = true + } + } + + /** Sends an error result to the page. */ + private fun reportFailure(message: String, type: String) { + havePendingRequest = false + pendingRequestIsDoomed = false + val reply: ReplyChannel = replyChannel!! // verifies non null by throwing NPE + replyChannel = null + postErrorMessage(reply, message, type) + } + + private fun postErrorMessage(reply: ReplyChannel, errorMessage: String, type: String) { + Log.i(TAG, "Sending error message back to the page via replyChannel $errorMessage"); + val array: MutableList = ArrayList() + array.add("error") + array.add(errorMessage) + array.add(type) + reply.send(JSONArray(array).toString()) + var toastMsg = errorMessage + Toast.makeText(this.activity.applicationContext, toastMsg, Toast.LENGTH_SHORT).show() + } + + private class JavaScriptReplyChannel(private val reply: JavaScriptReplyProxy) : + ReplyChannel { + override fun send(message: String?) { + try { + reply.postMessage(message!!) + }catch (t: Throwable) { + Log.i(TAG, "Reply failure due to: " + t.message); + } + } + } + + /** ReplyChannel is the interface over which replies to the embedded site are sent. This allows + for testing because AndroidX bans mocking its objects.*/ + interface ReplyChannel { + fun send(message: String?) + } + + companion object { + /** INTERFACE_NAME is the name of the MessagePort that must be injected into pages. */ + const val INTERFACE_NAME = "__webauthn_interface__" + + const val CREATE_UNIQUE_KEY = "create" + const val GET_UNIQUE_KEY = "get" + const val TYPE_KEY = "type" + const val REQUEST_KEY = "request" + + /** INJECTED_VAL is the minified version of the JavaScript code described at this class + * heading. The non minified form is found at credmanweb/javascript/encode.js.*/ + const val INJECTED_VAL = """ + var __webauthn_interface__,__webauthn_hooks__;!function(e){console.log("In the hook."),__webauthn_interface__.addEventListener("message",function e(n){var r=JSON.parse(n.data),t=r[2];"get"===t?o(r):"create"===t?u(r):console.log("Incorrect response format for reply")});var n=null,r=null,t=null,a=null;function o(e){if(null!==n&&null!==t){if("success"!=e[0]){var r=t;n=null,t=null,r(new DOMException(e[1],"NotAllowedError"));return}var a=i(e[1]),o=n;n=null,t=null,o(a)}}function l(e){var n=e.length%4;return Uint8Array.from(atob(e.replace(/-/g,"+").replace(/_/g,"/").padEnd(e.length+(0===n?0:4-n),"=")),function(e){return e.charCodeAt(0)}).buffer}function s(e){return btoa(Array.from(new Uint8Array(e),function(e){return String.fromCharCode(e)}).join("")).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+${'$'}/,"")}function u(e){if(null===r||null===a){console.log("Here: "+r+" and reject: "+a);return}if(console.log("Output back: "+e),"success"!=e[0]){var n=a;r=null,a=null,n(new DOMException(e[1],"NotAllowedError"));return}var t=i(e[1]),o=r;r=null,a=null,o(t)}function i(e){return console.log("Here is the response from credential manager: "+e),e.rawId=l(e.rawId),e.response.clientDataJSON=l(e.response.clientDataJSON),e.response.hasOwnProperty("attestationObject")&&(e.response.attestationObject=l(e.response.attestationObject)),e.response.hasOwnProperty("authenticatorData")&&(e.response.authenticatorData=l(e.response.authenticatorData)),e.response.hasOwnProperty("signature")&&(e.response.signature=l(e.response.signature)),e.response.hasOwnProperty("userHandle")&&(e.response.userHandle=l(e.response.userHandle)),e.getClientExtensionResults=function e(){return{}},e}e.create=function n(t){if(!("publicKey"in t))return e.originalCreateFunction(t);var o=new Promise(function(e,n){r=e,a=n}),l=t.publicKey;if(l.hasOwnProperty("challenge")){var u=s(l.challenge);l.challenge=u}if(l.hasOwnProperty("user")&&l.user.hasOwnProperty("id")){var i=s(l.user.id);l.user.id=i}var c=JSON.stringify({type:"create",request:l});return __webauthn_interface__.postMessage(c),o},e.get=function r(a){if(!("publicKey"in a))return e.originalGetFunction(a);var o=new Promise(function(e,r){n=e,t=r}),l=a.publicKey;if(l.hasOwnProperty("challenge")){var u=s(l.challenge);l.challenge=u}var i=JSON.stringify({type:"get",request:l});return __webauthn_interface__.postMessage(i),o},e.onReplyGet=o,e.CM_base64url_decode=l,e.CM_base64url_encode=s,e.onReplyCreate=u}(__webauthn_hooks__||(__webauthn_hooks__={})),__webauthn_hooks__.originalGetFunction=navigator.credentials.get,__webauthn_hooks__.originalCreateFunction=navigator.credentials.create,navigator.credentials.get=__webauthn_hooks__.get,navigator.credentials.create=__webauthn_hooks__.create,window.PublicKeyCredential=function(){},window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable=function(){return Promise.resolve(!1)}; + """ + const val TAG = "PasskeyWebListener" + } + +} \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 5564db3..6c95f55 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -9,12 +9,12 @@ def fronteggApplicationId = "16407b9a-5b6c-43de-9f58-6a1d1e0077f8" android { namespace 'com.frontegg.demo' - compileSdk 34 + compileSdk 35 defaultConfig { applicationId "com.frontegg.demo" minSdk 26 - targetSdk 33 + targetSdk 35 versionCode 1 versionName "1.0" @@ -46,6 +46,7 @@ android { } buildFeatures { viewBinding true + buildConfig true } buildToolsVersion buildToolsVersion diff --git a/build.gradle b/build.gradle index ba9ac0d..76a3d70 100644 --- a/build.gradle +++ b/build.gradle @@ -1,14 +1,14 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id 'com.android.application' version '7.4.2' apply false - id 'com.android.library' version '7.4.2' apply false + id 'com.android.application' version '8.5.2' apply false + id 'com.android.library' version '8.5.2' apply false id 'org.jetbrains.kotlin.android' version '1.9.10' apply false } ext { kotlinVersion = "1.7.10" - buildToolsVersion = "32.0.0" + buildToolsVersion = "35.0.0" androidxAnnotationVersion = "1.5.0" extTruthVersion = "1.6.0-alpha01" coreVersion = "1.6.0-alpha01" diff --git a/embedded/build.gradle b/embedded/build.gradle index d2bcd2a..736babb 100644 --- a/embedded/build.gradle +++ b/embedded/build.gradle @@ -46,6 +46,7 @@ android { } buildFeatures { viewBinding true + buildConfig true } buildToolsVersion buildToolsVersion @@ -56,14 +57,14 @@ dependencies { implementation 'androidx.core:core-ktx:1.10.0' implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'io.reactivex.rxjava3:rxkotlin:3.0.1' - implementation 'com.google.android.material:material:1.8.0' + implementation 'com.google.android.material:material:1.12.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' - implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3' - implementation 'androidx.navigation:navigation-ui-ktx:2.5.3' - implementation 'androidx.tracing:tracing:1.1.0' + implementation 'androidx.navigation:navigation-fragment-ktx:2.8.1' + implementation 'androidx.navigation:navigation-ui-ktx:2.8.1' + implementation 'androidx.tracing:tracing:1.2.0' implementation project(path: ':android') - implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.2' - implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2' + implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.8.6' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.6' implementation 'de.hdodenhof:circleimageview:3.1.0' implementation 'com.github.bumptech.glide:glide:4.12.0' diff --git a/embedded/proguard-rules.pro b/embedded/proguard-rules.pro index 481bb43..f1b4245 100644 --- a/embedded/proguard-rules.pro +++ b/embedded/proguard-rules.pro @@ -18,4 +18,4 @@ # If you keep the line number information, uncomment this to # hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file +#-renamesourcefileattribute SourceFile diff --git a/gradle.properties b/gradle.properties index 8ea0209..1ad7f01 100644 --- a/gradle.properties +++ b/gradle.properties @@ -22,3 +22,4 @@ kotlin.code.style=official # thereby reducing the size of the R class for that library android.nonTransitiveRClass=true +android.suppressUnsupportedCompileSdk=35 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index df250ba..8556058 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sat Jul 08 01:29:18 IDT 2023 +#Tue Oct 01 17:33:30 IDT 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/multi-region/build.gradle b/multi-region/build.gradle index 367ec05..b368e96 100644 --- a/multi-region/build.gradle +++ b/multi-region/build.gradle @@ -9,12 +9,12 @@ def fronteggClientId = "b6adfe4c-d695-4c04-b95f-3ec9fd0c6cca" android { namespace 'com.frontegg.demo' - compileSdk 34 + compileSdk 35 defaultConfig { applicationId "com.frontegg.demo" minSdk 26 - targetSdk 34 + targetSdk 35 versionCode 1 versionName "1.0" From dbe59d976ea4649e6dce855f19ef700a2af3d9af Mon Sep 17 00:00:00 2001 From: David Antoon Date: Wed, 2 Oct 2024 23:41:53 +0300 Subject: [PATCH 02/15] Reinitalize shared preference if maskerkey is corrupted --- .../android/services/CredentialManager.kt | 104 ++++++++++++++++-- 1 file changed, 93 insertions(+), 11 deletions(-) diff --git a/android/src/main/java/com/frontegg/android/services/CredentialManager.kt b/android/src/main/java/com/frontegg/android/services/CredentialManager.kt index 38c3519..ac17e36 100644 --- a/android/src/main/java/com/frontegg/android/services/CredentialManager.kt +++ b/android/src/main/java/com/frontegg/android/services/CredentialManager.kt @@ -3,32 +3,114 @@ package com.frontegg.android.services import android.annotation.SuppressLint import android.content.Context import android.content.SharedPreferences +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties import android.util.Log import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKey import com.frontegg.android.utils.CredentialKeys +import java.security.KeyStore +import javax.crypto.KeyGenerator +import javax.crypto.SecretKey -open class CredentialManager(context: Context) { + +open class CredentialManager(val context: Context) { companion object { private const val SHARED_PREFERENCES_NAME: String = "com.frontegg.services.CredentialManager" private val TAG = CredentialManager::class.java.simpleName } - private val sp: SharedPreferences; + private var sp: SharedPreferences; + private fun createKeyStore(): KeyStore { + val keyStore = KeyStore.getInstance("AndroidKeyStore") + keyStore.load(null) + keyStore.deleteEntry(MasterKey.DEFAULT_MASTER_KEY_ALIAS) + return keyStore + } + + fun clearSharedPreference(context: Context) { + context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE).edit().clear() + .apply() + } + + private fun createSecretKey(alias: String): SecretKey { + val keyGenerator = + KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore") + + keyGenerator.init( + KeyGenParameterSpec.Builder( + alias, + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT + ) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .build() + ) + + return keyGenerator.generateKey() + } + + private fun getSecretKey(keyStore: KeyStore, alias: String): SecretKey { + return if (keyStore.containsAlias(alias)) { + (keyStore.getEntry(alias, null) as KeyStore.SecretKeyEntry).secretKey + } else { + this.createSecretKey(alias) + } + } init { - val masterKey = MasterKey.Builder(context) - .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + + try { + val masterKey = MasterKey.Builder(context) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .build() + sp = EncryptedSharedPreferences.create( + context, + SHARED_PREFERENCES_NAME, + masterKey, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) + } catch (e: Exception) { + val cause: Throwable = e.cause!! + if (cause.message!!.contains("Signature/MAC verification failed")) { + Log.w(TAG, "Master key is corrupted. Recreating the master key") + // Recreate the Master Key + val masterKey = reinitializeMasterKey() + + // Recreate EncryptedSharedPreferences + sp = EncryptedSharedPreferences.create( + context, + SHARED_PREFERENCES_NAME, + masterKey, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) + } else { + // Handle other exceptions + throw e + } + } + } + + private fun reinitializeMasterKey(): MasterKey { + val keyStore = this.createKeyStore() + getSecretKey(keyStore, SHARED_PREFERENCES_NAME) + clearSharedPreference(this.context) + return MasterKey.Builder(context) + .setKeyGenParameterSpec( + KeyGenParameterSpec.Builder( + MasterKey.DEFAULT_MASTER_KEY_ALIAS, + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT + ) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .setKeySize(256) + .build() + ) .build() - sp = EncryptedSharedPreferences.create( - context, - SHARED_PREFERENCES_NAME, - masterKey, - EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, - EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM - ) } /** From 196e8c9888a2e201fb7e703515388fa4d1625cd7 Mon Sep 17 00:00:00 2001 From: David Antoon Date: Wed, 2 Oct 2024 23:48:09 +0300 Subject: [PATCH 03/15] upgrade to jdk 17 --- .github/actions/setup/action.yml | 8 +-- .github/old/build.yml | 99 -------------------------------- 2 files changed, 4 insertions(+), 103 deletions(-) delete mode 100644 .github/old/build.yml diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 84b6094..a21846e 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -20,19 +20,19 @@ runs: env: GITHUB_TOKEN: ${{ inputs.github_token }} - - name: Setup JDK 11 + - name: Setup JDK 17 uses: actions/setup-java@v3 if: ${{ inputs.gpg_signing_key == '' }} with: - java-version: 11 + java-version: 17 settings-path: ${{ github.workspace }} # location for the settings.xml file distribution: temurin - - name: Setup JDK 11 with Credentials + - name: Setup JDK 17 with Credentials if: ${{ inputs.gpg_signing_key != '' }} uses: actions/setup-java@v3 with: - java-version: 11 + java-version: 17 settings-path: ${{ github.workspace }} # location for the settings.xml file server-id: ossrh server-username: NEXUS_USERNAME diff --git a/.github/old/build.yml b/.github/old/build.yml deleted file mode 100644 index 3c6cc1b..0000000 --- a/.github/old/build.yml +++ /dev/null @@ -1,99 +0,0 @@ -name: Frontegg Android SDK - -on: - push: - branches-ignore: - - 'master' - - 'release/next' -env: - CI: true - LANG: en_US.UTF-8 - API_LEVEL: 29 - -concurrency: - group: ci-push-${{ github.ref }} - cancel-in-progress: true - -jobs: - build-and-test: - name: Build And Test - runs-on: macos-latest-xl - steps: - - name: Checkout - uses: actions/checkout@v3 -# - name: Clone Mock Server -# uses: actions/checkout@v3 -# with: -# repository: frontegg/frontegg-mock-server -# ssh-key: ${{ secrets.MOCK_SERVER_SSH_KEY }} -# ref: "master" -# path: mocker -# - name: Install Mock Server -# working-directory: mocker -# run: yarn install -# - name: Run Mock Server -# working-directory: mocker -# env: -# ANDROID_ASSOCIATED_DOMAIN_GRADLE_PATH: "${{ github.workspace }}/app/build.gradle" -# SERVER_HOSTNAME: "10.0.2.2" -# NGROCK_AUTH_TOKEN: "${{ secrets.NGROCK_AUTH_TOKEN }}" -# NGROCK_SUBDOMAIN: "frontegg-test" -# run: | -# echo "ANDROID_ASSOCIATED_DOMAIN_GRADLE_PATH: $ANDROID_ASSOCIATED_DOMAIN_GRADLE_PATH" -# echo "SERVER_HOSTNAME: $SERVER_HOSTNAME" -# echo "NGROCK_SUBDOMAIN: $NGROCK_SUBDOMAIN" -# (yarn start:mobile-mock&) -# sleep 40 - - - name: Set git config - run: | - git config --global user.name 'github-actions' - git config --global user.email 'github-actions@github.com' - - - name: Setup JDK 11 - uses: actions/setup-java@v3 - with: - java-version: 11 - settings-path: ${{ github.workspace }} # location for the settings.xml file - distribution: temurin - - - name: Gradle cache - uses: gradle/gradle-build-action@v2 - - - name: Build Libraries - run: ./gradlew :app:build --no-daemon -# - name: AVD cache -# uses: actions/cache@v3 -# id: avd-cache -# with: -# path: | -# ~/.android/avd/* -# ~/.android/adb* -# key: avd-${{env.API_LEVEL}} -# -# - name: Create AVD and generate snapshot for caching -# if: steps.avd-cache.outputs.cache-hit != 'true' -# uses: reactivecircus/android-emulator-runner@v2 -# with: -# avd-name: "AndroidEmulator" -# api-level: ${{env.API_LEVEL}} -# force-avd-creation: false -# emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -# disable-animations: true -# script: echo "Generated AVD snapshot for caching." - -# - name: Run tests -# uses: reactivecircus/android-emulator-runner@v2 -# with: -# api-level: ${{env.API_LEVEL}} -# force-avd-creation: false -# emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -# disable-animations: true -# script: ./run-tests.sh -# -# - name: Upload test results -# if: always() -# uses: actions/upload-artifact@v2 -# with: -# name: test-results -# path: app/build/reports/androidTests/connected/ From 7b80f72e5c4c08ecd3f81db2300b95346a4bc831 Mon Sep 17 00:00:00 2001 From: David Antoon Date: Thu, 3 Oct 2024 00:38:48 +0300 Subject: [PATCH 04/15] adjust compile sdk --- android/build.gradle | 11 +++++------ app/build.gradle | 4 ++-- multi-region/build.gradle | 4 ++-- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 2a01c50..6431110 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -12,11 +12,11 @@ version '1.2.25' android { namespace 'com.frontegg.android' - compileSdk 35 + compileSdk 34 defaultConfig { minSdk 26 - targetSdk 35 + targetSdk 34 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" versionName "$version" @@ -62,10 +62,9 @@ dependencies { implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation 'androidx.webkit:webkit:1.12.1' implementation 'androidx.lifecycle:lifecycle-process:2.8.6' - implementation 'androidx.credentials:credentials:1.5.0-alpha05' - // optional - needed for credentials support from play services, for devices running - // Android 13 and below. - implementation "androidx.credentials:credentials-play-services-auth:1.5.0-alpha05" + // optional - needed for credentials support from play services, for devices running Android 13 and below. + implementation "androidx.credentials:credentials-play-services-auth:1.3.0" + implementation 'androidx.credentials:credentials:1.3.0' } afterEvaluate { diff --git a/app/build.gradle b/app/build.gradle index 6c95f55..05fbe5c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -9,12 +9,12 @@ def fronteggApplicationId = "16407b9a-5b6c-43de-9f58-6a1d1e0077f8" android { namespace 'com.frontegg.demo' - compileSdk 35 + compileSdk 34 defaultConfig { applicationId "com.frontegg.demo" minSdk 26 - targetSdk 35 + targetSdk 34 versionCode 1 versionName "1.0" diff --git a/multi-region/build.gradle b/multi-region/build.gradle index b368e96..367ec05 100644 --- a/multi-region/build.gradle +++ b/multi-region/build.gradle @@ -9,12 +9,12 @@ def fronteggClientId = "b6adfe4c-d695-4c04-b95f-3ec9fd0c6cca" android { namespace 'com.frontegg.demo' - compileSdk 35 + compileSdk 34 defaultConfig { applicationId "com.frontegg.demo" minSdk 26 - targetSdk 35 + targetSdk 34 versionCode 1 versionName "1.0" From b1178d16e3269f10e72176eeb2f0aed89d0914d3 Mon Sep 17 00:00:00 2001 From: David Antoon Date: Thu, 3 Oct 2024 00:42:40 +0300 Subject: [PATCH 05/15] remove publish alpha on push --- .github/workflows/onPush.yml | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/.github/workflows/onPush.yml b/.github/workflows/onPush.yml index 0a3d9ff..af73d11 100644 --- a/.github/workflows/onPush.yml +++ b/.github/workflows/onPush.yml @@ -31,18 +31,3 @@ jobs: shell: bash run: ./gradlew :app:build --no-daemon - - name: Set Alpha Version - id: incremented-alpha-version - uses: ./.github/actions/update-gradle-version - with: - type: alpha - - - name: prepare release - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NEXUS_USERNAME: ${{ secrets.NEXUS_USERNAME }} - NEXUS_PASSWORD: ${{ secrets.NEXUS_PASSWORD }} - GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - GPG_PRIVATE_KEY: ${{ secrets.GPG_SIGNING_KEY }} - run: ./gradlew publish --no-daemon --no-parallel - From 017933600cfdbe8eefca5ce2822adf4c60a2f6ad Mon Sep 17 00:00:00 2001 From: David Antoon Date: Thu, 3 Oct 2024 02:48:31 +0300 Subject: [PATCH 06/15] test build for tests --- .github/test-jks/debug.keystore | Bin 0 -> 2603 bytes .github/test-jks/release.keystore | Bin 0 -> 2750 bytes .github/workflows/onPush.yml | 1 - .github/workflows/onTestWorkflow.yml | 40 +++++++++++++++++++++++++++ 4 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 .github/test-jks/debug.keystore create mode 100644 .github/test-jks/release.keystore create mode 100644 .github/workflows/onTestWorkflow.yml diff --git a/.github/test-jks/debug.keystore b/.github/test-jks/debug.keystore new file mode 100644 index 0000000000000000000000000000000000000000..f7db5baae370401ae4b07e3ccc680f2429fefb75 GIT binary patch literal 2603 zcma)6WmFT48eRgEhEW5gYaommQc{8pX#_+>dJH8bCbdZn8AG~0Kmm~&q0$mcgE$0H z!jDo)LL{bsO3US(d(Zcs`|I8x?|I($iTCG&BAt-|0l_E|Y=HrWBxA_CtUyL!F$wk_ zLV}H)(lita+WlV=v>rl&K0T$YPN$rK`9CR^GeA%=395fe>Y%*;PC!{v*H9Y&oGd7L z2;A_hkX0$xS?!r$+U+*^E!lL5x+xGyh64l?MX@k2{re*b1_hvm8DMT?3=j*30Fe+5 zUj{%Ou@oz&!V*|}zEaEwLV~<|S`wGbkxURyN|WRZPDwQkUQuSZfx7``XCmcbLrw;< zLscH(-SCD%>xkc@@dASy@_3)GnBoWy-bouKuD%hj(V(2XYF&55B|Rcp!fPwyYV`AV z^USJ6d+xzya=fBO09CAzm#Tecl&IZcr*y~xmBsD#lq!2zTFz<)q&l1L1~SY(2}vrP z36D_(KeetYQni^Nt-`E)0_ z*~I&NNFvX5Ejb!5Oki3SbP}5uxkNyEdDPyjML)ARon!EU5H?g9G1@GKWK>zWIcN*% z+nu)ZR**DB1my_Na|jw{O4AVU8kQTLo3HzD6X%P1@;@Pl)>PMg@9s*?gbie6@2mW% z@o7x4))XK9jm)IQ_|8#?(qvwp)?cRZ;VzBnBe4-JmCJEo1N!#>?Ph{Pic|hT;I}PI zzp@!OgC|;VX{$-mM_|~!fICn&eSM$>rElbJmeNJ}qtmw5Habvb|79>PM{YbVdYmmc zTiC{q?Kn;$*vWWPdT!gp)IZeZc=SYtub143zA{*p;=^)5P{X@3Xws&IRr+kN-~DCr zs)V=8ExjrvhFBs<9~5(YuFME}>|B6}DwcOPz~|y_Kxe66Tc8zD9#t#P^&2^HvG4R}2avvIPRW7a*V`*eW|v7yy7U&mgEzJ2EMu9%B;_mi!m*2PzS1B)N$ z3D}7Tl$ts*x;0iL$18q4Och>le~ocHD2T|Ar~L4NTUo;*<7a;1jWBAIeEzRg1|G&> zC{OArwQzCVn`CS>`&_HZHuqyV=WtMKJGB*OiwKp7;K|&RILUA*{+zGok0rdbaLK~& z1x{6fp#9{m=DbJUG<)(fGyO@x>luKzUzW~M%~Jihl|vJYkB4X zyG7XE?=5c=Ac?W?Vb6_{Sjo1GDea=|6er4CZI-Z)8?F4J%Epxi>2H7`ByIG z0UfTX9r0_}YP|~`E*U})mL2FXkoGU`Dp4RpatBf9dTiawqk7swu4l`#z32JVS_Rs^ zkh_21Vd0|3Zl`?>`-LEVidiS$vHz51Ro$COZVOd!aV??r8Kd*A&Me7wmL>7FLq%p* zG>QnaDOVp150VSE8{ZN#8=fuj?Yqp9r?+auyv6pLpY21%bxx_?<{it@eyNJXy6i?K zc9-<|%1oIjcaLH_P23n)Wh1%@=GKKbv-fF+FZyZJC`Pn<__+ltug-*#yV|gDx>n5I z>a}C0KG6;MP-C2G!P*z`M_!1;nnmla3sl6~>!>mU)Iq5coDV2?FC(5FRl(K#9hgKD0$M q_geOqg5F=II@jky;E~3#%AFi}Il+BNb%8Z|bl!7H8wd;}7yk=7wUlK5 literal 0 HcmV?d00001 diff --git a/.github/test-jks/release.keystore b/.github/test-jks/release.keystore new file mode 100644 index 0000000000000000000000000000000000000000..eaa1ec21ebd40db5ac6e9b383dcea56ac976bac5 GIT binary patch literal 2750 zcma);c{mh|_Qz+K8D772C@M1q+eVq+8u-1r|OFdak!mmT8L!zhIC{9DBf19D`Mz`BP}8+G%y0?dc9 zL23Nm@}d+$a0OInk zKo?FBPyr-xW7kmTa(TZWF5K#PzHC-7hy?0^_LIz5!L3|?>fc<`qSBphZCwrSZ zBbxh2RaYtbU`@avb)*1;<2N<35dl`!KJPmgl~=NEfb41O2tjub80b9p?ZgY_x8B{^ z7sd1mpz&bGsfV_qRQ!Pow-$A^2j$>Xy`v$`78N}`lyVeb`qUywDWFpRds+AEE2dHs z=F`1=3NB)f<7Tqdv1mOx8*%t#uUjtpyKz9GNQ!Ju9FX*2#r;(_Ln@wL9ix zprPBC#y`^`@p9IrS2!rp0%^LsWUnIqz`9UhXgvIny$O?`qyqlekRYSQmu62gEOJ-_ zD}>C0e*Nyk1A~`g*xTLpuh*2y+yQBmvz5(Mcinwkd8;R>*_(B0g!<8#Za}Y^g-!*j z$7y202jZX6Sr8Ujo*i-n7ron+m1jQIH{!_XT}SVDi><^j{Z-D`A!grN9$%*uM5T{J z_JHLhu{kSDa+P~ALeN$yv~4?U9W|Dv3f@VXLX2&GG4fUp;b!IR)mqoqR1@)@82^AH z9941-epG!BvwAZChXYv#yJGXqH0E(qW&6u_TnObuOj;3?wrp$Gpl4PlIr^@JJC|rL z8PtiF+C@Q*O;Z)l3~h<~#(jxNOXs>%Isi-JYHPYR>|UG{Zj+` zHBqS&0v~rnul}kLmm3#`WgV>F#lZJ9*<)j zQ_fbCgC~3u$`htYX9?>Utsw|dYC?poW@1G7iNN#PypNk*${xNOe^OFpTfiMKkq&LJ z{^>+BRAZ(1p;u0@=5&aX!=_Bd56TmXL4o(rfv-q1RqiUa*DMM~vp^<=P71%KqMR1U z_txuLw<62c`=qC2pMG$LBqv1N4(z*8Ri5xgV<0a)dx{}}%{E+V7d2Sy6q@m`-zw_6 zGA+X10vJbF<&1g#2nsC;S(UaB&=#_rS-oE}?i_8W>N5H1a~_YDcYd3(4837}HoiAv z8>0PGl|9FcP^Skn3f25u>RR7%K1=3rj1SMs^$!s;aU_ELW# z#kUtL1&3m70`M7DzIycZg}YxF?hC?(oww!hy{mBuNSdC?otuH4&9;hBTpO_Rn5#}W z5nZqgOBxaxdmjp0Xc`4J%dg>#Ww7CqO~rnr!H7b1XXLk6!rO4X^Vj%Z9Db#o79ae7;wVj89cOpoPEhKhq-Im17mNDuZ)*0H!#tA-;d!+6gt`m*Q6qxeV zKg`Xyk6LD`s79w1KsIC$@`kge8~H<-9xV@R+u*X-=ttnkuT)#QA$6EGd1%J_2C)X| z?jlmpBffq`ISFb$&b5+mu5-&~t^B68!l;j5jsrnem_7jE3;K&LX8mr_K%{`PG4nx| zXII<~+7gBSZI@6u`lKWh;0Pc9oB=)nFM!+sR~LW>z~zwo9_HJJK|~?`mK;OFK}WG} zo-Wd8b&R?u8gmA7RvoQ|B7wqwFL6P#NFbj>MBo4d4vXvG6X1U{Z;JmrLHms8LG-hN zU;<6*tx$0No&UwW)uj3hy)B4FlEd~QRQ6D3D2eksr1G3hV=`Rj-1Ax-YzMe&=*wJy zd0ni^?YIK*o%!?Yn(g4ZRsTJs1U-FNesL!SIC5(=O*IF+1GEKKNP0xP%Q#Uh@ z|F8Z4s=jwzge?AiLMUqn45eN!u5k`^<(UnEIsAiQ%=A15+Ywel#L1E&v|@F zD_x|BMz~5!j!${?^Q4yW`OQFus19#3fYK>p^;!Ce?dbawC*k;N&u%6xbqOiKAxA0B ziEVNX>6swdr2RO1<1xVcIU_m!v$Mw0Z-XP8mvxAii*COD5RzZ)BI070#%Zg0gN4Rz zVOmWb=IvI{d-bP0!_BZ1KKBc$bv#oWNM%`J3Hy+%Y1U4A+ra_%@l$JgZ_(Q1^p{>; zQTk1Hej2LLV6ND$8Kj2z<7=y-#|IZL)0YMv3+H%AOlD%EBTlx4^FUoh#?_p9^YiPC z6h$agLufm{v`yq5Z1|!F&k|Owok)>M>(4AmyoXJod~4_3`FOCG;UZQboV5%dm`bwf z)c%g&LAcOcqD{IY+V@ELo<4Pymg&&?qszpn$7U#}sYODpJaE#QCjI0RaZL%5`iVVR zh20m9UOFdq6x!A_cV2(tVNEx#xMc+5ttz+R9W`*1MdV26-B~THxc=_bCwFq)K2JqT zhG*cw@WFMX`PyT4OP^GqHGLcHP@Ad33CEh&6=o2nu4^OI105Qi&tWF1GRs~u6%--& zC{w05|BH66p6-&`D7-O0QUX2Mk(?KW;YIrBHs@E2?RFz)6W(wdn4R6x2u z>OJRq|EuKzijYb6^8`B)4I{HM{yo0tfmO5P*@aSBl_Oo+s~*6L{SR-*48%GdJZ`Xl zUM640$YvzJj_0E4{LaeQMPmHX9pcKs`%LYDZ^9+KuQfAXo&L--<+^FA)_AIqf~~d4 zm%RKfOo}JCdLiSIC^nUWlk*CMY1MTT_nlR4(a0|MNaRdzc}8vBIXP|WaEspgBKz!XGR|RxlqMct zQfk6tN{+O@(Ml-~CptM3&7QKq>z`%s?sl8NKb`e07OqQ~Y^Q@G85866{!4{hgd&r! zo2+fCTP?4!jQYpd)uX$d(((;$6$kEqPoxE2S<+bH8JohG$?GUta{UsdvN+wdqGiwI z9MMWkR6gS~OYvs#Bywg(D`i=2crM@B@YDA*GZ#wNrfi5j^eIWi-@CG+>UW`5-OKg; Date: Thu, 3 Oct 2024 02:48:39 +0300 Subject: [PATCH 07/15] add tests --- .github/workflows/onTestWorkflow.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/onTestWorkflow.yml b/.github/workflows/onTestWorkflow.yml index 617f909..3795827 100644 --- a/.github/workflows/onTestWorkflow.yml +++ b/.github/workflows/onTestWorkflow.yml @@ -33,8 +33,14 @@ jobs: - name: Build release test apk run: ./gradlew :embedded:assembleRelease -Pandroid.injected.signing.store.file="${{ github.workspace }}/.github/test-jks/release.keystore" -Pandroid.injected.signing.store.password="android" -Pandroid.injected.signing.key.alias="androidreleasekey" -Pandroid.injected.signing.key.password="android" - - name: Upload a Build Artifact + - name: Upload a Assemble Debug Artifact uses: actions/upload-artifact@v3.1.3 with: - name: dsTemplate.apk - path: app/build/outputs/apk/debug/app-debug.apk + name: test-debug.apk + path: embedded/build/outputs/apk/debug/embedded-debug.apk + + - name: Upload a Assemble Release Artifact + uses: actions/upload-artifact@v3.1.3 + with: + name: test-release.apk + path: embedded/build/outputs/apk/release/embedded-release.apk From 6c4da9a8777c366e86d339efe59f7cfedb355166 Mon Sep 17 00:00:00 2001 From: David Antoon Date: Thu, 3 Oct 2024 03:03:31 +0300 Subject: [PATCH 08/15] fix test name --- .github/workflows/onTestWorkflow.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/onTestWorkflow.yml b/.github/workflows/onTestWorkflow.yml index 3795827..4aa6f3f 100644 --- a/.github/workflows/onTestWorkflow.yml +++ b/.github/workflows/onTestWorkflow.yml @@ -8,7 +8,7 @@ env: API_LEVEL: 29 concurrency: - group: ci-publish-alpha-${{ github.ref }} + group: ci-e2e-test-${{ github.ref }} cancel-in-progress: true jobs: From 7b1db79ce67add27076935aca332fb65957a5810 Mon Sep 17 00:00:00 2001 From: David Antoon Date: Thu, 3 Oct 2024 03:06:47 +0300 Subject: [PATCH 09/15] upgrade github action scripts --- .github/workflows/onPublishAlpha.yml | 4 ++-- .github/workflows/onPullRequestMerged.yaml | 4 ++-- .github/workflows/onPush.yml | 4 ++-- .github/workflows/onReleaseMerged.yaml | 4 ++-- .github/workflows/onTestWorkflow.yml | 8 ++++---- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/onPublishAlpha.yml b/.github/workflows/onPublishAlpha.yml index 7da99b9..728b4d5 100644 --- a/.github/workflows/onPublishAlpha.yml +++ b/.github/workflows/onPublishAlpha.yml @@ -5,7 +5,7 @@ on: env: CI: true LANG: en_US.UTF-8 - API_LEVEL: 29 + API_LEVEL: 34 concurrency: group: ci-publish-alpha-${{ github.ref }} @@ -17,7 +17,7 @@ jobs: runs-on: macos-latest-xl steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4.2.0 with: fetch-depth: "0" diff --git a/.github/workflows/onPullRequestMerged.yaml b/.github/workflows/onPullRequestMerged.yaml index dad9e89..b1d5d17 100644 --- a/.github/workflows/onPullRequestMerged.yaml +++ b/.github/workflows/onPullRequestMerged.yaml @@ -7,7 +7,7 @@ on: env: CI: true LANG: en_US.UTF-8 - API_LEVEL: 29 + API_LEVEL: 34 jobs: createReleasePullRequest: @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4.2.0 - name: Setup uses: ./.github/actions/setup diff --git a/.github/workflows/onPush.yml b/.github/workflows/onPush.yml index c71cf08..1cab2ec 100644 --- a/.github/workflows/onPush.yml +++ b/.github/workflows/onPush.yml @@ -8,7 +8,7 @@ on: env: CI: true LANG: en_US.UTF-8 - API_LEVEL: 29 + API_LEVEL: 34 concurrency: group: ci-push-${{ github.ref }} @@ -20,7 +20,7 @@ jobs: runs-on: macos-latest-xl steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4.2.0 - name: Setup uses: ./.github/actions/setup diff --git a/.github/workflows/onReleaseMerged.yaml b/.github/workflows/onReleaseMerged.yaml index 2fe2cc0..64ea941 100644 --- a/.github/workflows/onReleaseMerged.yaml +++ b/.github/workflows/onReleaseMerged.yaml @@ -16,7 +16,7 @@ jobs: runs-on: macos-12 steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4.2.0 - name: Setup uses: ./.github/actions/setup @@ -93,7 +93,7 @@ jobs: - - uses: actions/github-script@0.8.0 + - uses: actions/github-script@v7 with: github-token: ${{secrets.GITHUB_TOKEN}} script: | diff --git a/.github/workflows/onTestWorkflow.yml b/.github/workflows/onTestWorkflow.yml index 4aa6f3f..85489d7 100644 --- a/.github/workflows/onTestWorkflow.yml +++ b/.github/workflows/onTestWorkflow.yml @@ -5,7 +5,7 @@ on: env: CI: true LANG: en_US.UTF-8 - API_LEVEL: 29 + API_LEVEL: 34 concurrency: group: ci-e2e-test-${{ github.ref }} @@ -17,7 +17,7 @@ jobs: runs-on: macos-latest-xl steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4.2.0 with: fetch-depth: "0" @@ -34,13 +34,13 @@ jobs: run: ./gradlew :embedded:assembleRelease -Pandroid.injected.signing.store.file="${{ github.workspace }}/.github/test-jks/release.keystore" -Pandroid.injected.signing.store.password="android" -Pandroid.injected.signing.key.alias="androidreleasekey" -Pandroid.injected.signing.key.password="android" - name: Upload a Assemble Debug Artifact - uses: actions/upload-artifact@v3.1.3 + uses: actions/upload-artifact@v4.4.0 with: name: test-debug.apk path: embedded/build/outputs/apk/debug/embedded-debug.apk - name: Upload a Assemble Release Artifact - uses: actions/upload-artifact@v3.1.3 + uses: actions/upload-artifact@v4.4.0 with: name: test-release.apk path: embedded/build/outputs/apk/release/embedded-release.apk From 53793be049ff1eb631c0d2b9d3e22c687412b3d0 Mon Sep 17 00:00:00 2001 From: David Antoon Date: Thu, 3 Oct 2024 03:36:08 +0300 Subject: [PATCH 10/15] modify test apks and add custom root certificate for test network intercept --- .github/workflows/onTestWorkflow.yml | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/.github/workflows/onTestWorkflow.yml b/.github/workflows/onTestWorkflow.yml index 85489d7..08c3d0e 100644 --- a/.github/workflows/onTestWorkflow.yml +++ b/.github/workflows/onTestWorkflow.yml @@ -27,11 +27,25 @@ jobs: github_token: ${{ secrets.GITHUB_TOKEN }} + - name: Config root certificate for testing + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const path = require('path'); + const securityConfig = `\n \n \n \n \n \n \n \n \n` + fs.mkdirSync(path.join(process.env.GITHUB_WORKSPACE, 'embedded/src/main/res/xml'), { recursive: true }); + fs.writeFileSync(path.join(process.env.GITHUB_WORKSPACE, 'embedded/src/main/res/xml/network_security_config.xml'), securityConfig, 'utf8'); + + let manifest = fs.readFileSync(path.join(process.env.GITHUB_WORKSPACE, 'embedded/src/main/AndroidManifest.xml'), 'utf8'); + manifest = manifest.replace(/ Date: Thu, 3 Oct 2024 03:37:07 +0300 Subject: [PATCH 11/15] clean before start building --- .github/workflows/onTestWorkflow.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/onTestWorkflow.yml b/.github/workflows/onTestWorkflow.yml index 08c3d0e..3bb0f10 100644 --- a/.github/workflows/onTestWorkflow.yml +++ b/.github/workflows/onTestWorkflow.yml @@ -41,11 +41,13 @@ jobs: manifest = manifest.replace(/ Date: Fri, 4 Oct 2024 18:59:15 +0300 Subject: [PATCH 12/15] add support for callback using standard login activity --- .../main/java/com/frontegg/android/EmbeddedAuthActivity.kt | 3 ++- android/src/main/java/com/frontegg/android/FronteggAuth.kt | 4 ++-- embedded/src/main/java/com/frontegg/demo/AuthFragment.kt | 4 +++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/android/src/main/java/com/frontegg/android/EmbeddedAuthActivity.kt b/android/src/main/java/com/frontegg/android/EmbeddedAuthActivity.kt index 758d71d..f37ff80 100644 --- a/android/src/main/java/com/frontegg/android/EmbeddedAuthActivity.kt +++ b/android/src/main/java/com/frontegg/android/EmbeddedAuthActivity.kt @@ -186,12 +186,13 @@ class EmbeddedAuthActivity : Activity() { var onAuthFinishedCallback: (() -> Unit)? = null // Store callback - fun authenticate(activity: Activity) { + fun authenticate(activity: Activity, callback: (() -> Unit)? = null) { val intent = Intent(activity, EmbeddedAuthActivity::class.java) val authorizeUri = AuthorizeUrlGenerator().generate() intent.putExtra(AUTH_LAUNCHED, true) intent.putExtra(AUTHORIZE_URI, authorizeUri.first) + onAuthFinishedCallback = callback activity.startActivityForResult(intent, OAUTH_LOGIN_REQUEST) } diff --git a/android/src/main/java/com/frontegg/android/FronteggAuth.kt b/android/src/main/java/com/frontegg/android/FronteggAuth.kt index 879d002..10608ea 100644 --- a/android/src/main/java/com/frontegg/android/FronteggAuth.kt +++ b/android/src/main/java/com/frontegg/android/FronteggAuth.kt @@ -347,9 +347,9 @@ class FronteggAuth( } - fun login(activity: Activity) { + fun login(activity: Activity, callback: (() -> Unit)? = null) { if (FronteggApp.getInstance().isEmbeddedMode) { - EmbeddedAuthActivity.authenticate(activity) + EmbeddedAuthActivity.authenticate(activity, callback) } else { AuthenticationActivity.authenticate(activity) } diff --git a/embedded/src/main/java/com/frontegg/demo/AuthFragment.kt b/embedded/src/main/java/com/frontegg/demo/AuthFragment.kt index 2c36c66..1ad1bfa 100644 --- a/embedded/src/main/java/com/frontegg/demo/AuthFragment.kt +++ b/embedded/src/main/java/com/frontegg/demo/AuthFragment.kt @@ -36,7 +36,9 @@ class AuthFragment : Fragment() { super.onViewCreated(view, savedInstanceState) binding.loginButton.setOnClickListener { - FronteggAuth.instance.login(requireActivity()) + FronteggAuth.instance.login(requireActivity()) { + Log.d("AuthFragment", "Login callback") + } } binding.googleLoginButton.setOnClickListener { From e557c09cbfeb1101acd3baca1bf452a561cac2b7 Mon Sep 17 00:00:00 2001 From: David Antoon Date: Thu, 10 Oct 2024 01:48:58 +0300 Subject: [PATCH 13/15] test e2e --- .github/workflows/onTestWorkflow.yml | 17 +++++++++++++++-- embedded/build.gradle | 18 ++++++++++++++++++ gradle.properties | 15 ++++++++++++++- 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/.github/workflows/onTestWorkflow.yml b/.github/workflows/onTestWorkflow.yml index 3bb0f10..b306caa 100644 --- a/.github/workflows/onTestWorkflow.yml +++ b/.github/workflows/onTestWorkflow.yml @@ -44,10 +44,10 @@ jobs: - name: Clean build run: ./gradlew clean - name: Build debug test apk - run: ./gradlew :embedded:assembleDebug -Pandroid.injected.signing.store.file="${{ github.workspace }}/.github/test-jks/debug.keystore" -Pandroid.injected.signing.store.password="android" -Pandroid.injected.signing.key.alias="androiddebugkey" -Pandroid.injected.signing.key.password="android" + run: ./gradlew :embedded:assembleDebug - name: Build release test apk - run: ./gradlew :embedded:assembleRelease -Pandroid.injected.signing.store.file="${{ github.workspace }}/.github/test-jks/release.keystore" -Pandroid.injected.signing.store.password="android" -Pandroid.injected.signing.key.alias="androidreleasekey" -Pandroid.injected.signing.key.password="android" + run: ./gradlew :embedded:assembleRelease - name: Upload a Assemble Debug Artifact uses: actions/upload-artifact@v4.4.0 @@ -60,3 +60,16 @@ jobs: with: name: test-release.apk path: embedded/build/outputs/apk/release/embedded-release.apk + + - name: Trigger Private Repo Workflow + run: | + # Retrieve artifact URLs + artifacts=$(curl -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + -H "Accept: application/vnd.github.v3+json" \ + https://api.github.com/repos/${{ github.repository }}/actions/artifacts \ + | jq -r '.artifacts[] | select(.name | startswith("android-apk")) | .archive_download_url') + + # Convert the artifact URLs to a JSON list + apk_urls=$(echo "$artifacts" | jq -Rsc 'split("\n") | map(select(length > 0))') + + echo "artifact urls: $apk_urls" \ No newline at end of file diff --git a/embedded/build.gradle b/embedded/build.gradle index 736babb..ddbcb10 100644 --- a/embedded/build.gradle +++ b/embedded/build.gradle @@ -30,11 +30,29 @@ android { } + signingConfigs { + release { + storeFile file(project.property("RELEASE_STORE_FILE")) + storePassword project.property("RELEASE_STORE_PASSWORD") + keyAlias project.property("RELEASE_KEY_ALIAS") + keyPassword project.property("RELEASE_KEY_PASSWORD") + } + debug { + storeFile file(project.property("DEBUG_STORE_FILE")) + storePassword project.property("DEBUG_STORE_PASSWORD") + keyAlias project.property("DEBUG_KEY_ALIAS") + keyPassword project.property("DEBUG_KEY_PASSWORD") + } + } buildTypes { release { minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + signingConfig signingConfigs.release + } + debug { + signingConfig signingConfigs.debug } } compileOptions { diff --git a/gradle.properties b/gradle.properties index 1ad7f01..e174f9e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,5 +21,18 @@ kotlin.code.style=official # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library android.nonTransitiveRClass=true +android.suppressUnsupportedCompileSdk=35 + + +# Test release gradle settings +RELEASE_STORE_FILE=../.github/test-jks/release.keystore +RELEASE_STORE_PASSWORD=android +RELEASE_KEY_ALIAS=androidreleasekey +RELEASE_KEY_PASSWORD=android + +# Test debug gradle settings +DEBUG_STORE_FILE=../.github/test-jks/debug.keystore +DEBUG_STORE_PASSWORD=android +DEBUG_KEY_ALIAS=androiddebugkey +DEBUG_KEY_PASSWORD=android -android.suppressUnsupportedCompileSdk=35 \ No newline at end of file From 5d01010d588792bc5836f88eca2011d670f50f04 Mon Sep 17 00:00:00 2001 From: David Antoon Date: Thu, 31 Oct 2024 04:17:43 +0200 Subject: [PATCH 14/15] fix error messages --- README.md | 6 ++-- .../android/embedded/FronteggNativeBridge.kt | 9 ++++-- .../android/embedded/PasskeyWebListener.kt | 32 ++++++++++++------- app/build.gradle | 2 +- embedded/build.gradle | 7 +++- multi-region/build.gradle | 7 ++-- multi-region/src/main/AndroidManifest.xml | 22 +++++++++---- .../src/main/java/com/frontegg/demo/App.kt | 6 ++-- .../res/layout/activity_region_selection.xml | 4 +-- 9 files changed, 61 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 783446b..b2dcb5b 100644 --- a/README.md +++ b/README.md @@ -374,13 +374,13 @@ class App : Application() { listOf( RegionConfig( "eu", - "auth.davidantoon.me", + "autheu.davidantoon.me", "b6adfe4c-d695-4c04-b95f-3ec9fd0c6cca" ), RegionConfig( "us", - "davidprod.frontegg.com", - "d7d07347-2c57-4450-8418-0ec7ee6e096b" + "authus.frontegg.com", + "6903cab0-9809-4a2e-97dd-b8c0f966c813" ) ), this diff --git a/android/src/main/java/com/frontegg/android/embedded/FronteggNativeBridge.kt b/android/src/main/java/com/frontegg/android/embedded/FronteggNativeBridge.kt index 96adcd7..2377a06 100644 --- a/android/src/main/java/com/frontegg/android/embedded/FronteggNativeBridge.kt +++ b/android/src/main/java/com/frontegg/android/embedded/FronteggNativeBridge.kt @@ -11,7 +11,6 @@ import androidx.browser.customtabs.CustomTabsIntent import com.frontegg.android.EmbeddedAuthActivity import com.frontegg.android.FronteggApp import com.frontegg.android.utils.AuthorizeUrlGenerator -import com.google.androidbrowserhelper.trusted.LauncherActivity import org.json.JSONException import org.json.JSONObject @@ -30,11 +29,15 @@ class FronteggNativeBridge(val context: Context) { companion object { - fun directLoginWithContext(context: Context, directLogin: Map, preserveCodeVerifier:Boolean) { + fun directLoginWithContext( + context: Context, + directLogin: Map, + preserveCodeVerifier: Boolean + ) { val generatedUrl = try { val jsonData = JSONObject(directLogin).toString().toByteArray(Charsets.UTF_8) - val jsonString = Base64.encodeToString(jsonData, Base64.NO_WRAP ) + val jsonString = Base64.encodeToString(jsonData, Base64.NO_WRAP) AuthorizeUrlGenerator().generate(null, jsonString, preserveCodeVerifier) } catch (e: JSONException) { AuthorizeUrlGenerator().generate(null, null, preserveCodeVerifier) diff --git a/android/src/main/java/com/frontegg/android/embedded/PasskeyWebListener.kt b/android/src/main/java/com/frontegg/android/embedded/PasskeyWebListener.kt index 8dbb4c4..08079e6 100644 --- a/android/src/main/java/com/frontegg/android/embedded/PasskeyWebListener.kt +++ b/android/src/main/java/com/frontegg/android/embedded/PasskeyWebListener.kt @@ -9,6 +9,7 @@ import androidx.annotation.UiThread import androidx.credentials.PublicKeyCredential import androidx.credentials.exceptions.CreateCredentialException import androidx.credentials.exceptions.GetCredentialException +import androidx.credentials.exceptions.NoCredentialException import androidx.webkit.JavaScriptReplyProxy import androidx.webkit.WebMessageCompat import androidx.webkit.WebViewCompat @@ -82,17 +83,18 @@ class PasskeyWebListener( val message = jsonObj.getString(REQUEST_KEY) if (havePendingRequest) { - postErrorMessage(reply, "request already in progress", type) + postErrorMessage(reply, "Request already in progress", type) return } replyChannel = reply if (!isMainFrame) { - reportFailure("requests from subframes are not supported", type) + reportFailure("Requests from SubFrames are not supported", type) return } val originScheme = sourceOrigin.scheme if (originScheme == null || originScheme.lowercase() != "https") { + val exception = Exception("requests from subframes are not supported") reportFailure("WebAuthn not permitted for current URL", type) return } @@ -146,9 +148,9 @@ class PasskeyWebListener( reply.send(JSONArray(successArray).toString()) replyChannel = null // setting initial replyChannel for next request given temp 'reply' } catch (e: GetCredentialException) { - reportFailure("Error: ${e.errorMessage} w type: ${e.type} w obj: $e", GET_UNIQUE_KEY) + reportFailure("Error: ${e.errorMessage} w type: ${e.type} w obj: $e", GET_UNIQUE_KEY, e) } catch (t: Throwable) { - reportFailure("Error: ${t.message}", GET_UNIQUE_KEY) + reportFailure("Error: ${t.message}", GET_UNIQUE_KEY, Exception(t)) } } @@ -169,10 +171,9 @@ class PasskeyWebListener( reply.send(JSONArray(successArray).toString()) replyChannel = null // setting initial replyChannel for next request given temp 'reply' } catch (e: CreateCredentialException) { - reportFailure("Error: ${e.errorMessage} w type: ${e.type} w obj: $e", - CREATE_UNIQUE_KEY) + reportFailure("Error: ${e.errorMessage} w type: ${e.type} w obj: $e", CREATE_UNIQUE_KEY, e) } catch (t: Throwable) { - reportFailure("Error: ${t.message}", CREATE_UNIQUE_KEY) + reportFailure("Error: ${t.message}", CREATE_UNIQUE_KEY, t as Exception) } } @@ -184,23 +185,30 @@ class PasskeyWebListener( } /** Sends an error result to the page. */ - private fun reportFailure(message: String, type: String) { + private fun reportFailure(message: String, type: String, e: Exception? = null) { havePendingRequest = false pendingRequestIsDoomed = false val reply: ReplyChannel = replyChannel!! // verifies non null by throwing NPE replyChannel = null - postErrorMessage(reply, message, type) + postErrorMessage(reply, message, type, e) } - private fun postErrorMessage(reply: ReplyChannel, errorMessage: String, type: String) { + private fun postErrorMessage(reply: ReplyChannel, errorMessage: String, type: String, e:Exception? = null) { Log.i(TAG, "Sending error message back to the page via replyChannel $errorMessage"); val array: MutableList = ArrayList() array.add("error") array.add(errorMessage) array.add(type) reply.send(JSONArray(array).toString()) - var toastMsg = errorMessage - Toast.makeText(this.activity.applicationContext, toastMsg, Toast.LENGTH_SHORT).show() + + if(e is NoCredentialException){ + Toast.makeText(this.activity.applicationContext, "No passkeys found. Try another sign-in option.", Toast.LENGTH_LONG).show() + }else if (e == null){ + Toast.makeText(this.activity.applicationContext, errorMessage, Toast.LENGTH_SHORT).show() + }else { + Toast.makeText(this.activity.applicationContext, "Unknown Error Occurred", Toast.LENGTH_SHORT).show() + + } } private class JavaScriptReplyChannel(private val reply: JavaScriptReplyProxy) : diff --git a/app/build.gradle b/app/build.gradle index 05fbe5c..3ae4575 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -3,7 +3,7 @@ plugins { id 'org.jetbrains.kotlin.android' } -def fronteggDomain = "auth.davidantoon.me" +def fronteggDomain = "autheu.davidantoon.me" def fronteggClientId = "b6adfe4c-d695-4c04-b95f-3ec9fd0c6cca" def fronteggApplicationId = "16407b9a-5b6c-43de-9f58-6a1d1e0077f8" diff --git a/embedded/build.gradle b/embedded/build.gradle index ddbcb10..07019ee 100644 --- a/embedded/build.gradle +++ b/embedded/build.gradle @@ -3,9 +3,14 @@ plugins { id 'org.jetbrains.kotlin.android' } -def fronteggDomain = "auth.davidantoon.me" + +def fronteggDomain = "autheu.davidantoon.me" def fronteggClientId = "b6adfe4c-d695-4c04-b95f-3ec9fd0c6cca" +//def fronteggDomain = "auth.davidantoon.me" +//def fronteggClientId = "04ae2174-d8d9-4a90-8bab-2548e210a508" + + android { namespace 'com.frontegg.demo' compileSdk 34 diff --git a/multi-region/build.gradle b/multi-region/build.gradle index 367ec05..ae4c8c9 100644 --- a/multi-region/build.gradle +++ b/multi-region/build.gradle @@ -4,9 +4,11 @@ plugins { } -def fronteggDomain = "auth.davidantoon.me" +def fronteggDomain = "autheu.davidantoon.me" def fronteggClientId = "b6adfe4c-d695-4c04-b95f-3ec9fd0c6cca" +def fronteggDomain2 = "authus.davidantoon.me" + android { namespace 'com.frontegg.demo' compileSdk 34 @@ -23,7 +25,8 @@ android { manifestPlaceholders = [ "package_name" : applicationId, "frontegg_domain" : fronteggDomain, - "frontegg_client_id": fronteggClientId + "frontegg_client_id": fronteggClientId, + "frontegg_domain_2" : fronteggDomain2, ] vectorDrawables { useSupportLibrary true diff --git a/multi-region/src/main/AndroidManifest.xml b/multi-region/src/main/AndroidManifest.xml index 12ac938..4902a40 100644 --- a/multi-region/src/main/AndroidManifest.xml +++ b/multi-region/src/main/AndroidManifest.xml @@ -41,16 +41,16 @@ @@ -66,11 +66,19 @@ + + + + + + + + diff --git a/multi-region/src/main/java/com/frontegg/demo/App.kt b/multi-region/src/main/java/com/frontegg/demo/App.kt index 8faff08..b0bf6e7 100644 --- a/multi-region/src/main/java/com/frontegg/demo/App.kt +++ b/multi-region/src/main/java/com/frontegg/demo/App.kt @@ -18,13 +18,13 @@ class App : Application() { listOf( RegionConfig( "eu", - "auth.davidantoon.me", + "autheu.davidantoon.me", "b6adfe4c-d695-4c04-b95f-3ec9fd0c6cca" ), RegionConfig( "us", - "davidprod.frontegg.com", - "d7d07347-2c57-4450-8418-0ec7ee6e096b" + "authus.davidantoon.me", + "6903cab0-9809-4a2e-97dd-b8c0f966c813" ) ), this diff --git a/multi-region/src/main/res/layout/activity_region_selection.xml b/multi-region/src/main/res/layout/activity_region_selection.xml index 47895b8..65571a2 100644 --- a/multi-region/src/main/res/layout/activity_region_selection.xml +++ b/multi-region/src/main/res/layout/activity_region_selection.xml @@ -26,7 +26,7 @@ @@ -51,7 +51,7 @@ From 126dd5e57eddf8457e0ad38bb805f25a5b25f755 Mon Sep 17 00:00:00 2001 From: David Antoon Date: Thu, 31 Oct 2024 18:09:54 +0200 Subject: [PATCH 15/15] format ccode --- .../frontegg/android/EmbeddedAuthActivity.kt | 8 ++- .../java/com/frontegg/android/FronteggAuth.kt | 22 +++++-- .../android/embedded/FronteggWebClient.kt | 4 +- .../android/embedded/FronteggWebView.kt | 1 - .../android/embedded/PasskeyWebListener.kt | 60 ++++++++++++++----- .../android/exceptions/FronteggException.kt | 9 ++- .../exceptions/KeyNotFoundException.kt | 3 +- .../android/services/RefreshTokenService.kt | 7 ++- .../frontegg/android/utils/ObservableValue.kt | 2 - 9 files changed, 82 insertions(+), 34 deletions(-) diff --git a/android/src/main/java/com/frontegg/android/EmbeddedAuthActivity.kt b/android/src/main/java/com/frontegg/android/EmbeddedAuthActivity.kt index 389003f..d293b24 100644 --- a/android/src/main/java/com/frontegg/android/EmbeddedAuthActivity.kt +++ b/android/src/main/java/com/frontegg/android/EmbeddedAuthActivity.kt @@ -186,10 +186,14 @@ class EmbeddedAuthActivity : Activity() { var onAuthFinishedCallback: (() -> Unit)? = null // Store callback - fun authenticate(activity: Activity, loginHint: String? = null, callback: (() -> Unit)? = null) { + fun authenticate( + activity: Activity, + loginHint: String? = null, + callback: (() -> Unit)? = null + ) { val intent = Intent(activity, EmbeddedAuthActivity::class.java) - val authorizeUri = AuthorizeUrlGenerator().generate(loginHint=loginHint) + val authorizeUri = AuthorizeUrlGenerator().generate(loginHint = loginHint) intent.putExtra(AUTH_LAUNCHED, true) intent.putExtra(AUTHORIZE_URI, authorizeUri.first) onAuthFinishedCallback = callback diff --git a/android/src/main/java/com/frontegg/android/FronteggAuth.kt b/android/src/main/java/com/frontegg/android/FronteggAuth.kt index a223372..839e693 100644 --- a/android/src/main/java/com/frontegg/android/FronteggAuth.kt +++ b/android/src/main/java/com/frontegg/android/FronteggAuth.kt @@ -230,11 +230,11 @@ class FronteggAuth( private fun cancelLastTimer() { Log.d(TAG, "Cancel Last Timer") - if(timerTask!= null){ + if (timerTask != null) { timerTask?.cancel() timerTask = null } - if(refreshTokenJob!= null) { + if (refreshTokenJob != null) { val context = FronteggApp.getInstance().context val jobScheduler = context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler @@ -245,15 +245,20 @@ class FronteggAuth( fun scheduleTimer(offset: Long) { FronteggApp.getInstance().lastJobStart = Instant.now().toEpochMilli() - if(FronteggApp.getInstance().appInForeground){ + if (FronteggApp.getInstance().appInForeground) { Log.d(TAG, "[foreground] Start Timer task (${offset} ms)") this.timerTask = Timer().schedule(offset) { - Log.d(TAG, "[foreground] Job started, (${Instant.now().toEpochMilli()-FronteggApp.getInstance().lastJobStart} ms)") + Log.d( + TAG, + "[foreground] Job started, (${ + Instant.now().toEpochMilli() - FronteggApp.getInstance().lastJobStart + } ms)" + ) refreshTokenIfNeeded() } - }else { + } else { Log.d(TAG, "[background] Start Job Scheduler task (${offset} ms)") val context = FronteggApp.getInstance().context val jobScheduler = @@ -355,7 +360,12 @@ class FronteggAuth( } } - fun directLoginAction(activity: Activity, type: String, data: String, callback: (() -> Unit)? = null) { + fun directLoginAction( + activity: Activity, + type: String, + data: String, + callback: (() -> Unit)? = null + ) { if (FronteggApp.getInstance().isEmbeddedMode) { EmbeddedAuthActivity.directLoginAction(activity, type, data, callback) } else { diff --git a/android/src/main/java/com/frontegg/android/embedded/FronteggWebClient.kt b/android/src/main/java/com/frontegg/android/embedded/FronteggWebClient.kt index 468ee6d..f0e9810 100644 --- a/android/src/main/java/com/frontegg/android/embedded/FronteggWebClient.kt +++ b/android/src/main/java/com/frontegg/android/embedded/FronteggWebClient.kt @@ -33,7 +33,8 @@ import okhttp3.Request import org.json.JSONObject -class FronteggWebClient(val context: Context, val passkeyWebListener: PasskeyWebListener) : WebViewClient() { +class FronteggWebClient(val context: Context, val passkeyWebListener: PasskeyWebListener) : + WebViewClient() { companion object { private val TAG = FronteggWebClient::class.java.simpleName } @@ -161,7 +162,6 @@ class FronteggWebClient(val context: Context, val passkeyWebListener: PasskeyWeb } - var json = JsonParser.parseString(text) while (!json.isJsonObject) { text = json.asString diff --git a/android/src/main/java/com/frontegg/android/embedded/FronteggWebView.kt b/android/src/main/java/com/frontegg/android/embedded/FronteggWebView.kt index e8cc648..a0b61d1 100644 --- a/android/src/main/java/com/frontegg/android/embedded/FronteggWebView.kt +++ b/android/src/main/java/com/frontegg/android/embedded/FronteggWebView.kt @@ -78,7 +78,6 @@ open class FronteggWebView : WebView { } - override fun onDetachedFromWindow() { super.onDetachedFromWindow() clearWebView() diff --git a/android/src/main/java/com/frontegg/android/embedded/PasskeyWebListener.kt b/android/src/main/java/com/frontegg/android/embedded/PasskeyWebListener.kt index 08079e6..4aec8be 100644 --- a/android/src/main/java/com/frontegg/android/embedded/PasskeyWebListener.kt +++ b/android/src/main/java/com/frontegg/android/embedded/PasskeyWebListener.kt @@ -13,6 +13,7 @@ import androidx.credentials.exceptions.NoCredentialException import androidx.webkit.JavaScriptReplyProxy import androidx.webkit.WebMessageCompat import androidx.webkit.WebViewCompat +import com.frontegg.android.FronteggAuth import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import org.json.JSONArray @@ -94,17 +95,16 @@ class PasskeyWebListener( val originScheme = sourceOrigin.scheme if (originScheme == null || originScheme.lowercase() != "https") { - val exception = Exception("requests from subframes are not supported") reportFailure("WebAuthn not permitted for current URL", type) return } // Verify that origin belongs to your website, // it's because the unknown origin may gain credential info. - // TODO: Implement this check -// if (isUnknownOrigin(originScheme)) { -// return -// } + if (isUnknownOrigin(sourceOrigin)) { + reportFailure("WebAuthn not permitted for current URL", type) + return + } havePendingRequest = true pendingRequestIsDoomed = false @@ -122,14 +122,21 @@ class PasskeyWebListener( this.coroutineScope.launch { handleCreateFlow(credentialManagerHandler, message, replyCurrent) } + GET_UNIQUE_KEY -> this.coroutineScope.launch { handleGetFlow(credentialManagerHandler, message, replyCurrent) } + else -> Log.i(TAG, "Incorrect request json") } } } + private fun isUnknownOrigin(sourceOrigin: Uri): Boolean { + val baseUrlHost = Uri.parse(FronteggAuth.instance.baseUrl).host + return sourceOrigin.host != baseUrlHost + } + // Handles the get flow in a less error-prone way private suspend fun handleGetFlow( credentialManagerHandler: CredentialManagerHandler, @@ -142,8 +149,11 @@ class PasskeyWebListener( val r = credentialManagerHandler.getPasskey(message) val successArray = ArrayList(); successArray.add("success"); - successArray.add(JSONObject( - (r.credential as PublicKeyCredential).authenticationResponseJson)) + successArray.add( + JSONObject( + (r.credential as PublicKeyCredential).authenticationResponseJson + ) + ) successArray.add(GET_UNIQUE_KEY); reply.send(JSONArray(successArray).toString()) replyChannel = null // setting initial replyChannel for next request given temp 'reply' @@ -171,7 +181,11 @@ class PasskeyWebListener( reply.send(JSONArray(successArray).toString()) replyChannel = null // setting initial replyChannel for next request given temp 'reply' } catch (e: CreateCredentialException) { - reportFailure("Error: ${e.errorMessage} w type: ${e.type} w obj: $e", CREATE_UNIQUE_KEY, e) + reportFailure( + "Error: ${e.errorMessage} w type: ${e.type} w obj: $e", + CREATE_UNIQUE_KEY, + e + ) } catch (t: Throwable) { reportFailure("Error: ${t.message}", CREATE_UNIQUE_KEY, t as Exception) } @@ -193,7 +207,12 @@ class PasskeyWebListener( postErrorMessage(reply, message, type, e) } - private fun postErrorMessage(reply: ReplyChannel, errorMessage: String, type: String, e:Exception? = null) { + private fun postErrorMessage( + reply: ReplyChannel, + errorMessage: String, + type: String, + e: Exception? = null + ) { Log.i(TAG, "Sending error message back to the page via replyChannel $errorMessage"); val array: MutableList = ArrayList() array.add("error") @@ -201,12 +220,21 @@ class PasskeyWebListener( array.add(type) reply.send(JSONArray(array).toString()) - if(e is NoCredentialException){ - Toast.makeText(this.activity.applicationContext, "No passkeys found. Try another sign-in option.", Toast.LENGTH_LONG).show() - }else if (e == null){ - Toast.makeText(this.activity.applicationContext, errorMessage, Toast.LENGTH_SHORT).show() - }else { - Toast.makeText(this.activity.applicationContext, "Unknown Error Occurred", Toast.LENGTH_SHORT).show() + if (e is NoCredentialException) { + Toast.makeText( + this.activity.applicationContext, + "No passkeys found. Try another sign-in option.", + Toast.LENGTH_LONG + ).show() + } else if (e == null) { + Toast.makeText(this.activity.applicationContext, errorMessage, Toast.LENGTH_SHORT) + .show() + } else { + Toast.makeText( + this.activity.applicationContext, + "Unknown Error Occurred", + Toast.LENGTH_SHORT + ).show() } } @@ -216,7 +244,7 @@ class PasskeyWebListener( override fun send(message: String?) { try { reply.postMessage(message!!) - }catch (t: Throwable) { + } catch (t: Throwable) { Log.i(TAG, "Reply failure due to: " + t.message); } } diff --git a/android/src/main/java/com/frontegg/android/exceptions/FronteggException.kt b/android/src/main/java/com/frontegg/android/exceptions/FronteggException.kt index 1d05c20..23d874e 100644 --- a/android/src/main/java/com/frontegg/android/exceptions/FronteggException.kt +++ b/android/src/main/java/com/frontegg/android/exceptions/FronteggException.kt @@ -5,8 +5,11 @@ public open class FronteggException(message: String, cause: Throwable? = null) : public companion object { public const val UNKNOWN_ERROR: String = "frontegg.error.unknown" - public const val FRONTEGG_APP_MUST_BE_INITIALIZED: String = "frontegg.error.app_must_be_initialized" - public const val FRONTEGG_DOMAIN_MUST_NOT_START_WITH_HTTPS: String = "frontegg.error.domain_must_not_start_with_https" - public const val KEY_NOT_FOUND_SHARED_PREFERENCES_ERROR: String = "frontegg.error.key_not_found_shared_preferences" + public const val FRONTEGG_APP_MUST_BE_INITIALIZED: String = + "frontegg.error.app_must_be_initialized" + public const val FRONTEGG_DOMAIN_MUST_NOT_START_WITH_HTTPS: String = + "frontegg.error.domain_must_not_start_with_https" + public const val KEY_NOT_FOUND_SHARED_PREFERENCES_ERROR: String = + "frontegg.error.key_not_found_shared_preferences" } } \ No newline at end of file diff --git a/android/src/main/java/com/frontegg/android/exceptions/KeyNotFoundException.kt b/android/src/main/java/com/frontegg/android/exceptions/KeyNotFoundException.kt index 31853af..d68862a 100644 --- a/android/src/main/java/com/frontegg/android/exceptions/KeyNotFoundException.kt +++ b/android/src/main/java/com/frontegg/android/exceptions/KeyNotFoundException.kt @@ -3,4 +3,5 @@ package com.frontegg.android.exceptions /** * Exception that represents key not found when trying to get value from Shared Preference */ -public class KeyNotFoundException(cause: Throwable) : FronteggException(KEY_NOT_FOUND_SHARED_PREFERENCES_ERROR, cause) +public class KeyNotFoundException(cause: Throwable) : + FronteggException(KEY_NOT_FOUND_SHARED_PREFERENCES_ERROR, cause) diff --git a/android/src/main/java/com/frontegg/android/services/RefreshTokenService.kt b/android/src/main/java/com/frontegg/android/services/RefreshTokenService.kt index 80f44fb..3ee9efa 100644 --- a/android/src/main/java/com/frontegg/android/services/RefreshTokenService.kt +++ b/android/src/main/java/com/frontegg/android/services/RefreshTokenService.kt @@ -16,7 +16,12 @@ class RefreshTokenService : JobService() { } override fun onStartJob(params: JobParameters?): Boolean { - Log.d(TAG, "Job started, (${Instant.now().toEpochMilli() - FronteggApp.getInstance().lastJobStart} ms)") + Log.d( + TAG, + "Job started, (${ + Instant.now().toEpochMilli() - FronteggApp.getInstance().lastJobStart + } ms)" + ) performBackgroundTask(params) return true } diff --git a/android/src/main/java/com/frontegg/android/utils/ObservableValue.kt b/android/src/main/java/com/frontegg/android/utils/ObservableValue.kt index 521edd6..ce6c668 100644 --- a/android/src/main/java/com/frontegg/android/utils/ObservableValue.kt +++ b/android/src/main/java/com/frontegg/android/utils/ObservableValue.kt @@ -1,7 +1,5 @@ package com.frontegg.android.utils -import android.os.Handler -import android.os.Looper import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.functions.Consumer import io.reactivex.rxjava3.subjects.PublishSubject