2016-12-23 02:07:47 +00:00
|
|
|
import 'dart:async';
|
2019-04-11 14:28:51 +00:00
|
|
|
import 'dart:convert';
|
2022-06-04 01:30:18 +00:00
|
|
|
import 'dart:io';
|
|
|
|
import 'package:angel3_auth/angel3_auth.dart';
|
|
|
|
import 'package:angel3_framework/angel3_framework.dart';
|
2019-03-02 00:03:48 +00:00
|
|
|
import 'package:http/http.dart' as http;
|
|
|
|
import 'package:path/path.dart' as p;
|
2022-06-04 01:30:18 +00:00
|
|
|
import 'package:oauth1/oauth1.dart' as oauth;
|
|
|
|
import 'package:dart_twitter_api/twitter_api.dart';
|
2016-12-23 02:07:47 +00:00
|
|
|
|
2019-03-23 04:22:20 +00:00
|
|
|
/// Authenticates users by connecting to Twitter's API.
|
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
|
|
|
/// A callback that uses Twitter to authenticate a [User].
|
|
|
|
///
|
|
|
|
/// As always, return `null` if authentication fails.
|
2022-06-04 01:30:18 +00:00
|
|
|
final FutureOr<User> Function(TwitterApi, RequestContext, ResponseContext)
|
2019-03-02 00:03:48 +00:00
|
|
|
verifier;
|
2016-12-23 02:07:47 +00:00
|
|
|
|
2019-04-11 14:28:51 +00:00
|
|
|
/// A callback that is triggered when an OAuth2 error occurs (i.e. the user declines to login);
|
|
|
|
final FutureOr<dynamic> Function(
|
|
|
|
TwitterAuthorizationException, RequestContext, ResponseContext) onError;
|
|
|
|
|
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
|
|
|
|
2022-06-04 01:30:18 +00:00
|
|
|
oauth.Client? _client;
|
2016-12-23 02:07:47 +00:00
|
|
|
|
2019-03-23 04:22:20 +00:00
|
|
|
/// The underlying [oauth.Client] used to query Twitter.
|
2022-06-04 01:30:18 +00:00
|
|
|
oauth.Client get client => _client!;
|
2016-12-23 02:07:47 +00:00
|
|
|
|
2019-04-11 14:28:51 +00:00
|
|
|
TwitterStrategy(this.options, this.verifier, this.onError,
|
2022-06-04 01:30:18 +00:00
|
|
|
{http.BaseClient? client, Uri? baseUrl})
|
2021-06-20 13:29:23 +00:00
|
|
|
: baseUrl = baseUrl ?? Uri.parse('https://api.twitter.com') {
|
2022-06-04 01:30:18 +00:00
|
|
|
// define platform (server)
|
|
|
|
final oauth.Platform platform = oauth.Platform(
|
|
|
|
'$baseUrl/oauth/request_token', // temporary credentials request
|
|
|
|
'$baseUrl/oauth/authorize', // resource owner authorization
|
|
|
|
'$baseUrl/oauth/access_token', // token credentials request
|
|
|
|
oauth.SignatureMethods.hmacSha1 // signature method
|
|
|
|
);
|
|
|
|
|
|
|
|
// define client credentials (consumer keys)
|
|
|
|
final oauth.ClientCredentials clientCredentials =
|
|
|
|
oauth.ClientCredentials(options.clientId, options.clientSecret);
|
|
|
|
|
|
|
|
// create Authorization object with client credentials and platform definition
|
|
|
|
final oauth.Authorization auth =
|
|
|
|
oauth.Authorization(clientCredentials, platform);
|
|
|
|
|
|
|
|
// request temporary credentials (request tokens)
|
|
|
|
auth.requestTemporaryCredentials('oob').then((res) {
|
|
|
|
// redirect to authorization page
|
|
|
|
print(
|
|
|
|
"Open with your browser: ${auth.getResourceOwnerAuthorizationURI(res.credentials.token)}");
|
|
|
|
|
|
|
|
// get verifier (PIN)
|
|
|
|
stdout.write("PIN: ");
|
|
|
|
String verifier = stdin.readLineSync() ?? '';
|
|
|
|
|
|
|
|
// request token credentials (access tokens)
|
|
|
|
return auth.requestTokenCredentials(res.credentials, verifier);
|
|
|
|
}).then((res) {
|
|
|
|
// create Client object
|
|
|
|
_client = oauth.Client(
|
|
|
|
platform.signatureMethod, clientCredentials, res.credentials);
|
|
|
|
});
|
2016-12-23 02:07:47 +00:00
|
|
|
}
|
|
|
|
|
2019-03-23 04:22:20 +00:00
|
|
|
/// Handle a response from Twitter.
|
|
|
|
Future<Map<String, String>> handleUrlEncodedResponse(http.Response rs) async {
|
2019-03-02 00:03:48 +00:00
|
|
|
var body = rs.body;
|
2016-12-23 02:07:47 +00:00
|
|
|
|
2019-03-02 00:03:48 +00:00
|
|
|
if (rs.statusCode != 200) {
|
2019-04-11 14:28:51 +00:00
|
|
|
var err = json.decode(rs.body) as Map;
|
|
|
|
var errors = err['errors'] as List;
|
|
|
|
|
|
|
|
if (errors.isNotEmpty) {
|
|
|
|
throw TwitterAuthorizationException(
|
|
|
|
errors[0]['message'] as String, false);
|
|
|
|
} else {
|
|
|
|
throw StateError(
|
|
|
|
'Twitter returned an error response without an error message: ${rs.body}');
|
|
|
|
}
|
2016-12-23 02:07:47 +00:00
|
|
|
}
|
|
|
|
|
2019-03-02 00:03:48 +00:00
|
|
|
return Uri.splitQueryString(body);
|
2016-12-23 02:07:47 +00:00
|
|
|
}
|
|
|
|
|
2019-03-23 04:22:20 +00:00
|
|
|
/// Get an access token.
|
|
|
|
Future<Map<String, String>> getAccessToken(String token, String verifier) {
|
2022-06-04 01:30:18 +00:00
|
|
|
return client.post(
|
2019-03-23 04:22:20 +00:00
|
|
|
baseUrl.replace(path: p.join(baseUrl.path, 'oauth/access_token')),
|
2019-04-11 14:28:51 +00:00
|
|
|
headers: {
|
|
|
|
'accept': 'application/json'
|
|
|
|
},
|
2019-03-23 04:22:20 +00:00
|
|
|
body: {
|
|
|
|
'oauth_token': token,
|
|
|
|
'oauth_verifier': verifier
|
|
|
|
}).then(handleUrlEncodedResponse);
|
|
|
|
// var request = await createRequest("oauth/access_token",
|
|
|
|
// method: "POST", data: {"verifier": verifier}, accessToken: token);
|
2016-12-23 02:07:47 +00:00
|
|
|
}
|
|
|
|
|
2019-03-23 04:22:20 +00:00
|
|
|
/// Get a request token.
|
|
|
|
Future<Map<String, String>> getRequestToken() {
|
2022-06-04 01:30:18 +00:00
|
|
|
return client.post(
|
2019-03-23 04:22:20 +00:00
|
|
|
baseUrl.replace(path: p.join(baseUrl.path, 'oauth/request_token')),
|
2019-04-11 14:28:51 +00:00
|
|
|
headers: {
|
|
|
|
'accept': 'application/json'
|
|
|
|
},
|
2019-03-23 04:22:20 +00:00
|
|
|
body: {
|
2021-06-26 13:13:43 +00:00
|
|
|
'oauth_callback': options.redirectUri.toString()
|
2019-03-23 04:22:20 +00:00
|
|
|
}).then(handleUrlEncodedResponse);
|
2016-12-23 02:07:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
2022-06-04 01:30:18 +00:00
|
|
|
Future<User?> authenticate(RequestContext req, ResponseContext res,
|
|
|
|
[AngelAuthOptions? options]) async {
|
2019-04-11 14:28:51 +00:00
|
|
|
try {
|
|
|
|
if (options != null) {
|
|
|
|
var result = await authenticateCallback(req, res, options);
|
2021-06-26 13:13:43 +00:00
|
|
|
if (result is User) {
|
2019-04-11 14:28:51 +00:00
|
|
|
return result;
|
2021-06-26 13:13:43 +00:00
|
|
|
} else {
|
2019-04-11 14:28:51 +00:00
|
|
|
return null;
|
2021-06-26 13:13:43 +00:00
|
|
|
}
|
2019-04-11 14:28:51 +00:00
|
|
|
} else {
|
|
|
|
var result = await getRequestToken();
|
|
|
|
var token = result['oauth_token'];
|
|
|
|
var url = baseUrl.replace(
|
|
|
|
path: p.join(baseUrl.path, 'oauth/authorize'),
|
|
|
|
queryParameters: {'oauth_token': token});
|
2021-06-26 13:13:43 +00:00
|
|
|
await res.redirect(url);
|
2019-04-11 14:28:51 +00:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
} on TwitterAuthorizationException catch (e) {
|
|
|
|
var result = await onError(e, req, res);
|
2022-06-04 01:30:18 +00:00
|
|
|
await req.app?.executeHandler(result, req, res);
|
2019-04-11 14:28:51 +00:00
|
|
|
await res.close();
|
2019-03-02 00:03:48 +00:00
|
|
|
return null;
|
2016-12-23 02:07:47 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-04-11 14:28:51 +00:00
|
|
|
Future authenticateCallback(
|
2016-12-23 02:07:47 +00:00
|
|
|
RequestContext req, ResponseContext res, AngelAuthOptions options) async {
|
2019-04-11 14:28:51 +00:00
|
|
|
try {
|
|
|
|
if (req.queryParameters.containsKey('denied')) {
|
|
|
|
throw TwitterAuthorizationException(
|
|
|
|
'The user denied the Twitter authorization attempt.', true);
|
|
|
|
}
|
|
|
|
|
|
|
|
var token = req.queryParameters['oauth_token'] as String;
|
|
|
|
var verifier = req.queryParameters['oauth_verifier'] as String;
|
|
|
|
var loginData = await getAccessToken(token, verifier);
|
2022-06-04 01:30:18 +00:00
|
|
|
var twitter = TwitterApi(
|
|
|
|
client: TwitterClient(
|
|
|
|
consumerKey: this.options.clientId,
|
|
|
|
consumerSecret: this.options.clientSecret,
|
|
|
|
token: loginData['oauth_token'] ?? '',
|
|
|
|
secret: loginData['oauth_token_secret'] ?? ''));
|
|
|
|
|
2019-04-11 14:28:51 +00:00
|
|
|
return await this.verifier(twitter, req, res);
|
|
|
|
} on TwitterAuthorizationException catch (e) {
|
|
|
|
return await onError(e, req, res);
|
|
|
|
}
|
2016-12-23 02:07:47 +00:00
|
|
|
}
|
|
|
|
}
|
2019-04-11 14:28:51 +00:00
|
|
|
|
|
|
|
class TwitterAuthorizationException implements Exception {
|
|
|
|
/// The message associated with this exception.
|
|
|
|
final String message;
|
|
|
|
|
|
|
|
/// Whether the user denied the authorization attempt.
|
|
|
|
final bool isDenial;
|
|
|
|
|
|
|
|
TwitterAuthorizationException(this.message, this.isDenial);
|
|
|
|
|
|
|
|
@override
|
|
|
|
String toString() => 'TwitterAuthorizationException: $message';
|
|
|
|
}
|