T3sl4 f34rs my l33t h4ck1ng sk1llz!!!
Some clever hackers figured out how the app works and reverse-engineered the Tesla API. If you knew JSON and how to make HTTP requests, you could control your vehicle without using the official app at all. I could have written my own Android app to do what I wanted, but that wasn’t necessary. Automate by LlamaLab lets you automate various tasks on your smartphone or tablet. Automations can be created by using flowcharts (i.e., flows). It comes with many built-in functions and can interact with the phone including sending SMS, e‑mail, based on your location, time of day, or any other “event trigger.” I used Automate in the past to get my ancient home alarm system to send SMS and e‑mails when the siren gets triggered.
Details about Tesla’s authentication can be found here: https://tesla-api.timdorr.com/api-basics/authentication
Everything worked great until it didn’t
So here I am feeling all happy and proud that I have a car that can keep the temperature of my rear end just right and Tesla throws a wrench into the works. They upgraded the authentication system to OAuth 2.0. It used to be a single login request to get a token. Now there’s a whole bunch of handshaking going on that’s so complicated, I had to break out the token generation part into its own flow.
Enough backstory. Gimme the goods!
You can download the flow here:
Tesla SSO OAuth Token Generation.flo [PDF] [PNG]
You’ll need to import it into your own installation of Automate and edit one of the blocks to put your own e‑mail and password. This flow is under 30 blocks so it technically should run without buying the Premium version of Automate. However, this flow just sets up a telsa.txt file with the authentication token and vehicle information. It doesn’t contain any commands to operate the vehicle. An example that does that will be the subject of the next blog entry.
My Tesla SSO OAuth Token Generation flow assumes that each HTTP request will be successful on its first attempt. I don’t perform retries if any of the steps fail. I do provide error messages in the flow’s log and in the tesla.txt file in case something does go wrong. I’ve run this authentication flow many times and I have not had any failures. Communication with the Tesla server is very reliable, but interacting with the vehicle can be a pain. It may take multiple attempts to wake it up.
This flow does NOT handle Multi-Factor Authentication. I do not have MFA turned on because I don’t want to interact with my flows every time a new token is needed. Modifying the flow for MFA is probably trivial, but I have no desire to do it.
Please feel free to extend and expand on my flows if you wish. Just be kind and provide your enhancements to me and the rest of the community.
Step 1: Obtain the login page
I used a bit of trickery to get Automate to generate the random code_verifier and state variables in Automate. I used a shell command to create a random binary blob of 2,048 bytes stored in a variable called random_bytes:
dd if=/dev/urandom count=1 bs=2048
We only need 84 characters for code_verifier and (arbitrarily) 32 characters for state. The binary blob contains the characters we need and many that we don’t. We need to strip out the undesired values and cut it to the length needed for each variable. This can be done in Automate as follows:
substr(join(findAll(random_bytes, "[a-zA-Z0-9]"), ""), 0, 84)
The “findAll” command strips out the alphanumeric characters in the binary blob and returns them as an array. The “join” command concatenates the characters in the array into a string. Finally, the “substr” command returns the first 84 characters to create the code_verifier. The state variable is made from the next 32 characters. The binary blob was purposely made significantly bigger than the total length of the strings to ensure it contained enough random characters (both valid and invalid) that could be used. The code_challenge is the SHA256 of the code_verifier that is subsequently base64 encoded. I use the flags “wup” to ensure it’s URL safe, doesn’t include the trailing “==” characters, and doesn’t tack on a newline.
The rest of the URL parameters for the GET request are fixed. I created an Automate dictionary with all the parameters for use in the first two steps.
The status code returned should be 200.
Step 2: Obtain an authorization code
Scraping the response from the previous step is easy in Automate using regular expressions. The content body of the POST HTTP request is as follows:
{"_csrf": findAll(response, "(?<=name=\"_csrf\" value=\")[^\"]*")[0], "_phase": findAll(response, "(?<=name=\"_phase\" value=\")[^\"]*")[0], "_process": findAll(response, "(?<=name=\"_process\" value=\")[^\"]*")[0], "transaction_id": findAll(response, "(?<=name=\"transaction_id\" value=\")[^\"]*")[0], "cancel": findAll(response, "(?<=name=\"cancel\" value=\")[^\"]*")[0], "identity": "anon@domain.com", "credential": "password"}
I’m using positive look-behind to scrape the values of the fields. The identity and credential are the login and password of the user’s account. I don’t like embedding it in the flow, but there’s no clean solution other than throwing up a dialog for the user to enter them every time the flow is run.
The header needs the cookie scraped from the previous HTTP request’s header:
{"Cookie": findAll(headers, "tesla-auth.sid=[^;]+")[0]}
This HTTP request REQUIRES that “Don’t follow redirects” is enabled. We do NOT want to follow the redirect URL. We instead want a 302 status code with a response that includes the redirect URL and its parameters.
Step 3: Exchange authorization code for bearer token
This step is pretty easy since almost all of the payload is fixed or previously calculated. Just the “code” variable has to be scraped from the redirect URL in the previous response.
{"grant_type": "authorization_code", "client_id": "ownerapi", "code": findAll(response, "(?<=code=)[^&]*")[0], "code_verifier": random_params["code_verifier"], "redirect_uri": "https://auth.tesla.com/void/callback"}
The status code returned should be 200.
Step 4: Exchange bearer token for access token
The payload for this HTTP request is fixed. The client_id and client_secret may change in the future, but this is not something we can control.
{"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", "client_id": "81527cff06843c8634fdc09e8ac0abefb46ac849f38fe1e431c2ef2106796384", "client_secret": "c7257eb71a564034f9419ee651c7d0e5f7aa6bfbd18bafb5c5c033b093bb2fa3"}
The response from the previous step was pure JSON formatted text. There’s no HTML to parse. We can decode the text and set the bearer access token in the header.
{"Authorization": "Bearer " ++ jsonDecode(response)["access_token"]}
The response from this HTTP request will contain a JSON string of all the information we need to interact with the API using our access token (until it expires). I store this in a dictionary variable called tesla.
The status code returned should be 200.
Step 5: Get vehicle information
While this isn’t part of the authentication process, it retrieves information about our vehicle and stores it in the tesla.txt file along with the information from the final authentication HTTP request. It’s also a good test to ensure our token is valid since it doesn’t require communication with the vehicle.
DISCLAIMER: I only have one Tesla vehicle in my account. If there are multiple vehicles on the same account, this will only get the information for the first one. To select a different vehicle, change the subscript in the “Variable set” block from “0” to whatever number will point to the desired vehicle. Maybe one day when I have more than one Tesla, I’ll revisit this unless someone else want to tackle this enhancement (hint hint).
Error handling
Errors are determined by the expected status code returned by the HTTP request. The log shows the step where the error occurred and the tesla.txt file contains a success and error message variable that other flows can use to determine if the last run authentication is valid. Note that an error at a particular step implies that there may be an issue prior to that step.
No comments:
Post a Comment