platform/lib/angel_auth_twitter.dart

165 lines
5.3 KiB
Dart
Raw Normal View History

2016-12-23 02:07:47 +00:00
import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'dart:io';
import 'package:angel_auth/angel_auth.dart';
import 'package:angel_framework/angel_framework.dart';
import 'package:crypto/crypto.dart';
2017-02-25 20:21:07 +00:00
import 'package:http/http.dart' as http;
2016-12-23 02:07:47 +00:00
import 'package:random_string/random_string.dart' as rs;
2017-02-25 20:21:07 +00:00
import 'package:twit/twit.dart';
2016-12-23 02:07:47 +00:00
const String _ENDPOINT = "https://api.twitter.com";
2017-02-25 20:21:07 +00:00
typedef TwitterAuthVerifier(Twit twit);
2016-12-23 02:07:47 +00:00
class TwitterStrategy extends AuthStrategy {
HttpClient _client = new HttpClient();
final Map<String, dynamic> config;
2017-02-25 20:21:07 +00:00
final TwitterAuthVerifier verifier;
2016-12-23 02:07:47 +00:00
@override
String get name => 'twitter';
2017-02-25 20:21:07 +00:00
TwitterStrategy(this.verifier, {this.config: const {}});
2016-12-23 02:07:47 +00:00
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(
config['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);
}
Future<HttpClientRequest> _prepRequest(String path,
{String method: "GET",
Map data: const {},
String accessToken,
String tokenSecret: ""}) async {
Map headers = new Map.from(data);
headers["oauth_version"] = "1.0";
headers["oauth_consumer_key"] = config['key'];
// The implementation of _randomString doesn't matter - just generate a 32-char
// alphanumeric string.
headers["oauth_nonce"] = rs.randomAlphaNumeric(32);
headers["oauth_signature_method"] = "HMAC-SHA1";
headers["oauth_timestamp"] =
(new DateTime.now().millisecondsSinceEpoch / 1000).round().toString();
if (accessToken != null) {
headers["oauth_token"] = accessToken;
}
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);
var oauthString = headers.keys
.map((name) => '$name="${Uri.encodeComponent(headers[name])}"')
.join(", ");
return request
..headers.set(HttpHeaders.AUTHORIZATION, "OAuth $oauthString");
}
_mapifyRequest(HttpClientRequest request) async {
var rs = await request.close();
var body = await rs.transform(UTF8.decoder).join();
if (rs.statusCode != HttpStatus.OK) {
2017-02-25 20:21:07 +00:00
throw new AngelHttpException.notAuthenticated(
2016-12-23 02:07:47 +00:00
message: 'Twitter authentication error: $body');
}
var pairs = body.split('&');
var data = {};
for (var pair in pairs) {
var index = pair.indexOf('=');
if (index > -1) {
var key = pair.substring(0, index);
var value = Uri.decodeFull(pair.substring(index + 1));
data[key] = value;
}
}
return data;
}
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);
}
Future<Map<String, String>> createRequestToken() async {
var request = await _prepRequest("/oauth/request_token",
method: "POST", data: {"oauth_callback": config['callback']});
// _mapifyRequest is a function that sends a request and parses its URL-encoded
// response into a Map. This detail is not important.
return await _mapifyRequest(request);
}
@override
Future<bool> canLogout(RequestContext req, ResponseContext res) async => true;
2017-02-25 20:21:07 +00:00
2016-12-23 02:07:47 +00:00
@override
authenticate(RequestContext req, ResponseContext res,
[AngelAuthOptions options]) async {
if (options != null) {
return await authenticateCallback(req, res, options);
} else {
var result = await createRequestToken();
String token = result['oauth_token'];
res.redirect("$_ENDPOINT/oauth/authenticate?oauth_token=$token");
return false;
}
}
Future authenticateCallback(
RequestContext req, ResponseContext res, AngelAuthOptions options) async {
var token = req.query['oauth_token'];
var verifier = req.query['oauth_verifier'];
var loginData = await createAccessToken(token, verifier);
2017-02-25 20:21:07 +00:00
var credentials = new TwitterCredentials(
consumerKey: config['key'],
consumerSecret: config['secret'],
accessToken: loginData['oauth_token'],
accessTokenSecret: loginData['oauth_token_secret']
);
2016-12-23 02:07:47 +00:00
2017-02-25 20:21:07 +00:00
var twit = new Twit(credentials, new http.Client());
return await this.verifier(twit);
2016-12-23 02:07:47 +00:00
}
}