Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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
}

Expand Down Expand Up @@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Loading