Skip to content

Commit d56fd53

Browse files
committed
Streamline importing passwords from Google
1 parent faad679 commit d56fd53

18 files changed

+752
-78
lines changed

autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/AutofillJavascriptInterface.kt

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,23 @@ class AutofillStoredBackJavascriptInterface @Inject constructor(
262262
callback?.noCredentialsAvailable(url)
263263
}
264264
} else {
265-
callback?.onCredentialsAvailableToInject(url, finalCredentialList, triggerType)
265+
notifyListenerThatCredentialsAvailableToInject(url, finalCredentialList, triggerType, request)
266+
}
267+
}
268+
269+
private suspend fun notifyListenerThatCredentialsAvailableToInject(
270+
url: String,
271+
finalCredentialList: List<LoginCredentials>,
272+
triggerType: LoginTriggerType,
273+
request: AutofillDataRequest,
274+
) {
275+
when (val currentCallback = callback) {
276+
is InternalCallback -> {
277+
currentCallback.onCredentialsAvailableToInjectWithReauth(url, finalCredentialList, triggerType, request.subType)
278+
}
279+
else -> {
280+
currentCallback?.onCredentialsAvailableToInject(url, finalCredentialList, triggerType)
281+
}
266282
}
267283
}
268284

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright (c) 2025 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.autofill.impl
18+
19+
import com.duckduckgo.autofill.api.Callback
20+
import com.duckduckgo.autofill.api.domain.app.LoginCredentials
21+
import com.duckduckgo.autofill.api.domain.app.LoginTriggerType
22+
import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillInputSubType
23+
24+
/**
25+
* Internal extension of the public Callback interface for extensions that isn't required outside of this module
26+
*/
27+
interface InternalCallback : Callback {
28+
29+
/**
30+
* Called when we've determined we have credentials we can offer to autofill for the user,
31+
* with additional re-authentication context.
32+
* * @param originalUrl The URL where autofill was requested
33+
* @param credentials List of available stored credentials for this URL
34+
* @param triggerType How this autofill request was triggered
35+
* @param requestSubType The type of autofill request (USERNAME or PASSWORD)
36+
*/
37+
suspend fun onCredentialsAvailableToInjectWithReauth(
38+
originalUrl: String,
39+
credentials: List<LoginCredentials>,
40+
triggerType: LoginTriggerType,
41+
requestSubType: SupportedAutofillInputSubType,
42+
) {
43+
// Default implementation delegates to the public API
44+
onCredentialsAvailableToInject(originalUrl, credentials, triggerType)
45+
}
46+
}

autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/AutofillAvailableInputTypesProvider.kt

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,18 @@ import com.duckduckgo.autofill.impl.importing.InBrowserImportPromo
2424
import com.duckduckgo.autofill.impl.jsbridge.response.AvailableInputTypeCredentials
2525
import com.duckduckgo.autofill.impl.sharedcreds.ShareableCredentials
2626
import com.duckduckgo.autofill.impl.store.InternalAutofillStore
27+
import com.duckduckgo.autofill.impl.store.ReAuthenticationDetails
2728
import com.duckduckgo.common.utils.DispatcherProvider
2829
import com.duckduckgo.di.scopes.AppScope
2930
import com.squareup.anvil.annotations.ContributesBinding
3031
import javax.inject.Inject
3132
import kotlinx.coroutines.withContext
3233

3334
interface AutofillAvailableInputTypesProvider {
34-
suspend fun getTypes(url: String?): AvailableInputTypes
35+
suspend fun getTypes(
36+
url: String?,
37+
reAuthenticationDetails: ReAuthenticationDetails = ReAuthenticationDetails(),
38+
): AvailableInputTypes
3539

3640
data class AvailableInputTypes(
3741
val username: Boolean = false,
@@ -51,16 +55,26 @@ class RealAutofillAvailableInputTypesProvider @Inject constructor(
5155
private val dispatchers: DispatcherProvider,
5256
) : AutofillAvailableInputTypesProvider {
5357

54-
override suspend fun getTypes(url: String?): AvailableInputTypes {
58+
override suspend fun getTypes(
59+
url: String?,
60+
reAuthenticationDetails: ReAuthenticationDetails,
61+
): AvailableInputTypes {
5562
return withContext(dispatchers.io()) {
5663
val availableInputTypeCredentials = determineIfCredentialsAvailable(url)
57-
val credentialsAvailableOnThisPage = availableInputTypeCredentials.username || availableInputTypeCredentials.password
64+
val reauthCredentials = determineIfReauthenticationDetailsAvailable(reAuthenticationDetails)
65+
66+
val finalCredentials = AvailableInputTypeCredentials(
67+
username = availableInputTypeCredentials.username || reauthCredentials.username,
68+
password = availableInputTypeCredentials.password || reauthCredentials.password,
69+
)
70+
71+
val credentialsAvailableOnThisPage = finalCredentials.username || finalCredentials.password
5872
val emailAvailable = determineIfEmailAvailable()
5973
val importPromoAvailable = inBrowserPromo.canShowPromo(credentialsAvailableOnThisPage, url)
6074

6175
AvailableInputTypes(
62-
username = availableInputTypeCredentials.username,
63-
password = availableInputTypeCredentials.password,
76+
username = finalCredentials.username,
77+
password = finalCredentials.password,
6478
email = emailAvailable,
6579
credentialsImport = importPromoAvailable,
6680
)
@@ -83,5 +97,15 @@ class RealAutofillAvailableInputTypesProvider @Inject constructor(
8397
AvailableInputTypeCredentials(username = usernameSearch != null, password = passwordSearch != null)
8498
}
8599
}
100+
101+
private fun determineIfReauthenticationDetailsAvailable(reAuthenticationDetails: ReAuthenticationDetails): AvailableInputTypeCredentials {
102+
val reauthPasswordAvailable = !reAuthenticationDetails.password.isNullOrEmpty()
103+
104+
return AvailableInputTypeCredentials(
105+
username = false, // Re-authentication only provides passwords
106+
password = reauthPasswordAvailable,
107+
)
108+
}
109+
86110
private fun determineIfEmailAvailable(): Boolean = emailManager.isSignedIn()
87111
}

autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/AutofillRuntimeConfigProvider.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import com.duckduckgo.autofill.api.AutofillCapabilityChecker
2020
import com.duckduckgo.autofill.api.AutofillFeature
2121
import com.duckduckgo.autofill.impl.email.incontext.availability.EmailProtectionInContextAvailabilityRules
2222
import com.duckduckgo.autofill.impl.store.NeverSavedSiteRepository
23+
import com.duckduckgo.autofill.impl.store.ReAuthenticationDetails
2324
import com.duckduckgo.di.scopes.AppScope
2425
import com.squareup.anvil.annotations.ContributesBinding
2526
import javax.inject.Inject
@@ -30,6 +31,7 @@ interface AutofillRuntimeConfigProvider {
3031
suspend fun getRuntimeConfiguration(
3132
rawJs: String,
3233
url: String?,
34+
reAuthenticationDetails: ReAuthenticationDetails,
3335
): String
3436
}
3537

@@ -46,6 +48,7 @@ class RealAutofillRuntimeConfigProvider @Inject constructor(
4648
override suspend fun getRuntimeConfiguration(
4749
rawJs: String,
4850
url: String?,
51+
reAuthenticationDetails: ReAuthenticationDetails,
4952
): String {
5053
logcat(VERBOSE) { "BrowserAutofill: getRuntimeConfiguration called" }
5154

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

6871
return StringBuilder(rawJs).apply {
6972
replacePlaceholder(this, TAG_INJECT_CONTENT_SCOPE, contentScope)
@@ -80,8 +83,8 @@ class RealAutofillRuntimeConfigProvider @Inject constructor(
8083
}
8184
}
8285

83-
private suspend fun generateAvailableInputTypes(url: String?): String {
84-
val inputTypes = autofillAvailableInputTypesProvider.getTypes(url)
86+
private suspend fun generateAvailableInputTypes(url: String?, reAuthenticationDetails: ReAuthenticationDetails): String {
87+
val inputTypes = autofillAvailableInputTypesProvider.getTypes(url, reAuthenticationDetails)
8588

8689
val json = runtimeConfigurationWriter.generateResponseGetAvailableInputTypes(inputTypes).also {
8790
logcat(VERBOSE) { "autofill-config: availableInputTypes for $url: \n$it" }

autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/InlineBrowserAutofillConfigurator.kt

Lines changed: 2 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -15,54 +15,12 @@
1515
*/
1616
package com.duckduckgo.autofill.impl.configuration
1717

18-
import android.webkit.WebView
19-
import com.duckduckgo.app.di.AppCoroutineScope
20-
import com.duckduckgo.autofill.api.AutofillCapabilityChecker
2118
import com.duckduckgo.autofill.api.BrowserAutofill.Configurator
22-
import com.duckduckgo.common.utils.DefaultDispatcherProvider
23-
import com.duckduckgo.common.utils.DispatcherProvider
2419
import com.duckduckgo.di.scopes.AppScope
2520
import com.squareup.anvil.annotations.ContributesBinding
2621
import javax.inject.Inject
27-
import kotlinx.coroutines.CoroutineScope
28-
import kotlinx.coroutines.launch
29-
import kotlinx.coroutines.withContext
30-
import logcat.LogPriority.VERBOSE
31-
import logcat.logcat
3222

3323
@ContributesBinding(AppScope::class)
3424
class InlineBrowserAutofillConfigurator @Inject constructor(
35-
private val autofillRuntimeConfigProvider: AutofillRuntimeConfigProvider,
36-
@AppCoroutineScope private val coroutineScope: CoroutineScope,
37-
private val dispatchers: DispatcherProvider = DefaultDispatcherProvider(),
38-
private val autofillCapabilityChecker: AutofillCapabilityChecker,
39-
private val autofillJavascriptLoader: AutofillJavascriptLoader,
40-
) : Configurator {
41-
override fun configureAutofillForCurrentPage(
42-
webView: WebView,
43-
url: String?,
44-
) {
45-
coroutineScope.launch(dispatchers.io()) {
46-
if (canJsBeInjected(url)) {
47-
logcat(VERBOSE) { "Injecting autofill JS into WebView for $url" }
48-
49-
val rawJs = autofillJavascriptLoader.getAutofillJavascript()
50-
val formatted = autofillRuntimeConfigProvider.getRuntimeConfiguration(rawJs, url)
51-
52-
withContext(dispatchers.main()) {
53-
webView.evaluateJavascript("javascript:$formatted", null)
54-
}
55-
} else {
56-
logcat(VERBOSE) { "Won't inject autofill JS into WebView for: $url" }
57-
}
58-
}
59-
}
60-
61-
private suspend fun canJsBeInjected(url: String?): Boolean {
62-
url?.let {
63-
// note, we don't check for autofillEnabledByUser here, as the user-facing preference doesn't cover email
64-
return autofillCapabilityChecker.isAutofillEnabledByConfiguration(it)
65-
}
66-
return false
67-
}
68-
}
25+
private val configurator: InternalBrowserAutofillConfigurator,
26+
) : Configurator by configurator
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright (c) 2025 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.autofill.impl.configuration
18+
19+
import android.webkit.WebView
20+
import com.duckduckgo.autofill.api.BrowserAutofill.Configurator
21+
import com.duckduckgo.autofill.impl.store.ReAuthenticationDetails
22+
23+
interface InternalBrowserAutofillConfigurator : Configurator {
24+
/**
25+
* Configures autofill for the current webpage with optional automatic re-authentication support.
26+
* This should be called once per page load (e.g., onPageStarted())
27+
*
28+
* @param webView The WebView to configure
29+
* @param url The URL of the current page
30+
* @param reauthenticationDetails Whether to enable automatic re-authentication for this page
31+
*/
32+
fun configureAutofillForCurrentPage(
33+
webView: WebView,
34+
url: String?,
35+
reauthenticationDetails: ReAuthenticationDetails,
36+
)
37+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright (c) 2025 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.duckduckgo.autofill.impl.configuration
17+
18+
import android.webkit.WebView
19+
import com.duckduckgo.app.di.AppCoroutineScope
20+
import com.duckduckgo.autofill.api.AutofillCapabilityChecker
21+
import com.duckduckgo.autofill.impl.store.ReAuthenticationDetails
22+
import com.duckduckgo.common.utils.DefaultDispatcherProvider
23+
import com.duckduckgo.common.utils.DispatcherProvider
24+
import com.duckduckgo.di.scopes.AppScope
25+
import com.squareup.anvil.annotations.ContributesBinding
26+
import javax.inject.Inject
27+
import kotlinx.coroutines.CoroutineScope
28+
import kotlinx.coroutines.launch
29+
import kotlinx.coroutines.withContext
30+
import logcat.LogPriority.VERBOSE
31+
import logcat.logcat
32+
33+
@ContributesBinding(AppScope::class)
34+
class RealInlineBrowserAutofillConfigurator @Inject constructor(
35+
private val autofillRuntimeConfigProvider: AutofillRuntimeConfigProvider,
36+
@AppCoroutineScope private val coroutineScope: CoroutineScope,
37+
private val dispatchers: DispatcherProvider = DefaultDispatcherProvider(),
38+
private val autofillCapabilityChecker: AutofillCapabilityChecker,
39+
private val autofillJavascriptLoader: AutofillJavascriptLoader,
40+
) : InternalBrowserAutofillConfigurator {
41+
override fun configureAutofillForCurrentPage(
42+
webView: WebView,
43+
url: String?,
44+
) {
45+
configureAutofillForCurrentPage(webView, url, ReAuthenticationDetails())
46+
}
47+
48+
override fun configureAutofillForCurrentPage(
49+
webView: WebView,
50+
url: String?,
51+
reauthenticationDetails: ReAuthenticationDetails,
52+
) {
53+
coroutineScope.launch(dispatchers.io()) {
54+
if (canJsBeInjected(url)) {
55+
val rawJs = autofillJavascriptLoader.getAutofillJavascript()
56+
val formatted = autofillRuntimeConfigProvider.getRuntimeConfiguration(rawJs, url, reauthenticationDetails)
57+
58+
withContext(dispatchers.main()) {
59+
webView.evaluateJavascript("javascript:$formatted", null)
60+
}
61+
} else {
62+
logcat(VERBOSE) { "Won't inject autofill JS into WebView for: $url" }
63+
}
64+
}
65+
}
66+
67+
private suspend fun canJsBeInjected(url: String?): Boolean {
68+
url?.let {
69+
// note, we don't check for autofillEnabledByUser here, as the user-facing preference doesn't cover email
70+
return autofillCapabilityChecker.isAutofillEnabledByConfiguration(it)
71+
}
72+
return false
73+
}
74+
}

autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/encoding/UrlUnicodeNormalizer.kt

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,19 @@ class UrlUnicodeNormalizerImpl @Inject constructor() : UrlUnicodeNormalizer {
3636
val originalScheme = url.scheme() ?: ""
3737
val noScheme = url.removePrefix(originalScheme)
3838

39+
// Extract just the hostname/domain part for IDNA processing
40+
val hostEndIndex = noScheme.indexOfFirst { it == '/' || it == '?' || it == '#' }
41+
val hostname = if (hostEndIndex == -1) noScheme else noScheme.substring(0, hostEndIndex)
42+
val pathAndQuery = if (hostEndIndex == -1) "" else noScheme.substring(hostEndIndex)
43+
3944
val sb = StringBuilder()
4045
val info = IDNA.Info()
41-
IDNA.getUTS46Instance(IDNA.DEFAULT).nameToASCII(noScheme, sb, info)
46+
IDNA.getUTS46Instance(IDNA.DEFAULT).nameToASCII(hostname, sb, info)
4247
if (info.hasErrors()) {
43-
logcat { "Unable to convert to ASCII: $url" }
48+
logcat { "Unable to convert hostname to ASCII: $hostname" }
4449
return url
4550
}
46-
return "${originalScheme}$sb"
51+
return "${originalScheme}$sb$pathAndQuery"
4752
}
4853

4954
override fun normalizeUnicode(url: String?): String? {

autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/gpm/feature/AutofillImportPasswordSettings.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ class AutofillImportPasswordConfigStoreImpl @Inject constructor(
8080

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

0 commit comments

Comments
 (0)