2016-12-23 02:07:47 +00:00
|
|
|
import 'dart:async';
|
|
|
|
import 'dart:collection';
|
|
|
|
import 'dart:convert';
|
|
|
|
import 'package:angel_auth/angel_auth.dart';
|
|
|
|
import 'package:angel_framework/angel_framework.dart';
|
|
|
|
import 'package:crypto/crypto.dart';
|
2019-03-02 00:03:48 +00:00
|
|
|
import 'package:http/http.dart' as http;
|
|
|
|
import 'package:meta/meta.dart';
|
|
|
|
import 'package:path/path.dart' as p;
|
2016-12-23 02:07:47 +00:00
|
|
|
import 'package:random_string/random_string.dart' as rs;
|
2019-03-02 00:03:48 +00:00
|
|
|
import 'package:twitter/twitter.dart';
|
2016-12-23 02:07:47 +00:00
|
|
|
|
2019-03-02 00:03:48 +00:00
|
|
|
class TwitterStrategy<User> extends AuthStrategy<User> {
|
|
|
|
/// The options defining how to connect to the third-party.
|
|
|
|
final ExternalAuthOptions options;
|
2016-12-23 02:07:47 +00:00
|
|
|
|
2019-03-02 00:03:48 +00:00
|
|
|
/// The underlying [BaseClient] used to query Twitter.
|
|
|
|
final http.BaseClient httpClient;
|
2017-02-25 20:21:07 +00:00
|
|
|
|
2019-03-02 00:03:48 +00:00
|
|
|
/// A callback that uses Twitter to authenticate a [User].
|
|
|
|
///
|
|
|
|
/// As always, return `null` if authentication fails.
|
|
|
|
final FutureOr<User> Function(Twitter, RequestContext, ResponseContext)
|
|
|
|
verifier;
|
2016-12-23 02:07:47 +00:00
|
|
|
|
2019-03-02 00:03:48 +00:00
|
|
|
/// The root of Twitter's API. Defaults to `'https://api.twitter.com'`.
|
|
|
|
final Uri baseUrl;
|
2016-12-23 02:07:47 +00:00
|
|
|
|
2019-03-02 00:03:48 +00:00
|
|
|
TwitterStrategy(this.options, this.verifier,
|
|
|
|
{http.BaseClient client, Uri baseUrl})
|
|
|
|
: this.baseUrl = baseUrl ?? Uri.parse('https://api.twitter.com'),
|
|
|
|
this.httpClient = client ?? http.Client() as http.BaseClient;
|
2016-12-23 02:07:47 +00:00
|
|
|
|
|
|
|
String _createSignature(
|
|
|
|
String method, String uriString, Map<String, String> params,
|
2019-03-02 00:03:48 +00:00
|
|
|
{@required String tokenSecret}) {
|
2016-12-23 02:07:47 +00:00
|
|
|
// 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 =
|
2019-03-02 00:03:48 +00:00
|
|
|
"$method&${Uri.encodeComponent(uriString)}&${Uri.encodeComponent(collectedParams)}";
|
2016-12-23 02:07:47 +00:00
|
|
|
|
2019-03-02 00:03:48 +00:00
|
|
|
String signingKey =
|
|
|
|
"${Uri.encodeComponent(options.clientSecret)}&$tokenSecret";
|
2016-12-23 02:07:47 +00:00
|
|
|
|
|
|
|
// 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
|
2019-03-02 00:03:48 +00:00
|
|
|
return base64.encode(hmac.convert(baseString.codeUnits).bytes);
|
2016-12-23 02:07:47 +00:00
|
|
|
}
|
|
|
|
|
2019-03-02 00:03:48 +00:00
|
|
|
Future<http.Request> _prepRequest(String path,
|
|
|
|
{String method = "GET",
|
|
|
|
Map<String, String> data = const {},
|
2016-12-23 02:07:47 +00:00
|
|
|
String accessToken,
|
2019-03-02 00:03:48 +00:00
|
|
|
String tokenSecret = ''}) async {
|
|
|
|
var headers = new Map<String, String>.from(data);
|
2016-12-23 02:07:47 +00:00
|
|
|
headers["oauth_version"] = "1.0";
|
2019-03-02 00:03:48 +00:00
|
|
|
headers["oauth_consumer_key"] = options.clientId;
|
2016-12-23 02:07:47 +00:00
|
|
|
|
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
|
2019-03-02 00:03:48 +00:00
|
|
|
var request = http.Request(method, baseUrl.replace(path: path));
|
2016-12-23 02:07:47 +00:00
|
|
|
|
2019-03-02 00:03:48 +00:00
|
|
|
headers['oauth_signature'] = _createSignature(
|
|
|
|
method, request.url.toString(), headers,
|
2016-12-23 02:07:47 +00:00
|
|
|
tokenSecret: tokenSecret);
|
|
|
|
|
|
|
|
var oauthString = headers.keys
|
|
|
|
.map((name) => '$name="${Uri.encodeComponent(headers[name])}"')
|
|
|
|
.join(", ");
|
|
|
|
|
|
|
|
return request
|
2019-03-02 00:03:48 +00:00
|
|
|
..headers.addAll(headers)
|
|
|
|
..headers['authorization'] = "OAuth $oauthString";
|
2016-12-23 02:07:47 +00:00
|
|
|
}
|
|
|
|
|
2019-03-02 00:03:48 +00:00
|
|
|
Future<Map<String, String>> _parseUrlEncoded(http.BaseRequest rq) async {
|
|
|
|
var response = await httpClient.send(rq);
|
|
|
|
var rs = await http.Response.fromStream(response);
|
|
|
|
var body = rs.body;
|
2016-12-23 02:07:47 +00:00
|
|
|
|
2019-03-02 00:03:48 +00:00
|
|
|
if (rs.statusCode != 200) {
|
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');
|
|
|
|
}
|
|
|
|
|
2019-03-02 00:03:48 +00:00
|
|
|
return Uri.splitQueryString(body);
|
2016-12-23 02:07:47 +00:00
|
|
|
}
|
|
|
|
|
2019-03-02 00:03:48 +00:00
|
|
|
Future<Map<String, String>> _createAccessToken(
|
2016-12-23 02:07:47 +00:00
|
|
|
String token, String verifier) async {
|
2019-03-02 00:03:48 +00:00
|
|
|
var request = await _prepRequest("oauth/access_token",
|
2016-12-23 02:07:47 +00:00
|
|
|
method: "POST", data: {"verifier": verifier}, accessToken: token);
|
2019-03-02 00:03:48 +00:00
|
|
|
request.bodyFields = {'oauth_verifier': verifier};
|
|
|
|
return _parseUrlEncoded(request);
|
2016-12-23 02:07:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Future<Map<String, String>> createRequestToken() async {
|
2019-03-02 00:03:48 +00:00
|
|
|
var request = await _prepRequest("oauth/request_token",
|
|
|
|
method: "POST",
|
|
|
|
data: {"oauth_callback": options.redirectUri.toString()});
|
2016-12-23 02:07:47 +00:00
|
|
|
|
|
|
|
// _mapifyRequest is a function that sends a request and parses its URL-encoded
|
|
|
|
// response into a Map. This detail is not important.
|
2019-03-02 00:03:48 +00:00
|
|
|
return await _parseUrlEncoded(request);
|
2016-12-23 02:07:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
2019-03-02 00:03:48 +00:00
|
|
|
Future<User> authenticate(RequestContext req, ResponseContext res,
|
2016-12-23 02:07:47 +00:00
|
|
|
[AngelAuthOptions options]) async {
|
|
|
|
if (options != null) {
|
|
|
|
return await authenticateCallback(req, res, options);
|
|
|
|
} else {
|
|
|
|
var result = await createRequestToken();
|
2019-03-02 00:03:48 +00:00
|
|
|
var token = result['oauth_token'];
|
|
|
|
var url = baseUrl.replace(
|
|
|
|
path: p.join(baseUrl.path, 'oauth/authenticate'),
|
|
|
|
queryParameters: {'oauth_token': token});
|
|
|
|
res.redirect(url);
|
|
|
|
return null;
|
2016-12-23 02:07:47 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-02 00:03:48 +00:00
|
|
|
Future<User> authenticateCallback(
|
2016-12-23 02:07:47 +00:00
|
|
|
RequestContext req, ResponseContext res, AngelAuthOptions options) async {
|
2019-03-02 00:03:48 +00:00
|
|
|
// TODO: Handle errors
|
|
|
|
print('Query: ${req.queryParameters}');
|
|
|
|
var token = req.queryParameters['oauth_token'] as String;
|
|
|
|
var verifier = req.queryParameters['oauth_verifier'] as String;
|
|
|
|
var loginData = await _createAccessToken(token, verifier);
|
|
|
|
var twitter = Twitter(this.options.clientId, this.options.clientSecret,
|
|
|
|
loginData['oauth_token'], loginData['oauth_token_secret']);
|
|
|
|
return await this.verifier(twitter, req, res);
|
2016-12-23 02:07:47 +00:00
|
|
|
}
|
|
|
|
}
|