diff --git a/app/src/main/java/com/nextcloud/talk/account/BrowserLoginActivity.kt b/app/src/main/java/com/nextcloud/talk/account/BrowserLoginActivity.kt index d8ed370d53..699aa87fe8 100644 --- a/app/src/main/java/com/nextcloud/talk/account/BrowserLoginActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/account/BrowserLoginActivity.kt @@ -21,6 +21,7 @@ import androidx.lifecycle.repeatOnLifecycle import autodagger.AutoInjector import com.google.android.material.snackbar.Snackbar import com.nextcloud.talk.R +import com.nextcloud.talk.account.data.LoginRepository import com.nextcloud.talk.account.viewmodels.BrowserLoginActivityViewModel import com.nextcloud.talk.activities.BaseActivity import com.nextcloud.talk.activities.MainActivity @@ -124,7 +125,12 @@ class BrowserLoginActivity : BaseActivity() { if (extras.containsKey(BundleKeys.KEY_FROM_QR)) { val uri = extras.getString(BundleKeys.KEY_FROM_QR)!! - viewModel.loginWithQR(uri, reauthorizeAccount) + + if (uri.startsWith(LoginRepository.ONE_TIME_PREFIX)) { + viewModel.loginWithOTPQR(uri, reauthorizeAccount) + } else { + viewModel.loginWithQR(uri, reauthorizeAccount) + } } else if (baseUrl != null) { viewModel.startWebBrowserLogin(baseUrl, reauthorizeAccount) } diff --git a/app/src/main/java/com/nextcloud/talk/account/data/LoginRepository.kt b/app/src/main/java/com/nextcloud/talk/account/data/LoginRepository.kt index d2ddf883e4..2a9e406e0c 100644 --- a/app/src/main/java/com/nextcloud/talk/account/data/LoginRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/account/data/LoginRepository.kt @@ -21,6 +21,7 @@ import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USERNAME import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.withContext +import okhttp3.Credentials import java.net.URLDecoder @Suppress("TooManyFunctions", "ReturnCount") @@ -34,6 +35,7 @@ class LoginRepository(val network: NetworkLoginDataSource, val local: LocalLogin private const val SERVER_KEY = "server:" private const val PASS_KEY = "password:" private const val PREFIX = "nc://login/" + const val ONE_TIME_PREFIX = "nc://onetime-login/" private const val MAX_ARGS = 3 } @@ -103,6 +105,56 @@ class LoginRepository(val network: NetworkLoginDataSource, val local: LocalLogin } } + /** + * Entry point for a one-time QR code + */ + suspend fun startOTPLoginFlow(dataString: String, reAuth: Boolean = false): LoginCompletion? = + withContext(Dispatchers.IO) { + shouldReauthorizeUser = reAuth + + if (!dataString.startsWith(ONE_TIME_PREFIX)) { + Log.e(TAG, "Invalid login URL detected") + return@withContext null + } + + val data = dataString.removePrefix(ONE_TIME_PREFIX) + val values = data.split('&') + + if (values.size !in 1..MAX_ARGS) { + Log.e(TAG, "Illegal number of login URL elements detected: ${values.size}") + return@withContext null + } + + var server = "" + var loginName = "" + var appPassword = "" + values.forEach { value -> + when { + value.startsWith(USER_KEY) -> { + loginName = URLDecoder.decode(value.removePrefix(USER_KEY), "UTF-8") + } + + value.startsWith(PASS_KEY) -> { + appPassword = URLDecoder.decode(value.removePrefix(PASS_KEY), "UTF-8") + } + + value.startsWith(SERVER_KEY) -> { + server = URLDecoder.decode(value.removePrefix(SERVER_KEY), "UTF-8") + } + } + } + + // Need to use the qr code token to create temporary credentials to get access to the actual app password + val credentials = Credentials.basic(loginName, appPassword) + val oneTimePassword = network.oneTimePasswordRequest(server, credentials) + + return@withContext if (server.isNotEmpty() && loginName.isNotEmpty() && oneTimePassword != null) { + LoginCompletion(HTTP_OK, server, loginName, oneTimePassword) + } else { + null + } + } + /** * Entry point to the login process */ diff --git a/app/src/main/java/com/nextcloud/talk/account/data/network/NetworkLoginDataSource.kt b/app/src/main/java/com/nextcloud/talk/account/data/network/NetworkLoginDataSource.kt index 2090fb7620..f852b79dd4 100644 --- a/app/src/main/java/com/nextcloud/talk/account/data/network/NetworkLoginDataSource.kt +++ b/app/src/main/java/com/nextcloud/talk/account/data/network/NetworkLoginDataSource.kt @@ -27,6 +27,50 @@ class NetworkLoginDataSource(val okHttpClient: OkHttpClient) { val TAG: String = NetworkLoginDataSource::class.java.simpleName } + fun oneTimePasswordRequest(baseUrl: String, oneTimeCredentials: String): String? { + val url = "$baseUrl/ocs/v2.php/core/getapppassword-onetime" + var result: String? = null + runCatching { + val response = getResponseOfOTPRequest(url, oneTimeCredentials) + val jsonObject: JsonObject = JsonParser.parseString(response).asJsonObject + val appPassword = jsonObject + .getAsJsonObject("ocs") + .getAsJsonObject("data") + .get("apppassword").asString + + result = appPassword + }.getOrElse { e -> + when (e) { + is SSLHandshakeException, + is NullPointerException, + is IOException -> { + Log.e(TAG, "Error caught at oneTimePasswordRequest: $e") + } + + else -> throw e + } + } + + return result + } + + private fun getResponseOfOTPRequest(url: String, oneTimeCredentials: String): String? { + val request = Request.Builder() // GET is default + .url(url) + .header("Authorization", oneTimeCredentials) + .header("OCS-APIRequest", "true") + .header("Accept", "application/json") + .build() + + val newOkHttpClient = OkHttpClient() + newOkHttpClient.newCall(request).execute().use { response -> + if (!response.isSuccessful) { + throw IOException("Unexpected code $response") + } + return response.body?.string() + } + } + fun anonymouslyPostLoginRequest(baseUrl: String): LoginResponse? { val url = "$baseUrl/index.php/login/v2" var result: LoginResponse? = null diff --git a/app/src/main/java/com/nextcloud/talk/account/viewmodels/BrowserLoginActivityViewModel.kt b/app/src/main/java/com/nextcloud/talk/account/viewmodels/BrowserLoginActivityViewModel.kt index ac0d1399f6..de3ae70b7c 100644 --- a/app/src/main/java/com/nextcloud/talk/account/viewmodels/BrowserLoginActivityViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/account/viewmodels/BrowserLoginActivityViewModel.kt @@ -106,5 +106,23 @@ class BrowserLoginActivityViewModel @Inject constructor(val repository: LoginRep } } + fun loginWithOTPQR(dataString: String, reAuth: Boolean = false) { + viewModelScope.launch { + val loginCompletionResponse = repository.startOTPLoginFlow(dataString, reAuth) + if (loginCompletionResponse == null) { + _postLoginState.value = PostLoginViewState.PostLoginError + return@launch + } + + val bundle = repository.parseAndLogin(loginCompletionResponse) + if (bundle == null) { + _postLoginState.value = PostLoginViewState.PostLoginRestartApp + return@launch + } + + _postLoginState.value = PostLoginViewState.PostLoginContinue(bundle) + } + } + fun cancelLogin() = repository.cancelLoginFlow() }