Skip to content

Commit 2eed441

Browse files
authored
Enable EmailAuth support. (#6277)
1 parent 56fa8ce commit 2eed441

File tree

9 files changed

+76
-17
lines changed

9 files changed

+76
-17
lines changed

app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.kt

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ class LoginActivity : AccountAuthenticatorActivity() {
6565
private val delegate: AppCompatDelegate by lazy {
6666
AppCompatDelegate.create(this, null)
6767
}
68+
private var lastLoginResult: LoginResult? = null
6869

6970
public override fun onCreate(savedInstanceState: Bundle?) {
7071
super.onCreate(savedInstanceState)
@@ -271,6 +272,7 @@ class LoginActivity : AccountAuthenticatorActivity() {
271272
showLoggingProgressBar()
272273
loginClient.doLogin(username,
273274
password,
275+
lastLoginResult,
274276
twoFactorCode,
275277
Locale.getDefault().language,
276278
object : LoginCallback {
@@ -280,9 +282,17 @@ class LoginActivity : AccountAuthenticatorActivity() {
280282
onLoginSuccess(loginResult)
281283
}
282284

283-
override fun twoFactorPrompt(caught: Throwable, token: String?) = runOnUiThread {
285+
override fun twoFactorPrompt(loginResult: LoginResult, caught: Throwable, token: String?) = runOnUiThread {
284286
Timber.d("Requesting 2FA prompt")
285287
progressDialog!!.dismiss()
288+
lastLoginResult = loginResult
289+
askUserForTwoFactorAuth()
290+
}
291+
292+
override fun emailAuthPrompt(loginResult: LoginResult, caught: Throwable, token: String?) {
293+
Timber.d("Requesting email auth prompt")
294+
progressDialog!!.dismiss()
295+
lastLoginResult = loginResult
286296
askUserForTwoFactorAuth()
287297
}
288298

@@ -341,12 +351,13 @@ class LoginActivity : AccountAuthenticatorActivity() {
341351
progressDialog!!.dismiss()
342352
with(binding!!) {
343353
twoFactorContainer.visibility = View.VISIBLE
354+
twoFactorContainer.hint = getString(if (lastLoginResult is LoginResult.EmailAuthResult) R.string.email_auth_code else R.string._2fa_code)
344355
loginTwoFactor.visibility = View.VISIBLE
345356
loginTwoFactor.requestFocus()
346357
}
347358
val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
348359
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY)
349-
showMessageAndCancelDialog(R.string.login_failed_2fa_needed)
360+
showMessageAndCancelDialog(getString(if (lastLoginResult is LoginResult.EmailAuthResult) R.string.login_failed_email_auth_needed else R.string.login_failed_2fa_needed))
350361
}
351362

352363
@VisibleForTesting

app/src/main/java/fr/free/nrw/commons/auth/csrf/CsrfTokenClient.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class CsrfTokenClient(
3232
try {
3333
if (retry > 0) {
3434
// Log in explicitly
35-
loginClient.loginBlocking(userName, password, "")
35+
loginClient.loginBlocking(userName, password)
3636
}
3737

3838
// Get CSRFToken response off the main thread.
@@ -92,6 +92,8 @@ class CsrfTokenClient(
9292
override fun failure(caught: Throwable?) = retryWithLogin(cb) { caught }
9393

9494
override fun twoFactorPrompt() = cb.twoFactorPrompt()
95+
96+
override fun emailAuthPrompt() = cb.emailAuthPrompt()
9597
},
9698
)
9799

@@ -165,10 +167,17 @@ class CsrfTokenClient(
165167
}
166168

167169
override fun twoFactorPrompt(
170+
loginResult: LoginResult,
168171
caught: Throwable,
169172
token: String?,
170173
) = callback.twoFactorPrompt()
171174

175+
override fun emailAuthPrompt(
176+
loginResult: LoginResult,
177+
caught: Throwable,
178+
token: String?,
179+
) = callback.emailAuthPrompt()
180+
172181
// Should not happen here, but call the callback just in case.
173182
override fun passwordResetPrompt(token: String?) = callback.failure(LoginFailedException("Logged in with temporary password."))
174183

@@ -190,6 +199,8 @@ class CsrfTokenClient(
190199
fun failure(caught: Throwable?)
191200

192201
fun twoFactorPrompt()
202+
203+
fun emailAuthPrompt()
193204
}
194205

195206
companion object {

app/src/main/java/fr/free/nrw/commons/auth/login/LoginCallback.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ interface LoginCallback {
44
fun success(loginResult: LoginResult)
55

66
fun twoFactorPrompt(
7+
loginResult: LoginResult,
8+
caught: Throwable,
9+
token: String?,
10+
)
11+
12+
fun emailAuthPrompt(
13+
loginResult: LoginResult,
714
caught: Throwable,
815
token: String?,
916
)

app/src/main/java/fr/free/nrw/commons/auth/login/LoginClient.kt

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package fr.free.nrw.commons.auth.login
22

33
import android.text.TextUtils
4+
import fr.free.nrw.commons.auth.login.LoginResult.EmailAuthResult
45
import fr.free.nrw.commons.auth.login.LoginResult.OAuthResult
56
import fr.free.nrw.commons.auth.login.LoginResult.ResetPasswordResult
67
import fr.free.nrw.commons.wikidata.WikidataConstants.WIKIPEDIA_URL
@@ -51,6 +52,7 @@ class LoginClient(
5152
password,
5253
null,
5354
null,
55+
null,
5456
response.body()!!.query()!!.loginToken(),
5557
userLanguage,
5658
cb,
@@ -75,21 +77,23 @@ class LoginClient(
7577
password: String,
7678
retypedPassword: String?,
7779
twoFactorCode: String?,
80+
emailAuthCode: String?,
7881
loginToken: String?,
7982
userLanguage: String,
8083
cb: LoginCallback,
8184
) {
8285
this.userLanguage = userLanguage
8386

8487
loginCall =
85-
if (twoFactorCode.isNullOrEmpty() && retypedPassword.isNullOrEmpty()) {
88+
if (twoFactorCode.isNullOrEmpty() && emailAuthCode.isNullOrEmpty() && retypedPassword.isNullOrEmpty()) {
8689
loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL)
8790
} else {
8891
loginInterface.postLogIn(
8992
userName,
9093
password,
9194
retypedPassword,
9295
twoFactorCode,
96+
emailAuthCode,
9397
loginToken,
9498
userLanguage,
9599
true,
@@ -112,10 +116,18 @@ class LoginClient(
112116
when (loginResult) {
113117
is OAuthResult ->
114118
cb.twoFactorPrompt(
119+
loginResult,
115120
LoginFailedException(loginResult.message),
116121
loginToken,
117122
)
118123

124+
is EmailAuthResult ->
125+
cb.emailAuthPrompt(
126+
loginResult,
127+
LoginFailedException(loginResult.message),
128+
loginToken
129+
)
130+
119131
is ResetPasswordResult -> cb.passwordResetPrompt(loginToken)
120132

121133
is LoginResult.Result ->
@@ -147,6 +159,7 @@ class LoginClient(
147159
fun doLogin(
148160
username: String,
149161
password: String,
162+
lastLoginResult: LoginResult?,
150163
twoFactorCode: String,
151164
userLanguage: String,
152165
loginCallback: LoginCallback,
@@ -159,7 +172,10 @@ class LoginClient(
159172
) = if (response.isSuccessful) {
160173
val loginToken = response.body()?.query()?.loginToken()
161174
loginToken?.let {
162-
login(username, password, null, twoFactorCode, it, userLanguage, loginCallback)
175+
login(username, password, null,
176+
if (lastLoginResult is OAuthResult) twoFactorCode else null,
177+
if (lastLoginResult is EmailAuthResult) twoFactorCode else null,
178+
it, userLanguage, loginCallback)
163179
} ?: run {
164180
loginCallback.error(IOException("Failed to retrieve login token"))
165181
}
@@ -181,7 +197,8 @@ class LoginClient(
181197
fun loginBlocking(
182198
userName: String,
183199
password: String,
184-
twoFactorCode: String?,
200+
twoFactorCode: String? = null,
201+
emailAuthCode: String? = null
185202
) {
186203
val tokenResponse = getLoginToken().execute()
187204
if (tokenResponse
@@ -195,14 +212,15 @@ class LoginClient(
195212

196213
val loginToken = tokenResponse.body()?.query()?.loginToken()
197214
val tempLoginCall =
198-
if (twoFactorCode.isNullOrEmpty()) {
215+
if (twoFactorCode.isNullOrEmpty() && emailAuthCode.isNullOrEmpty()) {
199216
loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL)
200217
} else {
201218
loginInterface.postLogIn(
202219
userName,
203220
password,
204221
null,
205222
twoFactorCode,
223+
emailAuthCode,
206224
loginToken,
207225
userLanguage,
208226
true,
@@ -214,7 +232,7 @@ class LoginClient(
214232
val loginResult = loginResponse.toLoginResult(password) ?: throw IOException("Unexpected response when logging in.")
215233

216234
if ("UI" == loginResult.status) {
217-
if (loginResult is OAuthResult) {
235+
if (loginResult is OAuthResult || loginResult is EmailAuthResult) {
218236
// TODO: Find a better way to boil up the warning about 2FA
219237
throw LoginFailedException(loginResult.message)
220238
}

app/src/main/java/fr/free/nrw/commons/auth/login/LoginInterface.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ interface LoginInterface {
3535
@Field("password") pass: String?,
3636
@Field("retype") retypedPass: String?,
3737
@Field("OATHToken") twoFactorCode: String?,
38-
@Field("logintoken") token: String?,
38+
@Field("token") emailAuthToken: String?,
39+
@Field("logintoken") loginToken: String?,
3940
@Field("uselang") userLanguage: String?,
4041
@Field("logincontinue") loginContinue: Boolean,
4142
): Call<LoginResponse?>

app/src/main/java/fr/free/nrw/commons/auth/login/LoginResponse.kt

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package fr.free.nrw.commons.auth.login
22

33
import com.google.gson.annotations.SerializedName
44
import fr.free.nrw.commons.auth.login.LoginResult.OAuthResult
5+
import fr.free.nrw.commons.auth.login.LoginResult.EmailAuthResult
56
import fr.free.nrw.commons.auth.login.LoginResult.ResetPasswordResult
67
import fr.free.nrw.commons.auth.login.LoginResult.Result
78
import fr.free.nrw.commons.wikidata.mwapi.MwServiceError
@@ -27,11 +28,13 @@ internal class ClientLogin {
2728
fun toLoginResult(password: String): LoginResult {
2829
var userMessage = message
2930
if ("UI" == status) {
30-
if (requests != null) {
31-
for (req in requests) {
32-
if ("MediaWiki\\Extension\\OATHAuth\\Auth\\TOTPAuthenticationRequest" == req.id()) {
31+
requests?.forEach { request ->
32+
request.id()?.let {
33+
if (it.endsWith("TOTPAuthenticationRequest")) {
3334
return OAuthResult(status, userName, password, message)
34-
} else if ("MediaWiki\\Auth\\PasswordAuthenticationRequest" == req.id()) {
35+
} else if (it.endsWith("EmailAuthAuthenticationRequest")) {
36+
return EmailAuthResult(status, userName, password, message)
37+
} else if (it.endsWith("PasswordAuthenticationRequest")) {
3538
return ResetPasswordResult(status, userName, password, message)
3639
}
3740
}
@@ -49,13 +52,13 @@ internal class Request {
4952
private val required: String? = null
5053
private val provider: String? = null
5154
private val account: String? = null
52-
private val fields: Map<String, RequestField>? = null
55+
internal val fields: Map<String, RequestField>? = null
5356

5457
fun id(): String? = id
5558
}
5659

5760
internal class RequestField {
5861
private val type: String? = null
5962
private val label: String? = null
60-
private val help: String? = null
63+
internal val help: String? = null
6164
}

app/src/main/java/fr/free/nrw/commons/auth/login/LoginResult.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ sealed class LoginResult(
2424
message: String?,
2525
) : LoginResult(status, userName, password, message)
2626

27+
class EmailAuthResult(
28+
status: String,
29+
userName: String?,
30+
password: String?,
31+
message: String?,
32+
) : LoginResult(status, userName, password, message)
33+
2734
class ResetPasswordResult(
2835
status: String,
2936
userName: String?,

app/src/main/res/layout/activity_login.xml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@
146146
android:layout_marginEnd="@dimen/standard_gap"
147147
android:layout_marginRight="@dimen/standard_gap"
148148
android:layout_marginBottom="@dimen/standard_gap"
149+
android:hint="@string/_2fa_code"
149150
android:visibility="gone"
150151
app:passwordToggleEnabled="false"
151152
tools:visibility="visible">
@@ -154,9 +155,7 @@
154155
android:id="@+id/login_two_factor"
155156
android:layout_width="match_parent"
156157
android:layout_height="wrap_content"
157-
android:hint="@string/_2fa_code"
158158
android:imeOptions="flagNoExtractUi"
159-
android:inputType="number"
160159
android:visibility="gone"
161160
tools:visibility="visible" />
162161

app/src/main/res/values/strings.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@
111111
<string name="login_failed_throttled">Too many unsuccessful attempts. Please try again in a few minutes.</string>
112112
<string name="login_failed_blocked">Sorry, this user has been blocked on Commons</string>
113113
<string name="login_failed_2fa_needed">You must provide your two factor authentication code.</string>
114+
<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>
114115
<string name="login_failed_generic">Log-in failed</string>
115116
<string name="share_upload_button">Upload</string>
116117
<string name="multiple_share_base_title">Name this set</string>
@@ -218,6 +219,7 @@
218219
<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>
219220
<string name="beta_opt_in_link">https://play.google.com/apps/testing/fr.free.nrw.commons</string>
220221
<string name="_2fa_code">2FA Code</string>
222+
<string name="email_auth_code">Email verification code</string>
221223
<string name="logout_verification">Do you really want to logout?</string>
222224
<string name="mediaimage_failed">Media Image Failed</string>
223225
<string name="no_subcategory_found">No subcategories found</string>

0 commit comments

Comments
 (0)