Skip to content
This repository has been archived by the owner on Jan 12, 2024. It is now read-only.

Commit

Permalink
feat: auth fingerprint + improve login flow
Browse files Browse the repository at this point in the history
  • Loading branch information
rushiiMachine committed Feb 26, 2023
1 parent ae754ad commit 0ed6976
Show file tree
Hide file tree
Showing 8 changed files with 205 additions and 103 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,7 @@ data class EntityAccount(

@ColumnInfo(name = "cookies")
val cookies: String? = null,

@ColumnInfo(name = "fingerprint")
val fingerprint: String? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ fun ApiLogin.toDomain(): DomainLogin {
DomainLogin.Login(
token = token,
mfa = mfa,
theme = userSettings?.theme ?: "",
locale = userSettings?.locale ?: "",
theme = userSettings?.theme!!,
locale = userSettings.locale,
)
}
else -> DomainLogin.Error
Expand Down
9 changes: 3 additions & 6 deletions app/src/main/java/com/xinto/opencord/rest/body/Login.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,9 @@ data class LoginBody(
@SerialName("password")
val password: String,

@SerialName("captcha_key")
val captchaKey: String? = null,

@SerialName("undelete")
val undelete: Boolean = false,
val undelete: Boolean,

@SerialName("login_source")
val loginSource: String? = null,
@SerialName("captcha_key")
val captchaKey: String? = null,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.xinto.opencord.rest.models

import kotlinx.serialization.Serializable

// Only used for fetching the fingerprint in auth
@Serializable
data class ApiExperiments(
val fingerprint: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -3,54 +3,93 @@ package com.xinto.opencord.rest.service
import com.xinto.opencord.BuildConfig
import com.xinto.opencord.rest.body.LoginBody
import com.xinto.opencord.rest.body.TwoFactorBody
import com.xinto.opencord.rest.models.ApiExperiments
import com.xinto.opencord.rest.models.login.ApiLogin
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

private val HttpHeaders.XFingerprint: String
get() = "X-Fingerprint"

interface DiscordAuthService {
suspend fun login(body: LoginBody): Pair<ApiLogin, List<Cookie>>
suspend fun verifyTwoFactor(body: TwoFactorBody, cookies: List<Cookie>?): ApiLogin
suspend fun getFingerprint(): Pair<String, List<Cookie>>

suspend fun login(
body: LoginBody,
existingCookies: List<Cookie>?,
xFingerprint: String,
): ApiLogin

suspend fun verifyTwoFactor(
body: TwoFactorBody,
existingCookies: List<Cookie>?,
xFingerprint: String,
): ApiLogin
}

class DiscordAuthServiceImpl(
private val client: HttpClient
) : DiscordAuthService {
override suspend fun login(body: LoginBody): Pair<ApiLogin, List<Cookie>> {
val url = getLoginUrl()
override suspend fun getFingerprint(): Pair<String, List<Cookie>> {
return withContext(Dispatchers.IO) {
val response = client.post(url) {
setBody(body)
val response = client.get(getExperimentsUrl())
val data = response.body<ApiExperiments>()
val fixedCookies = response.setCookie().map { cookie ->
if (cookie.domain !== null) {
cookie
} else cookie.copy(
domain = BASE_DOMAIN,
)
}

response.body<ApiLogin>() to getSetCookies(response)
data.fingerprint to fixedCookies
}
}

override suspend fun verifyTwoFactor(body: TwoFactorBody, cookies: List<Cookie>?): ApiLogin {
val url = getTwoFactorUrl()
override suspend fun login(
body: LoginBody,
existingCookies: List<Cookie>?,
xFingerprint: String,
): ApiLogin {
return withContext(Dispatchers.IO) {
client.post(url) {
client.post(getLoginUrl()) {
setBody(body)
setCookies(existingCookies)
header(HttpHeaders.XFingerprint, xFingerprint)
}.body()
}
}

if (cookies != null) {
header(HttpHeaders.Cookie, cookies.joinToString("; ") { renderCookieHeader(it) })
}
override suspend fun verifyTwoFactor(
body: TwoFactorBody,
existingCookies: List<Cookie>?,
xFingerprint: String,
): ApiLogin {
return withContext(Dispatchers.IO) {
client.post(getTwoFactorUrl()) {
setBody(body)
setCookies(existingCookies)
header(HttpHeaders.XFingerprint, xFingerprint)
}.body()
}
}

private fun getSetCookies(response: HttpResponse): List<Cookie> {
return response.headers.getAll(HttpHeaders.SetCookie)
?.map { parseServerSetCookieHeader(it) } ?: emptyList()
private fun HttpRequestBuilder.setCookies(cookies: List<Cookie>?) {
if (!cookies.isNullOrEmpty()) {
val stringCookies = cookies.joinToString("; ") {
renderCookieHeader(it)
}
header(HttpHeaders.Cookie, stringCookies)
}
}

private companion object {
const val BASE = BuildConfig.URL_API
val BASE_DOMAIN = Url(BASE).host

fun getLoginUrl(): String {
return "$BASE/auth/login"
Expand All @@ -59,5 +98,9 @@ class DiscordAuthServiceImpl(
fun getTwoFactorUrl(): String {
return "$BASE/auth/mfa/totp"
}

fun getExperimentsUrl(): String {
return "$BASE/experiments"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ fun LoginScreen(
Spacer(Modifier.weight(1f))
Button(
modifier = Modifier.fillMaxWidth(),
onClick = { viewModel.login() },
onClick = { viewModel.login(null) },
enabled = !viewModel.isLoading,
) {
Text(stringResource(R.string.login_action_login))
Expand Down Expand Up @@ -151,7 +151,7 @@ fun LoginScreen(
},
onDismissRequest = viewModel::dismissMfa,
confirmButton = {
Button(onClick = { viewModel.verifyTwoFactor(viewModel.mfaCode) }) {
Button(onClick = viewModel.verify2fa) {
Text(stringResource(R.string.login_mfa_confirm))
}
},
Expand Down

0 comments on commit 0ed6976

Please sign in to comment.