fluttertesla

Flutter Web Auth: How to intercept the redirect uri (Tesla API)


I'm using flutter_web_auth_2 to build a web app that will use Oauth2 to access my Tesla account. I'm running this on the Web.

Tesla has a fixed redirect URI of https://auth.tesla.com/void/callback. I can use the authenticate method which opens a new tab at the tesla authorization end point where I login with my credentials. However, my app does not get the redirect URL and instead I get a page not found from Tesla.

The flutter_web_auth_2 docs mention something about creating an endpoint with a page that publishes the redirect to my app but I'm not sure how to setup an endpoint at the this fixed address (https://auth.tesla.com/void/callback) within my app. Is this even possible?

If not, how would I get the redirect results? How do other sites that allow third-party apps to use OAuth2 to login a user and intercept the redirect_uri? Do they get to specify custom URIs within their apps domains?

Any help would be appreciated.


EDIT Thanks a bunch for the help. I'm posting my code here for your reference. I'm a flutter / dart noob so please forgive my untidy or immature code. If you see anything I would appreciate any input. I gather I should try to perform the authentication in a mobile app (Like Android) and see if I can find some success before trying the webapp.

I created an abstract class which basically has an authenticate method (the implementation invokes flutter_web_auth2)

abstract class WebAuthService {
  final String authUrl;
  final String urlScheme;

  WebAuthService(this.authUrl, this.urlScheme);

  Future<String> authenticate();
}

The implementation of the above service is

class TeslaAuth extends WebAuthService
{
  TeslaAuth() : super("https://auth.tesla.com/oauth2/v3/authorize", "https");

  @override
  Future<String> authenticate() async {
    var request = TeslaAuthRequestHelper().asRequest();
    var response = await http.Client().get(request);
    if(response.statusCode == 200) {
      return response.body;
    } else {
      return "";
    }
  }
}

This uses a TeslaAuthRequestHelper which creates the request as per the unofficial documentation (step 1)

class TeslaAuthRequestHelper
{

  final clientId = "81527cff06843c8634fdc09e8ac0abefb46ac849f38fe1e431c2ef2106796384";
  final clientSecret = "c7257eb71a564034f9419ee651c7d0e5f7aa6bfbd18bafb5c5c033b093bb2fa3";
  late final String codeVerifier;
  late final String codeChallenge;
  final codeChallengeMethod = "S256";
  final redirectUri = "https://auth.tesla.com/void/callback";
  final responseType = "code";
  final scope = "openid email offline_access";
  final state = utf8.fuse(base64Url).encode(getRandomString(20));

  static const _chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890';
  static final Random _rnd = Random();

  static String getRandomString(int length) => String.fromCharCodes(Iterable.generate(
      length, (_) => _chars.codeUnitAt(_rnd.nextInt(_chars.length))));
  TeslaAuthRequestHelper() {
    codeVerifier = getRandomString(87);
    //Creates a codec fused with a ut8f and then a base64url codec.
    //The encode on the resulting codec will first encode the string to utf-8
    //and then to base64Url
    codeChallenge = utf8.fuse(base64Url).encode(codeVerifier);
  }

  Uri asRequest() {
    return Uri.https('auth.tesla.com', 'oauth2/v3/authorize', {
      "client_id": "ownerapi",
      // "client_secret": clientSecret,
      "code_challenge": codeChallenge,
      "code_challenge_method": codeChallengeMethod,
      "redirect_uri": redirectUri,
      "response_type": responseType,
      "scope": scope,
      "state": state,
    });
  }
}

And finally I have a main widget that has an Authenticate button in the middle to kick off and then tries to display a result which I was hoping would be the redirect URL, however I'm not sure how to get it.

final authService = TeslaAuthService();
final auth2 = TeslaAuth();

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Tesla Authorizer',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Tesla Web Auth Demo'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});
  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  String _result = '';

  @override
  void initState() {
    super.initState();
    print("Current Uri : ${Uri.base}");
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('Result: $_result\n'),
            const SizedBox(height: 80),
            ElevatedButton(
                onPressed: () async {
                  _result = await authService.authenticate();
                  print(_result);
                },
                child: const Text('Authenticate'))
          ],
        ),
      ),
    );
  }
}

I tried this on a Pixel 4a, but I login on the portal but the webview doesn't return back to my app and I get the page not found, with redirect URL in the address bar. I added to my manifest file an extra activity tag as suggested by the flutter_web_auth_2 docs.

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application
        android:label="tesla_auth"
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:launchMode="singleTop"
            android:theme="@style/LaunchTheme"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize">
            <!-- Specifies an Android theme to apply to this Activity as soon as
                 the Android process has started. This theme is visible to the user
                 while the Flutter UI initializes. After that, this theme continues
                 to determine the Window background behind the Flutter UI. -->
            <meta-data
              android:name="io.flutter.embedding.android.NormalTheme"
              android:resource="@style/NormalTheme"
              />
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <activity
                android:name="com.linusu.flutter_web_auth_2.CallbackActivity"
                android:exported="true">
            <intent-filter android:label="flutter_web_auth_2">
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data android:scheme="https" />
            </intent-filter>
        </activity>
        <!-- Don't delete the meta-data below.
             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
        <meta-data
            android:name="flutterEmbedding"
            android:value="2" />
    </application>
</manifest>

Solution

  • To answer your last question first, yes, other sites that allow third-party apps to use OAuth2 will allow the third-party to specify a custom redirect URI.

    Tesla's API isn't official, so there isn't any way to set an OAuth redirect URL other than the /void/callback/ URL that they use internally.

    After a little bit of searching, I found https://github.com/carlosonunez/deprecated-tesla-oauth2-token-bot, which says:

    Tesla's Auth API has enabled reCAPTCHA on /oauth2/v3/authorize and /oauth2/v1/authorize. Moreover, Tesla provides the code in a Location header, which browsers will redirect to automatically. Tesla will present a 404 page upon doing this. Consequently, the only way to work around this is to use a webdriver and capture redirects, which is only possible (or documented) with Chrome CDP or a proxy server fronting the webdriver, both of which are...a lot of work to get an OAuth token.

    Consider using a mobile app with an embedded webview for your token generation needs.

    Without code, it's hard to say what could be going wrong in your implementation.

    Rather than opening login in a new tab, you could consider an embedded webview in your page that tracks the URL and captures the login token when it detects that the user has finished logging in (when they reach the /void/callback/ page).

    Unfortunately Flutter Web doesn't have a lot of great support for webview/iframe. https://pub.dev/packages?q=platform%3Aweb+webview reveals a few options, some of them archived, and some of them featureless. https://pub.dev/packages/webviewx_plus may be a good package to try first, since it seems to have callbacks you can register to capture URL changes.

    The flutter_web_auth_2 plugin you are using seems like it might work for this though. If you publish the relevant code, I'd be happy to see if there's anything you've done incorrectly.