Skip to content

Commit 740c65d

Browse files
committed
WIP - adding one time password functionality
Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>
1 parent 6c6e240 commit 740c65d

File tree

4 files changed

+118
-1
lines changed

4 files changed

+118
-1
lines changed

app/src/main/java/com/nextcloud/talk/account/BrowserLoginActivity.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import androidx.lifecycle.repeatOnLifecycle
2121
import autodagger.AutoInjector
2222
import com.google.android.material.snackbar.Snackbar
2323
import com.nextcloud.talk.R
24+
import com.nextcloud.talk.account.data.LoginRepository
2425
import com.nextcloud.talk.account.viewmodels.BrowserLoginActivityViewModel
2526
import com.nextcloud.talk.activities.BaseActivity
2627
import com.nextcloud.talk.activities.MainActivity
@@ -124,7 +125,12 @@ class BrowserLoginActivity : BaseActivity() {
124125

125126
if (extras.containsKey(BundleKeys.KEY_FROM_QR)) {
126127
val uri = extras.getString(BundleKeys.KEY_FROM_QR)!!
127-
viewModel.loginWithQR(uri, reauthorizeAccount)
128+
129+
if (uri.startsWith(LoginRepository.ONE_TIME_PREFIX)) {
130+
viewModel.loginWithOTPQR(uri, reauthorizeAccount)
131+
} else {
132+
viewModel.loginWithQR(uri, reauthorizeAccount)
133+
}
128134
} else if (baseUrl != null) {
129135
viewModel.startWebBrowserLogin(baseUrl, reauthorizeAccount)
130136
}

app/src/main/java/com/nextcloud/talk/account/data/LoginRepository.kt

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USERNAME
2121
import kotlinx.coroutines.Dispatchers
2222
import kotlinx.coroutines.delay
2323
import kotlinx.coroutines.withContext
24+
import okhttp3.Credentials
2425
import java.net.URLDecoder
2526

2627
@Suppress("TooManyFunctions", "ReturnCount")
@@ -34,6 +35,7 @@ class LoginRepository(val network: NetworkLoginDataSource, val local: LocalLogin
3435
private const val SERVER_KEY = "server:"
3536
private const val PASS_KEY = "password:"
3637
private const val PREFIX = "nc://login/"
38+
const val ONE_TIME_PREFIX = "nc://onetime-login/"
3739
private const val MAX_ARGS = 3
3840
}
3941

@@ -103,6 +105,56 @@ class LoginRepository(val network: NetworkLoginDataSource, val local: LocalLogin
103105
}
104106
}
105107

108+
/**
109+
* Entry point for a one-time QR code
110+
*/
111+
suspend fun startOTPLoginFlow(dataString: String, reAuth: Boolean = false): LoginCompletion? =
112+
withContext(Dispatchers.IO) {
113+
shouldReauthorizeUser = reAuth
114+
115+
if (!dataString.startsWith(ONE_TIME_PREFIX)) {
116+
Log.e(TAG, "Invalid login URL detected")
117+
return@withContext null
118+
}
119+
120+
val data = dataString.removePrefix(ONE_TIME_PREFIX)
121+
val values = data.split('&')
122+
123+
if (values.size !in 1..MAX_ARGS) {
124+
Log.e(TAG, "Illegal number of login URL elements detected: ${values.size}")
125+
return@withContext null
126+
}
127+
128+
var server = ""
129+
var loginName = ""
130+
var appPassword = ""
131+
values.forEach { value ->
132+
when {
133+
value.startsWith(USER_KEY) -> {
134+
loginName = URLDecoder.decode(value.removePrefix(USER_KEY), "UTF-8")
135+
}
136+
137+
value.startsWith(PASS_KEY) -> {
138+
appPassword = URLDecoder.decode(value.removePrefix(PASS_KEY), "UTF-8")
139+
}
140+
141+
value.startsWith(SERVER_KEY) -> {
142+
server = URLDecoder.decode(value.removePrefix(SERVER_KEY), "UTF-8")
143+
}
144+
}
145+
}
146+
147+
// Need to use the qr code token to create temporary credentials to get access to the actual app password
148+
val credentials = Credentials.basic(loginName, appPassword)
149+
val oneTimePassword = network.oneTimePasswordRequest(server, credentials)
150+
151+
return@withContext if (server.isNotEmpty() && loginName.isNotEmpty() && oneTimePassword != null) {
152+
LoginCompletion(HTTP_OK, server, loginName, oneTimePassword)
153+
} else {
154+
null
155+
}
156+
}
157+
106158
/**
107159
* Entry point to the login process
108160
*/

app/src/main/java/com/nextcloud/talk/account/data/network/NetworkLoginDataSource.kt

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,47 @@ class NetworkLoginDataSource(val okHttpClient: OkHttpClient) {
2727
val TAG: String = NetworkLoginDataSource::class.java.simpleName
2828
}
2929

30+
fun oneTimePasswordRequest(baseUrl: String, oneTimeCredentials: String): String? {
31+
val url = "$baseUrl/ocs/v2.php/core/getapppassword-onetime"
32+
var result: String? = null
33+
runCatching {
34+
val response = getResponseOfOTPRequest(url, oneTimeCredentials)
35+
val jsonObject: JsonObject = JsonParser.parseString(response).asJsonObject
36+
val appPassword = jsonObject
37+
.getAsJsonObject("ocs")
38+
.getAsJsonObject("data")
39+
.get("apppassword").asString
40+
41+
result = appPassword
42+
}.getOrElse { e ->
43+
when (e) {
44+
is SSLHandshakeException,
45+
is NullPointerException,
46+
is IOException -> {
47+
Log.e(TAG, "Error caught at oneTimePasswordRequest: $e")
48+
}
49+
50+
else -> throw e
51+
}
52+
}
53+
54+
return result
55+
}
56+
57+
private fun getResponseOfOTPRequest(url: String, oneTimeCredentials: String): String? {
58+
val request = Request.Builder() // GET is default
59+
.url(url)
60+
.header("Authorization", oneTimeCredentials)
61+
.build()
62+
63+
okHttpClient.newCall(request).execute().use { response ->
64+
if (!response.isSuccessful) {
65+
throw IOException("Unexpected code $response")
66+
}
67+
return response.body?.string()
68+
}
69+
}
70+
3071
fun anonymouslyPostLoginRequest(baseUrl: String): LoginResponse? {
3172
val url = "$baseUrl/index.php/login/v2"
3273
var result: LoginResponse? = null

app/src/main/java/com/nextcloud/talk/account/viewmodels/BrowserLoginActivityViewModel.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,5 +106,23 @@ class BrowserLoginActivityViewModel @Inject constructor(val repository: LoginRep
106106
}
107107
}
108108

109+
fun loginWithOTPQR(dataString: String, reAuth: Boolean = false) {
110+
viewModelScope.launch {
111+
val loginCompletionResponse = repository.startOTPLoginFlow(dataString, reAuth)
112+
if (loginCompletionResponse == null) {
113+
_postLoginState.value = PostLoginViewState.PostLoginError
114+
return@launch
115+
}
116+
117+
val bundle = repository.parseAndLogin(loginCompletionResponse)
118+
if (bundle == null) {
119+
_postLoginState.value = PostLoginViewState.PostLoginRestartApp
120+
return@launch
121+
}
122+
123+
_postLoginState.value = PostLoginViewState.PostLoginContinue(bundle)
124+
}
125+
}
126+
109127
fun cancelLogin() = repository.cancelLoginFlow()
110128
}

0 commit comments

Comments
 (0)