Someone I know got a Tesla some time ago, an amazing car. I can’t think of anything geekier. What can be geekier than a car that ships with auto-updates and … an API! (albeit unofficial, more below).

So naturally, this became a nice weekend project. I wanted to write a little SMS app that could send commands to the car. For example:

status

would return:

Battery: 56%
Range: 110 miles
Charging port door is OPEN
Currently: CHARGING
Time to fully charge: 4 hs

The API

This site documents all endpoints and available commands.

Notice that this is unofficial. It is not blessed by Tesla in any way. Use it at your own peril.

It is pretty comprehensive, covering all aspects of the car. You can do lots of things:

  1. Retrieving a bunch of information
  2. Acting on the car: honk, change temperature, open garage door (if HomeLink is configured), change sentry mode, and on and on.

Authentication

All endpoints require a bearer token (nice!). To get one, you need to authenticate using the /token endpoint which is essentially the OAuth2 Password Grant endpoint. This is not terrible, but not great for my project, because it requires capturing credentials and sending them over the wire.

I can understand why Tesla might have chosen this implementation. Their’s is a closed API, the client being their own application. It is not great, because you wouldn’t want to type username/password on SMS. It would stay there for a long time in cleartext.

I need a mechanism to authenticate securely outside SMS, store the Tesla token and then safely retrieve it every time I need to call the API.

I also need a way to associate a phone with the specific login. And Auth0 gives me all the building blocks for this:

  1. I can create a login that authenticates the user against Tesla and gives me an access_token
  2. It can store the access_token securely
  3. It can validate a phone number and associate it with a user
  4. It provides an API to tie everything together

Tesla Login

Because Tesla uses the password grant I created a custom database connection with the following script:

function login(email, password, callback) {
  const request = require('request');

  request.post({
    url: 'https://owner-api.teslamotors.com/oauth/token?grant_type=password',
    json: {
    "grant_type": "password",
    "client_id": configuration.CLIENT_ID,
    "client_secret": configuration.CLIENT_SECRET,
    "email": email,
    "password": password
  },
    headers: {
      "User-Agent": "my tesla experiment"
    }
  }, function(err, response, body) {
    if(err) return callback(err);
    if(response.statusCode === 401) return callback();
    const user = body;
    callback(null, {
      user_id: email,
      email: email,
      access_token: user.access_token,
    });
  });
}

Returning the access_token in the user profile object in the last callback, automatically stores it in Auth0’s user store.

You need a special scope in the Management API token to be able to retrieve these sensitive attributes: scope: read:user_idp_tokens

The first step is done.

Associating phone with user

The easiest way I’ve found for this is just turning SMS MFA. This will force an enrollment and a verification of the phone just after login. So we have proof that: (a) the user has access to the phone and (b) was able to login with Tesla.

Auth0 supports SMS based MFA ot of the box.

But there’s a caveat. Because the user interactions happen exclusively through SMS, the only information we have when a message arrives through Twilio is the phone number. We need a way to find the user with that given phone number (and that is verified).

Unfortunately, Auth0 doesn’t have an API to search users by MFA enrollment. But it does have a fairly powerful search on the user profile (including arbitrary information and data structures).

User metadata can be added to any user. See here

The solution is to add the phone number as part of the app_metadata object just after login. When we know that (a) the user is authenticated, and (b) the phone has been verified.

Let’s see the 2 big phases for this:

BrowserTesla AppAuth0Tesla APIAuth0 MFAPhone/loginredirect auth0.com/authorize/authorize{username/password}POST /token {username/password}{access_token}send SMSMFA Challenge{SMS}Enter codeValidate codeValidatedredirect /callback/callback

This first part is just the normal authentication, using 3-legged authorization, a custom “database” connection using the Tesla Auth API plus SMS MFA. The diagram shows the “happy path”.

The fact that Tesla uses Password Grant, doesn’t mean that our app cannot use 3-legged. Auth0 is hosting the login page for us.

Now, the second stage for this that happens all in the final “leg” of the 3-legged process (essentially in the /callback handler):

BrowserTesla AppAuth0/callbackexchange code/token{ access_token }GET /userprofile{ sub: user_id }GET /MFA_enrollments{ phone }Update App_metadata { user_id, phone }Done!

Again, this is the happy path. At the end of this, we end up with a user object in Auth0 that will look like this:

{
    "email": "you@example.com",
    "updated_at": "2019-12-02T17:05:52.345Z",
    "user_id": "auth0|you@example.com",
    "picture": "https://s.gravatar.com/avatar/9303fma.png",
    "nickname": "you",
    "identities": [
        {
            "user_id": "you@example.com",
            "provider": "auth0",
            "connection": "tesla",
            "access_token": "1234567ijhgfds4567ujhgfe4567ujhgfder",
            "isSocial": false
        }
    ],
    "created_at": "2019-03-15T15:56:44.577Z",
    "multifactor": [
        "guardian"
    ],
    "multifactor_last_modified": "2019-11-25T23:22:01.724Z",
    "user_metadata": {},
    "app_metadata": {
        "phone": "+14251112222"
    },
    "last_ip": "192.168.1.10",
    "last_login": "2019-12-02T16:35:50.204Z",
    "logins_count": 8,
    "blocked_for": [],
    "guardian_authenticators": [
        {
            "id": "sms|dev_87578rhjbfsd4",
            "type": "sms",
            "confirmed": true,
            "name": "+1 4251112222",
            "created_at": "2019-11-25T23:21:42.000Z",
            "last_auth_at": "2019-11-28T03:11:38.000Z"
        },
        {
            "id": "recovery-code|dev_TCskjdfhsjkfhdjk",
            "type": "recovery-code",
            "confirmed": true,
            "created_at": "2019-11-25T23:21:41.000Z",
            "enrolled_at": "2019-11-25T23:21:52.000Z"
        }
    ]
}

The nodejs auth0 module makes this really easy:

code is the authorization code returned in the 2 step of the authorization process:

function completeAuthEnrollment(code, done){

  var locals = {}; 

  // The payload for the code exchange
  var data = {
    code: code,
    redirect_uri: process.env.AUTH0_CALLBACK,
    client_id: process.env.AUTH0_CLIENT_ID,
    client_secret: process.env.AUTH0_CLIENT_SECRET
  };

  //Auth0 client used for authentication/user 
  var auth = new AuthenticationClient({
    domain: process.env.AUTH0_DOMAIN
  });

  //Auth0 client used for Mgmt API
  var auth0 = new ManagementClient({
    domain: process.env.AUTH0_DOMAIN,
    clientId: process.env.AUTH0_CLIENT_ID,
    clientSecret: process.env.AUTH0_CLIENT_SECRET
  });

  async.series([
    cb => {
      //1. Exchange code for token
      auth.oauth.authorizationCodeGrant(data, (err, token) => {
        if(err){
          return cb(err);
        }
        locals.token = token;
        cb();
      });
    },
    //2. Get user id
    cb => {
      auth.getProfile(locals.token.access_token, (err, userInfo) => {
        if(err){
          return cb(err);
        }
        locals.user_id = userInfo.sub;
        cb();
      });
    },
    //3. Get MFA enrollments
    cb => {
      auth0.getGuardianEnrollments({ id: locals.user_id }, (err, enrollments) => {
        if(err){
          return cb(err);
        }
        if(!enrollments || enrollments.length === 0){
          return cb('No MFA!');
        }
        //Search for SMS enrollment
        const enrollment = _.find(enrollments, (en) => { return (en.status === "confirmed" && en.type === "sms") });
        if(!enrollment){
          return cb('No SMS MFA found');
        }
        //Remove any spaces (Auth0 formats phone as "+1425 111333")
        locals.phone = enrollment.phone_number.replace(' ', '');
        cb();
      });
    },
    //4. Save phone in app_metadata
    cb => {
      auth0.updateAppMetadata({id: locals.user_id}, {phone: locals.phone}, (err, user) =>{
        if(err){
          return cb(err);
        }
        cb();
      });
    }
    ],
    (e) => {
      done(e, "Enrollment complete!");  
    });
};

Notice that there are 2 instances of the auth0 module in use:

  1. One used for user auth related APIs (e.g. exchange code, get profile). The first 2 steps in the series above.
  2. Another used for Management API (e.g. get enrollments, update metadata). The last 2 steps.

And the latter needs a different access_token. One that has the right scopes for these operations: read:users and update:users. We need to search and then update.

By default, the Enrollment API returns an obfuscated phone number. There’s a feature flag to enable the phone to be returned: disable_management_api_sms_obfuscation. See here

Processing commands

Now that we’ve got all the pieces in place, and the user is bootstrapped, we can send commands via SMS.

Here’s the overview of how the whole thing works:

PhoneTwilioTesla AppAuth0Tesla"status car"{phone, msg=status car}/searchuser { phone }{ user, access_token }/GET status car{ Battery, Range, ... }Format Message"Battery: 45%..."

The key is the searchUserByPhone function:

function searchSubscriberByPhone(phone, done){
  
  const auth0 = new ManagementClient({
    domain: process.env.AUTH0_DOMAIN,
    clientId: process.env.AUTH0_CLIENT_ID,
    clientSecret: process.env.AUTH0_CLIENT_SECRET
  });

  auth0
    .getUsers({
      search_engine: 'v3',
      per_page: 10,
      page: 0,
      q: util.format('app_metadata.phone:"%s"', phone)
    })
    .then(users => {
      if(!users || users.length === 0){
        return done('User not found');
      }
      done(null, users[0]); //Right now we limit for 1 (the first we find)
    })
    .catch(err => {
      done(err);
    });
}

There are a few caveats with this implementation that my colleague Eva Sarafianou, pointed out:

  1. As it is now, many accounts could be potentially associated with the same phone. There’s no protection for that right now. We simply return the first user we find with teh associated phone. Perhaps, as she suggested we could use SMS Passwordless instead of MFA and use the account linking feature.
  2. Likewise, I have not implemented anything to contemplate the situation of the same user having multiple phones. Likely, a more common use. (e.g. I have 2 phones: work and personal).
  3. The situation in which you would own multiple cars (lucky you!), is actually not complicated. A single Tesla account can have many cars associated with it. All car specific APIs require the vehicleId parameter. And because cars can have names, you can easily lookup the id based on it.

An oddity I found with standard JSON parsing in nodejs, the vehicle id property returned by the Tesla API is a very large integer, which JSON.parse unhelpfully rounds. Being an id it naturally doesn’t help. Thankfully, the API also returns id_s (_s for string presumably) which works just fine. We don’t really care what the type of the id is.

Recap on all authentications happening

  1. The first authentication happens between Twilio and the API. Twilio will send a signature on every request. This guarantees the request is originted by them. See here for details. In nodejs (with Express, it is trivial to add this validation with the webhook() middleware:
/*------------ Twilio App Main ---------------*/
server.post('/', twilio.webhook(), smsHandler);

function smsHandler(req, res, next){
 //code here
}
  1. The second one is User authentication. We are using a combination of (a) a custom db connection to authenticate the user credentials against the Tesla API (b) use Auth0 authorization code flow.

  2. The App backend itself, using client credentials. Our backend needs to call Auth0’s API for user search, query SMS enrollements, and update application metadata. The scope of access is restricted to tehse 2 operations.

Final caveats

Tesla’s API is not officialy documented. This is all using information available on the web. Of course Tesla might change anything without any notice. Again, beware that some operations on the API allow you to have your car do things. If you own a Tesla and build anything using these APIs, it is your own responsibility. The purpose of this post is really to illustrate the techinques to bridge SMS with a token based API, and how Auth0 helps you with that.