Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -157,4 +157,7 @@ interface AutofillFeature {

@Toggle.DefaultValue(defaultValue = DefaultFeatureValue.INTERNAL)
fun passkeySupport(): Toggle

@Toggle.DefaultValue(defaultValue = DefaultFeatureValue.TRUE)
fun canReAuthenticateGoogleLoginsAutomatically(): Toggle
}
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,23 @@ class AutofillStoredBackJavascriptInterface @Inject constructor(
callback?.noCredentialsAvailable(url)
}
} else {
callback?.onCredentialsAvailableToInject(url, finalCredentialList, triggerType)
notifyListenerThatCredentialsAvailableToInject(url, finalCredentialList, triggerType, request)
}
}

private suspend fun notifyListenerThatCredentialsAvailableToInject(
url: String,
finalCredentialList: List<LoginCredentials>,
triggerType: LoginTriggerType,
request: AutofillDataRequest,
) {
when (val currentCallback = callback) {
is InternalCallback -> {
currentCallback.onCredentialsAvailableToInjectWithReauth(url, finalCredentialList, triggerType, request.subType)
}
else -> {
currentCallback?.onCredentialsAvailableToInject(url, finalCredentialList, triggerType)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright (c) 2025 DuckDuckGo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.duckduckgo.autofill.impl

import com.duckduckgo.autofill.api.Callback
import com.duckduckgo.autofill.api.domain.app.LoginCredentials
import com.duckduckgo.autofill.api.domain.app.LoginTriggerType
import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillInputSubType

/**
* Internal extension of the public Callback interface for extensions that isn't required outside of this module
*/
interface InternalCallback : Callback {

/**
* Called when we've determined we have credentials we can offer to autofill for the user,
* with additional re-authentication context.
* * @param originalUrl The URL where autofill was requested
* @param credentials List of available stored credentials for this URL
* @param triggerType How this autofill request was triggered
* @param requestSubType The type of autofill request (USERNAME or PASSWORD)
*/
suspend fun onCredentialsAvailableToInjectWithReauth(
originalUrl: String,
credentials: List<LoginCredentials>,
triggerType: LoginTriggerType,
requestSubType: SupportedAutofillInputSubType,
) {
// Default implementation delegates to the public API
onCredentialsAvailableToInject(originalUrl, credentials, triggerType)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,19 @@ import com.duckduckgo.autofill.impl.importing.InBrowserImportPromo
import com.duckduckgo.autofill.impl.jsbridge.response.AvailableInputTypeCredentials
import com.duckduckgo.autofill.impl.sharedcreds.ShareableCredentials
import com.duckduckgo.autofill.impl.store.InternalAutofillStore
import com.duckduckgo.autofill.impl.store.ReAuthenticationDetails
import com.duckduckgo.autofill.impl.store.emptyReAuthenticationDetails
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.AppScope
import com.squareup.anvil.annotations.ContributesBinding
import javax.inject.Inject
import kotlinx.coroutines.withContext

interface AutofillAvailableInputTypesProvider {
suspend fun getTypes(url: String?): AvailableInputTypes
suspend fun getTypes(
url: String?,
reAuthenticationDetails: ReAuthenticationDetails = emptyReAuthenticationDetails(),
): AvailableInputTypes

data class AvailableInputTypes(
val username: Boolean = false,
Expand All @@ -51,16 +56,26 @@ class RealAutofillAvailableInputTypesProvider @Inject constructor(
private val dispatchers: DispatcherProvider,
) : AutofillAvailableInputTypesProvider {

override suspend fun getTypes(url: String?): AvailableInputTypes {
override suspend fun getTypes(
url: String?,
reAuthenticationDetails: ReAuthenticationDetails,
): AvailableInputTypes {
return withContext(dispatchers.io()) {
val availableInputTypeCredentials = determineIfCredentialsAvailable(url)
val credentialsAvailableOnThisPage = availableInputTypeCredentials.username || availableInputTypeCredentials.password
val reauthCredentials = determineIfReauthenticationDetailsAvailable(reAuthenticationDetails)

val finalCredentials = AvailableInputTypeCredentials(
username = availableInputTypeCredentials.username || reauthCredentials.username,
password = availableInputTypeCredentials.password || reauthCredentials.password,
)

val credentialsAvailableOnThisPage = finalCredentials.username || finalCredentials.password
val emailAvailable = determineIfEmailAvailable()
val importPromoAvailable = inBrowserPromo.canShowPromo(credentialsAvailableOnThisPage, url)

AvailableInputTypes(
username = availableInputTypeCredentials.username,
password = availableInputTypeCredentials.password,
username = finalCredentials.username,
password = finalCredentials.password,
email = emailAvailable,
credentialsImport = importPromoAvailable,
)
Expand All @@ -83,5 +98,15 @@ class RealAutofillAvailableInputTypesProvider @Inject constructor(
AvailableInputTypeCredentials(username = usernameSearch != null, password = passwordSearch != null)
}
}

private fun determineIfReauthenticationDetailsAvailable(reAuthenticationDetails: ReAuthenticationDetails): AvailableInputTypeCredentials {
val reauthPasswordAvailable = !reAuthenticationDetails.password.isNullOrEmpty()

return AvailableInputTypeCredentials(
username = false, // Re-authentication only provides passwords
password = reauthPasswordAvailable,
)
}

private fun determineIfEmailAvailable(): Boolean = emailManager.isSignedIn()
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import com.duckduckgo.autofill.api.AutofillCapabilityChecker
import com.duckduckgo.autofill.api.AutofillFeature
import com.duckduckgo.autofill.impl.email.incontext.availability.EmailProtectionInContextAvailabilityRules
import com.duckduckgo.autofill.impl.store.NeverSavedSiteRepository
import com.duckduckgo.autofill.impl.store.ReAuthenticationDetails
import com.duckduckgo.di.scopes.AppScope
import com.squareup.anvil.annotations.ContributesBinding
import javax.inject.Inject
Expand All @@ -30,6 +31,7 @@ interface AutofillRuntimeConfigProvider {
suspend fun getRuntimeConfiguration(
rawJs: String,
url: String?,
reAuthenticationDetails: ReAuthenticationDetails,
): String
}

Expand All @@ -46,6 +48,7 @@ class RealAutofillRuntimeConfigProvider @Inject constructor(
override suspend fun getRuntimeConfiguration(
rawJs: String,
url: String?,
reAuthenticationDetails: ReAuthenticationDetails,
): String {
logcat(VERBOSE) { "BrowserAutofill: getRuntimeConfiguration called" }

Expand All @@ -63,7 +66,7 @@ class RealAutofillRuntimeConfigProvider @Inject constructor(
).also {
logcat(VERBOSE) { "autofill-config: userPreferences for $url: \n$it" }
}
val availableInputTypes = generateAvailableInputTypes(url)
val availableInputTypes = generateAvailableInputTypes(url, reAuthenticationDetails)

return StringBuilder(rawJs).apply {
replacePlaceholder(this, TAG_INJECT_CONTENT_SCOPE, contentScope)
Expand All @@ -80,8 +83,8 @@ class RealAutofillRuntimeConfigProvider @Inject constructor(
}
}

private suspend fun generateAvailableInputTypes(url: String?): String {
val inputTypes = autofillAvailableInputTypesProvider.getTypes(url)
private suspend fun generateAvailableInputTypes(url: String?, reAuthenticationDetails: ReAuthenticationDetails): String {
val inputTypes = autofillAvailableInputTypesProvider.getTypes(url, reAuthenticationDetails)

val json = runtimeConfigurationWriter.generateResponseGetAvailableInputTypes(inputTypes).also {
logcat(VERBOSE) { "autofill-config: availableInputTypes for $url: \n$it" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,54 +15,12 @@
*/
package com.duckduckgo.autofill.impl.configuration

import android.webkit.WebView
import com.duckduckgo.app.di.AppCoroutineScope
import com.duckduckgo.autofill.api.AutofillCapabilityChecker
import com.duckduckgo.autofill.api.BrowserAutofill.Configurator
import com.duckduckgo.common.utils.DefaultDispatcherProvider
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.AppScope
import com.squareup.anvil.annotations.ContributesBinding
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import logcat.LogPriority.VERBOSE
import logcat.logcat

@ContributesBinding(AppScope::class)
class InlineBrowserAutofillConfigurator @Inject constructor(
private val autofillRuntimeConfigProvider: AutofillRuntimeConfigProvider,
@AppCoroutineScope private val coroutineScope: CoroutineScope,
private val dispatchers: DispatcherProvider = DefaultDispatcherProvider(),
private val autofillCapabilityChecker: AutofillCapabilityChecker,
private val autofillJavascriptLoader: AutofillJavascriptLoader,
) : Configurator {
override fun configureAutofillForCurrentPage(
webView: WebView,
url: String?,
) {
coroutineScope.launch(dispatchers.io()) {
if (canJsBeInjected(url)) {
logcat(VERBOSE) { "Injecting autofill JS into WebView for $url" }

val rawJs = autofillJavascriptLoader.getAutofillJavascript()
val formatted = autofillRuntimeConfigProvider.getRuntimeConfiguration(rawJs, url)

withContext(dispatchers.main()) {
webView.evaluateJavascript("javascript:$formatted", null)
}
} else {
logcat(VERBOSE) { "Won't inject autofill JS into WebView for: $url" }
}
}
}

private suspend fun canJsBeInjected(url: String?): Boolean {
url?.let {
// note, we don't check for autofillEnabledByUser here, as the user-facing preference doesn't cover email
return autofillCapabilityChecker.isAutofillEnabledByConfiguration(it)
}
return false
}
}
private val configurator: InternalBrowserAutofillConfigurator,
) : Configurator by configurator
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright (c) 2025 DuckDuckGo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.duckduckgo.autofill.impl.configuration

import android.webkit.WebView
import com.duckduckgo.autofill.api.BrowserAutofill.Configurator
import com.duckduckgo.autofill.impl.store.ReAuthenticationDetails

interface InternalBrowserAutofillConfigurator : Configurator {
/**
* Configures autofill for the current webpage with optional automatic re-authentication support.
* This should be called once per page load (e.g., onPageStarted())
*
* @param webView The WebView to configure
* @param url The URL of the current page
* @param reauthenticationDetails Whether to enable automatic re-authentication for this page
*/
fun configureAutofillForCurrentPage(
webView: WebView,
url: String?,
reauthenticationDetails: ReAuthenticationDetails,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright (c) 2025 DuckDuckGo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.duckduckgo.autofill.impl.configuration

import android.webkit.WebView
import com.duckduckgo.app.di.AppCoroutineScope
import com.duckduckgo.autofill.api.AutofillCapabilityChecker
import com.duckduckgo.autofill.impl.store.ReAuthenticationDetails
import com.duckduckgo.common.utils.DefaultDispatcherProvider
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.AppScope
import com.squareup.anvil.annotations.ContributesBinding
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import logcat.LogPriority.VERBOSE
import logcat.logcat

@ContributesBinding(AppScope::class)
class RealInlineBrowserAutofillConfigurator @Inject constructor(
private val autofillRuntimeConfigProvider: AutofillRuntimeConfigProvider,
@AppCoroutineScope private val coroutineScope: CoroutineScope,
private val dispatchers: DispatcherProvider = DefaultDispatcherProvider(),
private val autofillCapabilityChecker: AutofillCapabilityChecker,
private val autofillJavascriptLoader: AutofillJavascriptLoader,
) : InternalBrowserAutofillConfigurator {
override fun configureAutofillForCurrentPage(
webView: WebView,
url: String?,
) {
configureAutofillForCurrentPage(webView, url, ReAuthenticationDetails())
}

override fun configureAutofillForCurrentPage(
webView: WebView,
url: String?,
reauthenticationDetails: ReAuthenticationDetails,
) {
coroutineScope.launch(dispatchers.io()) {
if (canJsBeInjected(url)) {
val rawJs = autofillJavascriptLoader.getAutofillJavascript()
val formatted = autofillRuntimeConfigProvider.getRuntimeConfiguration(rawJs, url, reauthenticationDetails)

withContext(dispatchers.main()) {
webView.evaluateJavascript("javascript:$formatted", null)
}
} else {
logcat(VERBOSE) { "Won't inject autofill JS into WebView for: $url" }
}
}
}

private suspend fun canJsBeInjected(url: String?): Boolean {
url?.let {
// note, we don't check for autofillEnabledByUser here, as the user-facing preference doesn't cover email
return autofillCapabilityChecker.isAutofillEnabledByConfiguration(it)
}
return false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ class AutofillImportPasswordConfigStoreImpl @Inject constructor(

// order is important; first match wins so keep the most specific to start of the list
internal val URL_MAPPINGS_DEFAULT = listOf(
UrlMapping(key = "webflow-signin-rejected", url = "https://accounts.google.com/v3/signin/rejected"),
UrlMapping(key = "webflow-passphrase-encryption", url = "https://passwords.google.com/error/sync-passphrase"),
UrlMapping(key = "webflow-pre-login", url = "https://passwords.google.com/intro"),
UrlMapping(key = "webflow-export", url = "https://passwords.google.com/options?ep=1"),
Expand Down
Loading
Loading