Accessing Twitter with Dart

How to interface with those old OAuth 1 libraries.

Contents

I am working on a web app with a Dart backend that uses Twitter to authenticate users. However, the only Twitter library I found didn’t seem to have any actual provisions for creating request tokens or turning those into access tokens, which is necessary to implement Sign in with Twitter. I rolled this functionality myself, and it took time. Here’s the whole process, so you can get going right away when you need it.

Dependencies

I used the crypto library, along with the HttpClient provided by dart:io. Add to your pubspec.yaml:

dependencies:
    crypto: ^2.0.0

    # For older versions of pub:
    #    crypto: ">=2.0.0 <3.0.0"

Creating a Request Token

To kick off 3-legged authentication, we need to set up an endpoint that asks Twitter for a request token, and then redirects our user. It’s a good idea to have a route serve as this endpoint.

And the TwitterClient functionality is here. This takes a Map of credentials in its constructor. Angel loaded this from my configuration file, which looks like the following. Regardless of the server implementation you are using, make sure you client has access to the following information:

twitter:
  callback: http://localhost:3000/auth/twitter/callback
  key: XXWsmWx79B8Sjf4G7qmkdHMeU
  secret: Bv9UAAHm2B3uJgjvgv5mV6MAvMQqGGLDAMPqUjxpU9ZTgAkIr3
  token_secret: w6OiRo3Pq9ruAktVhOxLAMbdqdCGwAE5KQeZ9KkOSABHd
class TwitterClient {
  final String ENDPOINT = "https://api.twitter.com";
  HttpClient _client = new HttpClient();
  Map<String, String> credentials;

  TwitterClient(Map<String, String> this.credentials);

  Future<Map<String, String>> createRequestToken() async {
    var request = await _prepRequest("/oauth/request_token",
        method: "POST", data: {"oauth_callback": credentials['callback']});

    // _mapifyRequest is a function that sends a request and parses its URL-encoded
    // response into a Map. This detail is not important.
    return _mapifyRequest(request, (body) {
      throw new AngelHttpException.NotAuthenticated(message: body);
    });
  }
}

Authorizing the Request

Every request to Twitter’s API requires authorization via OAuth 1.0. The process to authorize one request is tedious, so I created a function to do this.

Future<HttpClientRequest> _prepRequest(String path,
  {String method: "GET",
  Map data: const {},
  String accessToken,
  String tokenSecret: ""}) async {

  // To build a proper Authorization header, we need to collect ALL parameters,
  // both OAuth parameters and request query/body params, which should be passed as the `data`
  // argument.

  Map headers = new Map.from(data);
  header["oauth_version"] = "1.0";
  header["oauth_consumer_key"] = credentials['key'];

  // The implementation of _randomString doesn't matter - just generate a 32-char
  // alphanumeric string.
  header["oauth_nonce"] = _randomString();
  header["oauth_signature_method"] = "HMAC-SHA1";
  header["oauth_timestamp"] =
      (new DateTime.now().millisecondsSinceEpoch / 1000).round().toString();

  if (accessToken != null) {
    header["oauth_token"] = accessToken;
  }

// Now that we've collected parameters, we need to generate a signature, and add it
// to our headers.
var request = await _client.openUrl(method, Uri.parse("$ENDPOINT$path"));

headers['oauth_signature'] = _createSignature(method,
    request.uri.toString().replaceAll("?${request.uri.query}", ""), headers,
    tokenSecret: tokenSecret);

// Set the OAuth header and return the request
var oauthString = headers.keys
    .map((name) => "$name=\"${Uri.encodeComponent(headers[name])}\"")
    .join(", ");

return request
  ..headers.set(HttpHeaders.AUTHORIZATION, "OAuth $oauthString");
}

Creating a Signature

Lastly, to generate a signature, you need import dart:convert, dart:collection, and crypto. The SplayTreeMap sorts its keys. This is perfect because Twitter requires us to sort our parameters in alphabetical order before generating a request.

crypto exposes two converters which we need to generate an HMAC-SHA1 signature, which Twitter requires: Hmac and sha1 (who would’ve guessed?).

Lastly, we need dart:convert to Base64-encode our generated hash.

String _createSignature(
      String method, String uriString, Map<String, String> params,
      {String tokenSecret: ""}) {

    // Not only do we need to sort the parameters, but we need to URI-encode them as well.
    var encoded = new SplayTreeMap();
    for (String key in params.keys) {
      encoded[Uri.encodeComponent(key)] = Uri.encodeComponent(params[key]);
    }

    String collectedParams = encoded.keys.map((key) => "$key=${encoded[key]}").join("&");

    String baseString =
        "$method&${Uri.encodeComponent(uriString)}&${Uri.encodeComponent(
        collectedParams)}";

    String signingKey = "${Uri.encodeComponent(
        credentials['secret'])}&$tokenSecret";

    // After you create a base string and signing key, we need to hash this via HMAC-SHA1
    var hmac = new Hmac(sha1, signingKey.codeUnits);

    // The returned signature should be the resulting hash, Base64-encoded
    return BASE64.encode(hmac.convert(baseString.codeUnits).bytes);
}

If all is well, Twitter will return a 200 that looks something like this:

oauth_token=NPcudxy0yU5T3tBzho7iCotZ3cnetKwcTIRlX0iwRl0&
oauth_token_secret=veNRnAWe6inFuo8o2u8SLLZLjolYDmDP7SzL0YfYI&
oauth_callback_confirmed=true

You are free to handle this however you want, whether it be via Regex or something else. I chose to transform this into a Map.

Redirecting the User

The mechanics of this are completely dependent on the server platform you are using, but most of them allow some sort of request redirection mechanism.

I’m using the in-development Angel Framework, but if you are using another platform, such as Redstone, shelf, or even the built-in HttpServer API, you should be able to follow along.

@Expose("/auth")
class AuthController extends Controller {
  TwitterClient twitter;

  @Expose("/twitter")
  twitterLogin(RequestContext req, ResponseContext res) async {
    try {
      var tokenResult = await twitter.createRequestToken();

      // We should keep track of the returned oauth_token and oauth_token_secret
      req.session.addAll(tokenResult);

      String encodedToken = Uri.encodeQueryComponent(tokenResult['oauth_token']);
      return res.redirect("https://api.twitter.com/oauth/authenticate?oauth_token=$encodedToken", code: 302);
    } catch (e) {
      throw new AngelHttpException.NotAuthenticated();
    }
  }
}

Creating an Access Token

Now that we have the user’s consent, we need to transform our request token into an access token.

After the user signs in, they are sent to our callback, with an oauth_token and oauth_verifier in the query string. We need to use these to send a POST to /oauth/access_token, or else we will never be able to use any of Twitter API’s.

Fortunately, the code to prepare requests is already out of the way, so implementing this is trivial.

Future<Map<String, String>> createAccessToken(
      String token, String verifier) async {
    var request = await _prepRequest("/oauth/access_token",
        method: "POST", data: {"verifier": verifier}, accessToken: token);

    request.headers.contentType =
        ContentType.parse("application/x-www-form-urlencoded");
    request.writeln("oauth_verifier=$verifier");

    return _mapifyRequest(request, (body) {
      throw new AngelHttpException.NotAuthenticated(message: body);
    });
  }

Twitter will return a URL-encoded response on this endpoint, as well:

oauth_token=7588892-kagSNqWge8gB1WwE3plnFsJHAZVfxWD7Vb57p0b4&
oauth_token_secret=PbKfYqSryyeKDWz4ebtY3o5ogNLG11WJuZBc9fQrQo

Lastly, I put the following in my AuthController:

@Expose("/twitter/callback")
twitterCallback(RequestContext req, ResponseContext res) async {
    String token = req.query['oauth_token'];
    String verifier = req.query['oauth_verifier'];

    // Omitted - validation

    var loginData = await twitter.createAccessToken(token, verifier);

    // Update the data in the session to point to our new access token
    req.session['oauth_token'] = loginData['oauth_token'];
    req.session['oauth_token_secret'] = loginData['oauth_token_secret'];

    // Omitted - Login logic
}

Conclusion

That’s it! Your users can log into your site now. In addition, it is easy to modify your client to support another endpoint, just by calling _prepRequest.

Hopefully this was useful for you. The process is overly complicated, but now you don’t have to re-write the client each time.