Skip to content

Examples of successful login on Android #781

@Odaym

Description

@Odaym

Hi, so I'd like to know if there are any examples of a successful login attempt on Android, any example apps, something I can use to help me do it for my own app.

Here's what I've tried so far but I'm getting stuck at the challenge step.

class TeslaAuthViewModel : ViewModel() {

    private lateinit var codeVerifier: String
    private lateinit var codeChallenge: String

    private val _authState = MutableLiveData<String>()
    val authState: LiveData<String> = _authState

    init {
        generateCodeVerifierAndChallenge()
    }

    private val retrofit = Retrofit.Builder()
        .baseUrl("https://auth.tesla.com/")
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    private val teslaAuthApi = retrofit.create(TeslaAuthApi::class.java)

    private fun generateCodeVerifierAndChallenge() {
        codeVerifier = generateRandomString(86)
        codeChallenge = generateCodeChallenge(codeVerifier)
        Log.d("TeslaAuthViewModel", "Code Verifier: $codeVerifier")
        Log.d("TeslaAuthViewModel", "Code Challenge: $codeChallenge")
    }

    private fun getAuthUrl(loginHint: String? = null): String {
        val authUrl = Uri.parse("https://auth.tesla.com/oauth2/v3/authorize").buildUpon()
            .appendQueryParameter("client_id", "ownerapi")
            .appendQueryParameter("code_challenge", codeChallenge)
            .appendQueryParameter("code_challenge_method", "S256")
            .appendQueryParameter("redirect_uri", "https://auth.tesla.com/void/callback")
            .appendQueryParameter("response_type", "code")
            .appendQueryParameter("scope", "openid email offline_access")
            .appendQueryParameter("state", generateRandomString(16))
            .apply {
                loginHint?.let {
                    appendQueryParameter("login_hint", it)
                }
            }
            .build()
            .toString()

        Log.d("TeslaAuthViewModel", "Auth URL: $authUrl")

        return authUrl
    }

    fun handleRedirectUri(uri: Uri) {
        Log.d("TeslaAuthViewModel", "Handling Redirect URI: $uri")
        val code = uri.getQueryParameter("code")
        if (code != null) {
            Log.d("TeslaAuthViewModel", "Authorization Code: $code")
            exchangeAuthorizationCode(code)
        } else {
            Log.e("TeslaAuthViewModel", "Authorization failed: No code found in redirect URI")
            _authState.postValue("Authorization failed")
        }
    }

    private fun exchangeAuthorizationCode(code: String) {
        viewModelScope.launch {
            try {
                Log.d("TeslaAuthViewModel", "Exchanging Authorization Code for Token")
                val response = teslaAuthApi.exchangeCode(
                    mapOf(
                        "grant_type" to "authorization_code",
                        "client_id" to "ownerapi",
                        "code" to code,
                        "code_verifier" to codeVerifier,
                        "redirect_uri" to "https://auth.tesla.com/void/callback"
                    )
                )

                if (response.isSuccessful) {
                    val tokenResponse = response.body()
                    Log.d("TeslaAuthViewModel", "Token Response: $tokenResponse")
                    _authState.postValue(tokenResponse?.accessToken ?: "Authorization failed")
                } else {
                    Log.e("TeslaAuthViewModel", "Authorization failed: ${response.errorBody()?.string()}")
                    _authState.postValue("Authorization failed")
                }
            } catch (e: Exception) {
                Log.e("TeslaAuthViewModel", "Authorization failed with exception", e)
                _authState.postValue("Authorization failed")
            }
        }
    }

    private fun generateRandomString(length: Int): String {
        val allowedChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
        return (1..length)
            .map { allowedChars.random() }
            .joinToString("")
    }

    @SuppressLint("NewApi")
    private fun generateCodeChallenge(verifier: String): String {
        val bytes = verifier.toByteArray(StandardCharsets.US_ASCII)
        val md = MessageDigest.getInstance("SHA-256")
        val digest = md.digest(bytes)
        return Base64.getUrlEncoder().withoutPadding().encodeToString(digest)
    }

    private suspend fun extractHiddenFields(html: String): Map<String, String> {
        val document = Jsoup.parse(html)
        val hiddenFields = mutableMapOf<String, String>()
        document.select("input[type=hidden]").forEach { element ->
            hiddenFields[element.attr("name")] = element.attr("value")
        }
        Log.d("TeslaAuthViewModel", "Hidden Fields: $hiddenFields")
        return hiddenFields
    }

    private suspend fun checkForChallenge(html: String): Boolean {
        return html.contains("sec_chlge_form")
    }

    private suspend fun submitChallengeForm(html: String, cookie: String) {
        val document = Jsoup.parse(html)
        val challengeForm = document.select("form#chlge").first()
        val action = challengeForm?.attr("action") ?: ""
        val hiddenFields = mutableMapOf<String, String>()
        challengeForm?.select("input[type=hidden]")?.forEach { element ->
            hiddenFields[element.attr("name")] = element.attr("value")
        }
        val challengeUrl = "https://auth.tesla.com$action"
        Log.d("TeslaAuthViewModel", "Submitting Challenge Form: $hiddenFields")
        val response = teslaAuthApi.submitLoginForm(challengeUrl, hiddenFields, cookie)
        if (response.isSuccessful) {
            val locationHeader = response.headers()["location"]
            Log.d("TeslaAuthViewModel", "Location Header: $locationHeader")
            if (locationHeader != null) {
                val uri = Uri.parse(locationHeader)
                handleRedirectUri(uri)
            } else {
                Log.e("TeslaAuthViewModel", "Authorization failed: Location header is null")
                _authState.postValue("Authorization failed")
            }
        } else {
            Log.e("TeslaAuthViewModel", "Challenge Form Submission Response Body: ${response.body()?.string()}")
            _authState.postValue("Authorization failed")
        }
    }

    suspend fun submitLoginForm(
        url: String,
        hiddenFields: Map<String, String>,
        email: String,
        password: String,
        cookie: String
    ): Response<ResponseBody> {
        val formBody = hiddenFields.toMutableMap().apply {
            put("identity", email)
            put("credential", password)
        }

        Log.d("TeslaAuthViewModel", "Submitting Login Form: $formBody")

        return teslaAuthApi.submitLoginForm(url, formBody, cookie)
    }

    fun performLogin(email: String, password: String) {
        viewModelScope.launch {
            try {
                val authUrl = getAuthUrl(email)
                val initialResponse = teslaAuthApi.getLoginPage(authUrl)
                if (initialResponse.isSuccessful) {
                    val html = initialResponse.body()?.string() ?: ""
                    Log.d("TeslaAuthViewModel", "Initial Login Page Response Body: $html")
                    val hiddenFields = extractHiddenFields(html)
                    val cookie = initialResponse.headers()["set-cookie"] ?: ""
                    Log.d("TeslaAuthViewModel", "Initial Cookie: $cookie")
                    val submitResponse = submitLoginForm(authUrl, hiddenFields, email, password, cookie)
                    if (submitResponse.isSuccessful) {
                        val responseBody = submitResponse.body()?.string() ?: ""
                        Log.d("TeslaAuthViewModel", "Form Submission Response Body: $responseBody")
                        if (checkForChallenge(responseBody)) {
                            submitChallengeForm(responseBody, cookie)
                        } else {
                            val locationHeader = submitResponse.headers()["location"]
                            Log.d("TeslaAuthViewModel", "Location Header: $locationHeader")
                            if (locationHeader != null) {
                                val uri = Uri.parse(locationHeader)
                                handleRedirectUri(uri)
                            } else {
                                Log.e("TeslaAuthViewModel", "Authorization failed: Location header is null")
                                _authState.postValue("Authorization failed")
                            }
                        }
                    } else {
                        Log.e("TeslaAuthViewModel", "Form Submission Response Body: ${submitResponse.body()?.string()}")
                        _authState.postValue("Authorization failed")
                    }
                } else {
                    Log.e("TeslaAuthViewModel", "Initial Login Page Request failed: ${initialResponse.errorBody()?.string()}")
                    _authState.postValue("Authorization failed")
                }
            } catch (e: Exception) {
                Log.e("TeslaAuthViewModel", "Authorization failed with exception", e)
                _authState.postValue("Authorization failed")
            }
        }
    }
}

Here's the logs that are generated when this is run and sign in with valid credentials are provided:

 Hidden Fields: {_csrf=kNAnroAB-lFfZjT_dZmw5eWbwIQ9O7zE4tDo, _phase=authenticate, cancel=, transaction_id=Tzhufv3Z, fingerPrint=, _process=1, change_identity=, identity=csopacua@outlook.com, correlation_id=fe9ad78b-22c1-43c3-bc15-628b43560851, auth_method=email-login, =}


12:40:57.355  D  Initial Cookie: bm_sz=E5DB6D7A53F472FA5BEAF6A6D2D9471A~YAAQVQcQAgpZjkaQAQAAzJN7ghiG+LAwgtBBWMTfFcjnKvnMTnuKqQora/1o2fcy0N3aN94P1214i29T9UEj2b3C92Ci8yQIOno65Qfz4ecry7VNx5ogFV2NePRQAB2c1nneNyW/Wer1LF2gvgTHGX8zqbSwrWjUXI8oTw8F9PJxTh91E0QavW4NBtcRvjQjiZNScJDucaJ6dBssd/r8qSZg/jOAn7ZFtgkSj0wLRxtJboEnFJ6dalrRTGcXQYrCDaE56rHnPhZ+sbRFqwH5ZWu4TKPx95jLXKcG5j7D5V1PctRnKpxiMRL99gfoM0vQFzgwuObl+FR5gSvOhaJpe+jVA23jw5KKrC8E4/qjWCNoVeoAFtI=~4343346~3491376; Domain=.tesla.com; Path=/; Expires=Fri, 05 Jul 2024 14:40:54 GMT; Max-Age=14399


12:40:57.356  D  Submitting Login Form: {_csrf=kNAnroAB-lFfZjT_dZmw5eWbwIQ9O7zE4tDo, _phase=authenticate, cancel=, transaction_id=Tzhufv3Z, fingerPrint=, _process=1, change_identity=, identity=csopacua@outlook.com, correlation_id=fe9ad78b-22c1-43c3-bc15-628b43560851, auth_method=email-login, =, credential=PGToTheMo0n!*}
12:40:57.696  D  Form Submission Response Body: <!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"><title>Challenge Validation</title><link rel="stylesheet" type="text/css" href="/_sec/cp_challenge/sec-4-5.css"><script type="text/javascript">function cp_clge_done(){document.sec_chlge_form.submit(document.location.href)}</script><script src="/_sec/cp_challenge/sec-cpt-int-4-5.js" async defer></script><script type="text/javascript">sessionStorage.setItem('data-duration', 30);</script></head><body><div class="sec-container"><div id="sec-text-container"><iframe id="sec-text-if" class="custmsg" src="/_sec/cp_challenge/abc_message-4-5.htm"></iframe></div><div id="sec-if-container"><iframe id="sec-cpt-if" provider="adaptive"   class="adaptive" data-key="" data-duration=30 src="/_sec/cp_challenge/abc-challenge-4-5.htm"></iframe></div><form id="chlge" name="sec_chlge_form" method="POST" action="/oauth2/v3/authorize?client_id=ownerapi&code_challenge=_iZ1Le_NPuJWikY-zNK6qJ8e0RGIp16fJoDzlJVDi7Y&code_challenge_method=S256&redirect_uri=https%3A%2F%2Fauth.tesla.com%2Fvoid%2Fcallback&response_type=code&scope=openid%20email%20offline_access&state=jdod33TKOKU2pAPu&login_hint=csopacua%40outlook.com"><input type="hidden" name="sec_chlge_forward_wrap" value="AAQAAAAJ/////yn/oHO+kJiwa3l1lmVXdcpSm+rZa8snV5gUi8LPhvF86trxfuhzMo2kdM4qrTDlVoH0d8wlyq6Tksle0MOpbMUfnhxRO5vGYWMwgf5Sv+hR5GW6znyiZ9AGG5mgmp32JiqIWqy28FHAyGf8gtYSJelmhTjGAsBFuNmJ/D4CbbmW5QKrBoXFfUxlpACPXRprKyzsEybvn/TpsgiRzvwBAlVBr3B+M69k+fZgGjUzlGkRDDVzwA9Xtmm3sRMGWg11l54kmL57bIzTHokJRye2Xezgd2HgqNcPFG+scFEU4dGUppxtS3IZGNLqTgpCLU9TpQJxYoIgKPdWP8IZfARkB9Kt4tpiwqNyZFDrvX0XMaXbmNOaCkdf28yklzboS6wJvdnWnTTuBqA86QRqZUH8dzoEjZyIRPezMfEZTgNN/Q91"><input type="hidden" name="sec_chlge_content_type_wrap" value="AAQAAAAJ/////2+RvIuX2jNy4V7MYzUqbaORV0J43I7w4lrH584U62wq54+4GYIB7VoPbJnZmC0j7MrC6WWAt9OJ2aaS0CvD6gMRdd2eYQOWSFZoL7NoohZVJg=="></form></div></body></html>



12:40:57.700  D  Submitting Challenge Form: {sec_chlge_forward_wrap=AAQAAAAJ/////yn/oHO+kJiwa3l1lmVXdcpSm+rZa8snV5gUi8LPhvF86trxfuhzMo2kdM4qrTDlVoH0d8wlyq6Tksle0MOpbMUfnhxRO5vGYWMwgf5Sv+hR5GW6znyiZ9AGG5mgmp32JiqIWqy28FHAyGf8gtYSJelmhTjGAsBFuNmJ/D4CbbmW5QKrBoXFfUxlpACPXRprKyzsEybvn/TpsgiRzvwBAlVBr3B+M69k+fZgGjUzlGkRDDVzwA9Xtmm3sRMGWg11l54kmL57bIzTHokJRye2Xezgd2HgqNcPFG+scFEU4dGUppxtS3IZGNLqTgpCLU9TpQJxYoIgKPdWP8IZfARkB9Kt4tpiwqNyZFDrvX0XMaXbmNOaCkdf28yklzboS6wJvdnWnTTuBqA86QRqZUH8dzoEjZyIRPezMfEZTgNN/Q91, sec_chlge_content_type_wrap=AAQAAAAJ/////2+RvIuX2jNy4V7MYzUqbaORV0J43I7w4lrH584U62wq54+4GYIB7VoPbJnZmC0j7MrC6WWAt9OJ2aaS0CvD6gMRdd2eYQOWSFZoL7NoohZVJg==}


12:40:58.740  D  Location Header: null


12:40:58.740  E  Authorization failed: Location header is null

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions