-
-
Notifications
You must be signed in to change notification settings - Fork 536
Closed
Description
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
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels