Enable EmailAuth support.

This commit is contained in:
Dmitry Brant 2025-04-07 12:29:08 -04:00
parent 51da9e4dd6
commit 79a7e5c077
9 changed files with 76 additions and 17 deletions

View file

@ -65,6 +65,7 @@ class LoginActivity : AccountAuthenticatorActivity() {
private val delegate: AppCompatDelegate by lazy {
AppCompatDelegate.create(this, null)
}
private var lastLoginResult: LoginResult? = null
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -271,6 +272,7 @@ class LoginActivity : AccountAuthenticatorActivity() {
showLoggingProgressBar()
loginClient.doLogin(username,
password,
lastLoginResult,
twoFactorCode,
Locale.getDefault().language,
object : LoginCallback {
@ -280,9 +282,17 @@ class LoginActivity : AccountAuthenticatorActivity() {
onLoginSuccess(loginResult)
}
override fun twoFactorPrompt(caught: Throwable, token: String?) = runOnUiThread {
override fun twoFactorPrompt(loginResult: LoginResult, caught: Throwable, token: String?) = runOnUiThread {
Timber.d("Requesting 2FA prompt")
progressDialog!!.dismiss()
lastLoginResult = loginResult
askUserForTwoFactorAuth()
}
override fun emailAuthPrompt(loginResult: LoginResult, caught: Throwable, token: String?) {
Timber.d("Requesting email auth prompt")
progressDialog!!.dismiss()
lastLoginResult = loginResult
askUserForTwoFactorAuth()
}
@ -341,12 +351,13 @@ class LoginActivity : AccountAuthenticatorActivity() {
progressDialog!!.dismiss()
with(binding!!) {
twoFactorContainer.visibility = View.VISIBLE
twoFactorContainer.hint = getString(if (lastLoginResult is LoginResult.EmailAuthResult) R.string.email_auth_code else R.string._2fa_code)
loginTwoFactor.visibility = View.VISIBLE
loginTwoFactor.requestFocus()
}
val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY)
showMessageAndCancelDialog(R.string.login_failed_2fa_needed)
showMessageAndCancelDialog(getString(if (lastLoginResult is LoginResult.EmailAuthResult) R.string.login_failed_email_auth_needed else R.string.login_failed_2fa_needed))
}
@VisibleForTesting

View file

@ -32,7 +32,7 @@ class CsrfTokenClient(
try {
if (retry > 0) {
// Log in explicitly
loginClient.loginBlocking(userName, password, "")
loginClient.loginBlocking(userName, password)
}
// Get CSRFToken response off the main thread.
@ -92,6 +92,8 @@ class CsrfTokenClient(
override fun failure(caught: Throwable?) = retryWithLogin(cb) { caught }
override fun twoFactorPrompt() = cb.twoFactorPrompt()
override fun emailAuthPrompt() = cb.emailAuthPrompt()
},
)
@ -165,10 +167,17 @@ class CsrfTokenClient(
}
override fun twoFactorPrompt(
loginResult: LoginResult,
caught: Throwable,
token: String?,
) = callback.twoFactorPrompt()
override fun emailAuthPrompt(
loginResult: LoginResult,
caught: Throwable,
token: String?,
) = callback.emailAuthPrompt()
// Should not happen here, but call the callback just in case.
override fun passwordResetPrompt(token: String?) = callback.failure(LoginFailedException("Logged in with temporary password."))
@ -190,6 +199,8 @@ class CsrfTokenClient(
fun failure(caught: Throwable?)
fun twoFactorPrompt()
fun emailAuthPrompt()
}
companion object {

View file

@ -4,6 +4,13 @@ interface LoginCallback {
fun success(loginResult: LoginResult)
fun twoFactorPrompt(
loginResult: LoginResult,
caught: Throwable,
token: String?,
)
fun emailAuthPrompt(
loginResult: LoginResult,
caught: Throwable,
token: String?,
)

View file

@ -1,6 +1,7 @@
package fr.free.nrw.commons.auth.login
import android.text.TextUtils
import fr.free.nrw.commons.auth.login.LoginResult.EmailAuthResult
import fr.free.nrw.commons.auth.login.LoginResult.OAuthResult
import fr.free.nrw.commons.auth.login.LoginResult.ResetPasswordResult
import fr.free.nrw.commons.wikidata.WikidataConstants.WIKIPEDIA_URL
@ -51,6 +52,7 @@ class LoginClient(
password,
null,
null,
null,
response.body()!!.query()!!.loginToken(),
userLanguage,
cb,
@ -75,6 +77,7 @@ class LoginClient(
password: String,
retypedPassword: String?,
twoFactorCode: String?,
emailAuthCode: String?,
loginToken: String?,
userLanguage: String,
cb: LoginCallback,
@ -82,7 +85,7 @@ class LoginClient(
this.userLanguage = userLanguage
loginCall =
if (twoFactorCode.isNullOrEmpty() && retypedPassword.isNullOrEmpty()) {
if (twoFactorCode.isNullOrEmpty() && emailAuthCode.isNullOrEmpty() && retypedPassword.isNullOrEmpty()) {
loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL)
} else {
loginInterface.postLogIn(
@ -90,6 +93,7 @@ class LoginClient(
password,
retypedPassword,
twoFactorCode,
emailAuthCode,
loginToken,
userLanguage,
true,
@ -112,10 +116,18 @@ class LoginClient(
when (loginResult) {
is OAuthResult ->
cb.twoFactorPrompt(
loginResult,
LoginFailedException(loginResult.message),
loginToken,
)
is EmailAuthResult ->
cb.emailAuthPrompt(
loginResult,
LoginFailedException(loginResult.message),
loginToken
)
is ResetPasswordResult -> cb.passwordResetPrompt(loginToken)
is LoginResult.Result ->
@ -147,6 +159,7 @@ class LoginClient(
fun doLogin(
username: String,
password: String,
lastLoginResult: LoginResult?,
twoFactorCode: String,
userLanguage: String,
loginCallback: LoginCallback,
@ -159,7 +172,10 @@ class LoginClient(
) = if (response.isSuccessful) {
val loginToken = response.body()?.query()?.loginToken()
loginToken?.let {
login(username, password, null, twoFactorCode, it, userLanguage, loginCallback)
login(username, password, null,
if (lastLoginResult is OAuthResult) twoFactorCode else null,
if (lastLoginResult is EmailAuthResult) twoFactorCode else null,
it, userLanguage, loginCallback)
} ?: run {
loginCallback.error(IOException("Failed to retrieve login token"))
}
@ -181,7 +197,8 @@ class LoginClient(
fun loginBlocking(
userName: String,
password: String,
twoFactorCode: String?,
twoFactorCode: String? = null,
emailAuthCode: String? = null
) {
val tokenResponse = getLoginToken().execute()
if (tokenResponse
@ -195,7 +212,7 @@ class LoginClient(
val loginToken = tokenResponse.body()?.query()?.loginToken()
val tempLoginCall =
if (twoFactorCode.isNullOrEmpty()) {
if (twoFactorCode.isNullOrEmpty() && emailAuthCode.isNullOrEmpty()) {
loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL)
} else {
loginInterface.postLogIn(
@ -203,6 +220,7 @@ class LoginClient(
password,
null,
twoFactorCode,
emailAuthCode,
loginToken,
userLanguage,
true,
@ -214,7 +232,7 @@ class LoginClient(
val loginResult = loginResponse.toLoginResult(password) ?: throw IOException("Unexpected response when logging in.")
if ("UI" == loginResult.status) {
if (loginResult is OAuthResult) {
if (loginResult is OAuthResult || loginResult is EmailAuthResult) {
// TODO: Find a better way to boil up the warning about 2FA
throw LoginFailedException(loginResult.message)
}

View file

@ -35,7 +35,8 @@ interface LoginInterface {
@Field("password") pass: String?,
@Field("retype") retypedPass: String?,
@Field("OATHToken") twoFactorCode: String?,
@Field("logintoken") token: String?,
@Field("token") emailAuthToken: String?,
@Field("logintoken") loginToken: String?,
@Field("uselang") userLanguage: String?,
@Field("logincontinue") loginContinue: Boolean,
): Call<LoginResponse?>

View file

@ -2,6 +2,7 @@ package fr.free.nrw.commons.auth.login
import com.google.gson.annotations.SerializedName
import fr.free.nrw.commons.auth.login.LoginResult.OAuthResult
import fr.free.nrw.commons.auth.login.LoginResult.EmailAuthResult
import fr.free.nrw.commons.auth.login.LoginResult.ResetPasswordResult
import fr.free.nrw.commons.auth.login.LoginResult.Result
import fr.free.nrw.commons.wikidata.mwapi.MwServiceError
@ -27,11 +28,13 @@ internal class ClientLogin {
fun toLoginResult(password: String): LoginResult {
var userMessage = message
if ("UI" == status) {
if (requests != null) {
for (req in requests) {
if ("MediaWiki\\Extension\\OATHAuth\\Auth\\TOTPAuthenticationRequest" == req.id()) {
requests?.forEach { request ->
request.id()?.let {
if (it.endsWith("TOTPAuthenticationRequest")) {
return OAuthResult(status, userName, password, message)
} else if ("MediaWiki\\Auth\\PasswordAuthenticationRequest" == req.id()) {
} else if (it.endsWith("EmailAuthAuthenticationRequest")) {
return EmailAuthResult(status, userName, password, message)
} else if (it.endsWith("PasswordAuthenticationRequest")) {
return ResetPasswordResult(status, userName, password, message)
}
}
@ -49,7 +52,7 @@ internal class Request {
private val required: String? = null
private val provider: String? = null
private val account: String? = null
private val fields: Map<String, RequestField>? = null
internal val fields: Map<String, RequestField>? = null
fun id(): String? = id
}
@ -57,5 +60,5 @@ internal class Request {
internal class RequestField {
private val type: String? = null
private val label: String? = null
private val help: String? = null
internal val help: String? = null
}

View file

@ -24,6 +24,13 @@ sealed class LoginResult(
message: String?,
) : LoginResult(status, userName, password, message)
class EmailAuthResult(
status: String,
userName: String?,
password: String?,
message: String?,
) : LoginResult(status, userName, password, message)
class ResetPasswordResult(
status: String,
userName: String?,

View file

@ -146,6 +146,7 @@
android:layout_marginEnd="@dimen/standard_gap"
android:layout_marginRight="@dimen/standard_gap"
android:layout_marginBottom="@dimen/standard_gap"
android:hint="@string/_2fa_code"
android:visibility="gone"
app:passwordToggleEnabled="false"
tools:visibility="visible">
@ -154,9 +155,7 @@
android:id="@+id/login_two_factor"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/_2fa_code"
android:imeOptions="flagNoExtractUi"
android:inputType="number"
android:visibility="gone"
tools:visibility="visible" />

View file

@ -111,6 +111,7 @@
<string name="login_failed_throttled">Too many unsuccessful attempts. Please try again in a few minutes.</string>
<string name="login_failed_blocked">Sorry, this user has been blocked on Commons</string>
<string name="login_failed_2fa_needed">You must provide your two factor authentication code.</string>
<string name="login_failed_email_auth_needed">A login verification code has been sent to your email address. Please provide the code to log in.</string>
<string name="login_failed_generic">Log-in failed</string>
<string name="share_upload_button">Upload</string>
<string name="multiple_share_base_title">Name this set</string>
@ -218,6 +219,7 @@
<string name="become_a_tester_description">Opt-in to our beta channel on Google Play and get early access to new features and bug fixes</string>
<string name="beta_opt_in_link">https://play.google.com/apps/testing/fr.free.nrw.commons</string>
<string name="_2fa_code">2FA Code</string>
<string name="email_auth_code">Email verification code</string>
<string name="logout_verification">Do you really want to logout?</string>
<string name="mediaimage_failed">Media Image Failed</string>
<string name="no_subcategory_found">No subcategories found</string>