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:
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.
Here's the logs that are generated when this is run and sign in with valid credentials are provided: