Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use a webview for WP.com login #21414

Open
wants to merge 11 commits into
base: trunk
Choose a base branch
from
4 changes: 2 additions & 2 deletions .configure
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"project_name": "WordPress-Android",
"branch": "trunk",
"pinned_hash": "11fa9c59619ba8d31d0bce2b7ba4412074c00d69",
"pinned_hash": "1c83426e3f9175a6d561f4af53c69a1e2097a6ef",
"files_to_copy": [
{
"file": "android/WPAndroid/secrets.properties",
Expand Down Expand Up @@ -47,4 +47,4 @@
"file_dependencies": [

]
}
}
Binary file modified .configure-files/secrets.properties.enc
Binary file not shown.
4 changes: 4 additions & 0 deletions WordPress/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ android {
buildConfigField "String", "PUSH_NOTIFICATIONS_APP_KEY", '"org.wordpress.android"'
buildConfigField "boolean", "ENABLE_QRCODE_AUTH_FLOW", "false"
buildConfigField "boolean", "ENABLE_OPEN_WEB_LINKS_WITH_JP_FLOW", "true"
buildConfigField "String", "WPCOM_REDIRECT_URI", '"wordpress://wpcom-authorize"'

manifestPlaceholders = [magicLinkScheme:"wordpress"]
}
Expand All @@ -229,6 +230,7 @@ android {
buildConfigField "String", "PUSH_NOTIFICATIONS_APP_KEY", '"com.jetpack.android"'
buildConfigField "boolean", "ENABLE_QRCODE_AUTH_FLOW", "true"
buildConfigField "boolean", "ENABLE_OPEN_WEB_LINKS_WITH_JP_FLOW", "false"
buildConfigField "String", "WPCOM_REDIRECT_URI", '"jetpack://wpcom-authorize"'

manifestPlaceholders = [magicLinkScheme:"jetpack"]

Expand Down Expand Up @@ -443,6 +445,8 @@ dependencies {
implementation(libs.google.mlkit.text.recognition)
implementation(libs.google.mlkit.barcode.scanning.main)

implementation(libs.androidx.browser)

// CameraX
implementation(libs.androidx.camera.camera2)
implementation(libs.androidx.camera.lifecycle)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class LoginPrologueRevampedFragment : Fragment() {
LoginScreenRevamped(
onWpComLoginClicked = {
viewModel.onWpComLoginClicked()
loginPrologueListener.showEmailLoginScreen()
loginPrologueListener.showWPcomLoginScreen(this.context)
},
onSiteAddressLoginClicked = {
viewModel.onSiteAddressLoginClicked()
Expand Down
16 changes: 15 additions & 1 deletion WordPress/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,21 @@
<activity
android:name=".ui.accounts.LoginActivity"
android:theme="@style/LoginTheme.TransparentSystemBars"
android:windowSoftInputMode="adjustResize" />
android:windowSoftInputMode="adjustResize"
android:exported="true">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sonarcloud flags this as a security risk. Does it need to be exported?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is that this was required in order to handle custom URL schemes, but I might be incorrect!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I suspect you're right about that. This Sonarcloud page mentions ways to fix this warning.

Comment on lines +116 to +117
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you're missing android:launchMode="singleTask". Without that, a new Activity is created after the OAuth flow is approved, and you can navigate back to it (that shouldn't be possible).

jetpack-launchmode.mp4

singleTop does work as well in most browsers except Firefox. So singleTask is the safe option here.

Once you add that you should handle the onNewIntent properly.


<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="wpcom-authorize"
android:scheme="${magicLinkScheme}" />
</intent-filter>
</activity>

<activity
android:name=".ui.accounts.LoginMagicLinkInterceptActivity"

Check warning

Code scanning / Android Lint

Application has custom scheme intent filters with missing autoVerify attributes Warning

Custom scheme intent filters should explicitly set the autoVerify attribute to true
android:theme="@style/NoDisplay"
android:exported="true">

Expand Down Expand Up @@ -1155,6 +1166,9 @@
<intent>
<action android:name="android.intent.action.MAIN" />
</intent>
<intent>
<action android:name="android.support.customtabs.action.CustomTabsService" />
</intent>
<!-- required for Android 11 (API level 30) or higher -->
<package android:name="com.jetpack.android" />
</queries>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import android.content.Context;
import android.util.Base64;

import androidx.annotation.NonNull;

import com.goterl.lazysodium.utils.Key;

import org.wordpress.android.BuildConfig;
Expand All @@ -19,12 +21,13 @@
import dagger.hilt.android.qualifiers.ApplicationContext;
import dagger.hilt.components.SingletonComponent;


@InstallIn(SingletonComponent.class)
@Module
public class AppConfigModule {
@Provides
@NonNull @Provides
public AppSecrets provideAppSecrets() {
return new AppSecrets(BuildConfig.OAUTH_APP_ID, BuildConfig.OAUTH_APP_SECRET);
return new AppSecrets(BuildConfig.OAUTH_APP_ID, BuildConfig.OAUTH_APP_SECRET, BuildConfig.WPCOM_REDIRECT_URI);
}

@Singleton
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.wordpress.android.ui.accounts;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
Expand All @@ -9,6 +10,7 @@

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.browser.customtabs.CustomTabsIntent;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentTransaction;
import androidx.lifecycle.ViewModelProvider;
Expand Down Expand Up @@ -58,8 +60,10 @@
import org.wordpress.android.ui.accounts.UnifiedLoginTracker.Click;
import org.wordpress.android.ui.accounts.UnifiedLoginTracker.Flow;
import org.wordpress.android.ui.accounts.UnifiedLoginTracker.Source;
import org.wordpress.android.ui.accounts.UnifiedLoginTracker.Step;
import org.wordpress.android.ui.accounts.login.LoginPrologueListener;
import org.wordpress.android.ui.accounts.login.LoginPrologueRevampedFragment;
import org.wordpress.android.ui.accounts.login.WPcomLoginHelper;
import org.wordpress.android.ui.accounts.login.jetpack.LoginNoSitesFragment;
import org.wordpress.android.ui.accounts.login.jetpack.LoginSiteCheckErrorFragment;
import org.wordpress.android.ui.main.ChooseSiteActivity;
Expand Down Expand Up @@ -130,6 +134,7 @@ private enum SmartLockHelperState {

private LoginMode mLoginMode;
private LoginViewModel mViewModel;
@Inject protected WPcomLoginHelper mLoginHelper;

@Inject DispatchingAndroidInjector<Object> mDispatchingAndroidInjector;
@Inject protected LoginAnalyticsListener mLoginAnalyticsListener;
Expand All @@ -144,6 +149,18 @@ private enum SmartLockHelperState {
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

// Attempt Login if this activity was created in response to a user confirming login
mLoginHelper.tryLoginWithDataString(getIntent().getDataString());

// Start preloading the WordPress.com login page if needed – this avoids visual hitches
// when displaying that screen
mLoginHelper.bindCustomTabsService(this);

if (mLoginHelper.isLoggedIn()) {
this.loggedInAndFinish(new ArrayList<Integer>(), true);
return;
}

LoginFlowThemeHelper.injectMissingCustomAttributes(getTheme());

setContentView(R.layout.login_activity);
Expand Down Expand Up @@ -184,7 +201,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {
break;
case WPCOM_REAUTHENTICATE:
mUnifiedLoginTracker.setSource(Source.REAUTHENTICATION);
checkSmartLockPasswordAndStartLogin();
showWPcomLoginScreen(getBaseContext());
break;
case SHARE_INTENT:
mUnifiedLoginTracker.setSource(Source.SHARE);
Expand Down Expand Up @@ -451,9 +468,20 @@ private void startLogin() {

// LoginPrologueListener implementation methods

@Override
public void showEmailLoginScreen() {
checkSmartLockPasswordAndStartLogin();
public void showWPcomLoginScreen(@NonNull Context context) {
AnalyticsTracker.track(AnalyticsTracker.Stat.LOGIN_WPCOM_WEBVIEW);
mUnifiedLoginTracker.setFlowAndStep(Flow.WORDPRESS_COM_WEB, Step.WPCOM_WEB_START);

CustomTabsIntent intent = new CustomTabsIntent.Builder()
.setShareState(CustomTabsIntent.SHARE_STATE_OFF)
.setStartAnimations(this, R.anim.activity_slide_in_from_right, R.anim.activity_slide_out_to_left)
.setExitAnimations(this, R.anim.activity_slide_in_from_left, R.anim.activity_slide_out_to_right)
.setUrlBarHidingEnabled(true)
.setInstantAppsEnabled(false)
.setShowTitle(false)
.build();
Comment on lines +475 to +482
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for jumping in, but since I took a look I wanted to share.

Using Custom Tabs with the magic link leads to a weird experience. Assuming you are not logged into WP in the web browser, you will have to open the email client to get the magic link. Tapping this magic link opens the web browser, not the Custom Tab which was confusing slightly. In the end, it worked, however, 3 apps were required to finish the process.


intent.launchUrl(this, mLoginHelper.getWpcomLoginUri());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ sealed class LoginNavigationEvents {
data class SelectSite(val localId: Int) : LoginNavigationEvents()
object CreateNewSite : LoginNavigationEvents()
object CloseWithResultOk : LoginNavigationEvents()
object ShowEmailLoginScreen : LoginNavigationEvents()
object showWPcomLoginScreen : LoginNavigationEvents()
object ShowLoginViaSiteAddressScreen : LoginNavigationEvents()
object ShowJetpackIndividualPluginOverlay : LoginNavigationEvents()
}
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ class UnifiedLoginTracker
enum class Flow(val value: String) {
PROLOGUE("prologue"),
WORDPRESS_COM("wordpress_com"),
WORDPRESS_COM_WEB("wordpress_com_web"),
GOOGLE_LOGIN("google_login"),
SMART_LOCK_LOGIN("smart_lock_login"),
LOGIN_MAGIC_LINK("login_magic_link"),
Expand All @@ -147,7 +148,8 @@ class UnifiedLoginTracker
SHOW_EMAIL_HINTS("show_email_hints"),
PASSWORD_CHALLENGE("password_challenge"),
NOT_A_JETPACK_SITE("not_a_jetpack_site"),
NO_JETPACK_SITES("no_jetpack_sites")
NO_JETPACK_SITES("no_jetpack_sites"),
WPCOM_WEB_START("wpcom_web_start")
}

enum class Click(val value: String) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package org.wordpress.android.ui.accounts.login

import android.content.Context

interface LoginPrologueListener {
// Login Prologue callbacks
fun showEmailLoginScreen()
fun showWPcomLoginScreen(context: Context)
fun loginViaSiteAddress()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package org.wordpress.android.ui.accounts.login

import android.content.ComponentName
import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.browser.customtabs.CustomTabsCallback
import androidx.browser.customtabs.CustomTabsClient
import androidx.browser.customtabs.CustomTabsServiceConnection
import androidx.browser.customtabs.CustomTabsSession
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.runBlocking
import org.wordpress.android.fluxc.network.rest.wpapi.WPcomLoginClient
import org.wordpress.android.fluxc.network.rest.wpcom.auth.AppSecrets
import org.wordpress.android.fluxc.store.AccountStore
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext

class WPcomLoginHelper @Inject constructor(
private val loginClient: WPcomLoginClient,
private val accountStore: AccountStore,
appSecrets: AppSecrets
) {
private val context: CoroutineContext = Dispatchers.IO

val wpcomLoginUri = loginClient.loginUri(appSecrets.redirectUri)
private val customTabsServiceConnection = ServiceConnection(wpcomLoginUri)

fun tryLoginWithDataString(data: String?) {
if (data == null) {
return
}

val code = this.codeFromAuthorizationUri(data) ?: return

runBlocking {
val tokenResult = loginClient.exchangeAuthCodeForToken(code)
accountStore.updateAccessToken(tokenResult.getOrThrow())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you'd want to check if the token belongs to the current user before saving it. Users can log in with any account from the login webpage.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be for first-time authentication only – I'm thinking we'll want a reauthenticate method that does what you say – forces login to a specific account

Log.i("WPCOM_LOGIN", "Login Successful")
}
}

fun isLoggedIn(): Boolean {
return accountStore.hasAccessToken()
}

fun dispose() {
context.cancel()
}

fun bindCustomTabsService(context: Context) {
customTabsServiceConnection.bind(context)
}

private fun codeFromAuthorizationUri(string: String): String? {
return Uri.parse(string).getQueryParameter("code")
}
}

class ServiceConnection(
var uri: Uri
): CustomTabsServiceConnection() {
private var client: CustomTabsClient? = null
private var session: CustomTabsSession? = null

override fun onCustomTabsServiceConnected(name: ComponentName, client: CustomTabsClient) {
client.warmup(0)
this.client = client

val session = client.newSession(CustomTabsCallback())
session?.mayLaunchUrl(uri, null, null)
session?.mayLaunchUrl(Uri.parse("https://wordpress.com/log-in/"), null, null)

this.session = session
}

override fun onServiceDisconnected(name: ComponentName?) {
this.client = null
this.session = null
}

fun bind(context: Context) {
// Do nothing if there is an existing service connection
if (this.client != null) {
return
}

// Get the default browser package name, this will be null if
// the default browser does not provide a CustomTabsService
val packageName = CustomTabsClient.getPackageName(context, null) ?: return

CustomTabsClient.bindCustomTabsService(context, packageName, this)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class LoginPrologueFragment : Fragment(R.layout.login_signup_screen) {

continueWithWpcomButton.setOnClickListener {
unifiedLoginTracker.trackClick(Click.CONTINUE_WITH_WORDPRESS_COM)
loginPrologueListener.showEmailLoginScreen()
loginPrologueListener.showWPcomLoginScreen(view.context)
}

enterYourSiteAddressButton.setOnClickListener {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ class LoginPrologueRevampedFragment : Fragment() {
setContent {
AppThemeM2 {
LoginScreenRevamped(
onWpComLoginClicked = loginPrologueListener::showEmailLoginScreen,
onWpComLoginClicked = {
loginPrologueListener.showWPcomLoginScreen(this.context)
},
onSiteAddressLoginClicked = loginPrologueListener::loginViaSiteAddress,
)
}
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ androidx-activity = '1.9.3'
androidx-annotation = '1.9.1'
androidx-appcompat = '1.7.0'
androidx-arch-core = '2.2.0'
androidx-browser = '1.5.0'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this is an outdated version of the library. The latest is 1.8.0.

androidx-camera = '1.4.1'
androidx-cardview = '1.0.0'
androidx-compose-bom = '2024.12.01'
Expand Down Expand Up @@ -120,6 +121,7 @@ androidx-annotation = { group = "androidx.annotation", name = "annotation", vers
androidx-appcompat-main = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" }
androidx-appcompat-resources = { group = "androidx.appcompat", name = "appcompat-resources", version.ref = "androidx-appcompat" }
androidx-arch-core-testing = { group = "androidx.arch.core", name = "core-testing", version.ref = "androidx-arch-core" }
androidx-browser = { group="androidx.browser", "name" = "browser", version.ref = "androidx-browser" }
androidx-camera-camera2 = { group = "androidx.camera", name = "camera-camera2", version.ref = "androidx-camera" }
androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "androidx-camera" }
androidx-camera-view = { group = "androidx.camera", name = "camera-view", version.ref = "androidx-camera" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,7 @@ public enum Stat {
LOGIN_SOCIAL_2FA_NEEDED,
LOGIN_SOCIAL_ACCOUNTS_NEED_CONNECTING,
LOGIN_SOCIAL_ERROR_UNKNOWN_USER,
LOGIN_WPCOM_WEBVIEW,
LOGIN_WPCOM_BACKGROUND_SERVICE_UPDATE,
// This stat is part of a funnel that provides critical information. Before
// making ANY modification to this stat please refer to: p4qSXL-35X-p2
Expand Down
Loading
Loading