2.0 almost done

This commit is contained in:
Tobe O 2019-03-01 19:03:48 -05:00
parent 69826aed59
commit db1b0235a8
8 changed files with 166 additions and 152 deletions

View file

@ -1,3 +0,0 @@
analyzer:
exclude:
- .scripts-bin/**/*.dart

14
.vscode/launch.json vendored
View file

@ -1,14 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Example server",
"type": "dart-cli",
"request": "launch",
"cwd": "${workspaceRoot}/example",
"debugSettings": "${command:debugSettings}",
"program": "${workspaceRoot}/example/server.dart",
"args": []
}
]
}

3
CHANGELOG.md Normal file
View file

@ -0,0 +1,3 @@
# 2.0.0
* Angel 2 + Dart 2 suppport.
* Use `package:twitter` instead of `package:twit`.

4
analysis_options.yaml Normal file
View file

@ -0,0 +1,4 @@
include: package:pedantic/analysis_options.yaml
analyzer:
strong-mode:
implicit-casts: false

85
example/main.dart Normal file
View file

@ -0,0 +1,85 @@
import 'dart:convert';
import 'dart:io';
import 'package:angel_auth/angel_auth.dart';
import 'package:angel_auth_twitter/angel_auth_twitter.dart';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_framework/http.dart';
import 'package:logging/logging.dart';
class _User {
final String handle;
_User(this.handle);
Map<String, dynamic> toJson() => {'handle': handle};
}
main() async {
var app = Angel();
var http = AngelHttp(app);
var auth = AngelAuth<_User>(
jwtKey: 'AUTH_TWITTER_SECRET',
allowCookie: false,
serializer: (user) async => user.handle,
deserializer: (screenName) async {
// Of course, in a real app, you would fetch
// user data, but not here.
return _User(screenName.toString());
},
);
auth.strategies['twitter'] = TwitterStrategy(
ExternalAuthOptions(
clientId: Platform.environment['TWITTER_CLIENT_ID'] ??
'qlrBWXneoSYZKS2bT4TGHaNaV',
clientSecret: Platform.environment['TWITTER_CLIENT_SECRET'] ??
'n2oA0ZtR7TzYincpMYElRpyYovAQlhYizTkTm2x5QxjH6mLVyE',
redirectUri: Platform.environment['TWITTER_REDIRECT_URI'] ??
'http://localhost:3000/auth/twitter/callback',
),
(twit, req, res) async {
var response =
await twit.twitterClient.get('/account/verify_credentials.json');
var userData = json.decode(response.body) as Map;
return _User(userData['screen_name'] as String);
},
);
app
..fallback(auth.decodeJwt)
..get('/', auth.authenticate('twitter'));
app
..get(
'/auth/twitter/callback',
auth.authenticate(
'twitter',
AngelAuthOptions(
callback: (req, res, jwt) {
return res.redirect('/home?token=$jwt');
},
),
),
);
app.get(
'/home',
chain([
requireAuthentication<_User>(),
(req, res) {
var user = req.container.make<_User>();
res.write('Your Twitter handle is ${user.handle}');
},
]),
);
app.logger = Logger('angel_auth_twitter')
..onRecord.listen((rec) {
print(rec);
if (rec.error != null) print(rec.error);
if (rec.stackTrace != null) print(rec.stackTrace);
});
await http.startServer('127.0.0.1', 3000);
print('Listening at ${http.uri}');
}

View file

@ -1,52 +0,0 @@
import 'dart:io';
import 'package:angel_auth/angel_auth.dart';
import 'package:angel_auth_twitter/angel_auth_twitter.dart';
import 'package:angel_diagnostics/angel_diagnostics.dart';
import 'package:angel_framework/angel_framework.dart';
import 'package:twit/twit.dart';
const Map<String, String> TWITTER_CONFIG = const {
'callback': 'http://localhost:3000/auth/twitter/callback',
'key': 'qlrBWXneoSYZKS2bT4TGHaNaV',
'secret': 'n2oA0ZtR7TzYincpMYElRpyYovAQlhYizTkTm2x5QxjH6mLVyE'
};
verifier(TwitBase twit) async {
// Maybe fetch user credentials:
return await twit.get('/account/verify_credentials.json');
}
main() async {
var app = new Angel();
var auth = new AngelAuth(jwtKey: 'AUTH_TWITTER_SECRET', allowCookie: false);
await app.configure(auth);
auth.serializer = (user) async => user['screen_name'];
auth.deserializer = (screenName) async {
// Of course, in a real app, you would fetch
// user data, but not here.
return {'handle': '@$screenName'};
};
auth.strategies.add(new TwitterStrategy(verifier, config: TWITTER_CONFIG));
app
..get('/', auth.authenticate('twitter'))
..get(
'/auth/twitter/callback',
auth.authenticate('twitter',
new AngelAuthOptions(callback: (req, res, jwt) {
return res.redirect('/home?token=$jwt');
})))
..chain('auth').get('/home', (req, res) {
res
..write('Your Twitter handle is ${req.user["handle"]}.')
..end();
});
await app.configure(logRequests(new File('log.txt')));
var server = await app.startServer(null, 3000);
print('Listening at http://${server.address.address}:${server.port}');
}

View file

@ -1,30 +1,39 @@
import 'dart:async'; import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'package:angel_auth/angel_auth.dart'; import 'package:angel_auth/angel_auth.dart';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel_framework/angel_framework.dart';
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'package:http/http.dart' as http;
import 'package:meta/meta.dart';
import 'package:path/path.dart' as p;
import 'package:random_string/random_string.dart' as rs; import 'package:random_string/random_string.dart' as rs;
import 'package:twit/io.dart'; import 'package:twitter/twitter.dart';
const String _ENDPOINT = "https://api.twitter.com"; class TwitterStrategy<User> extends AuthStrategy<User> {
/// The options defining how to connect to the third-party.
final ExternalAuthOptions options;
typedef TwitterAuthVerifier(TwitBase twit); /// The underlying [BaseClient] used to query Twitter.
final http.BaseClient httpClient;
class TwitterStrategy extends AuthStrategy { /// A callback that uses Twitter to authenticate a [User].
HttpClient _client = new HttpClient(); ///
final Map<String, dynamic> config; /// As always, return `null` if authentication fails.
final TwitterAuthVerifier verifier; final FutureOr<User> Function(Twitter, RequestContext, ResponseContext)
verifier;
@override /// The root of Twitter's API. Defaults to `'https://api.twitter.com'`.
String get name => 'twitter'; final Uri baseUrl;
TwitterStrategy(this.verifier, {this.config: const {}}); 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;
String _createSignature( String _createSignature(
String method, String uriString, Map<String, String> params, String method, String uriString, Map<String, String> params,
{String tokenSecret: ""}) { {@required String tokenSecret}) {
// Not only do we need to sort the parameters, but we need to URI-encode them as well. // Not only do we need to sort the parameters, but we need to URI-encode them as well.
var encoded = new SplayTreeMap(); var encoded = new SplayTreeMap();
for (String key in params.keys) { for (String key in params.keys) {
@ -35,27 +44,26 @@ class TwitterStrategy extends AuthStrategy {
encoded.keys.map((key) => "$key=${encoded[key]}").join("&"); encoded.keys.map((key) => "$key=${encoded[key]}").join("&");
String baseString = String baseString =
"$method&${Uri.encodeComponent(uriString)}&${Uri.encodeComponent( "$method&${Uri.encodeComponent(uriString)}&${Uri.encodeComponent(collectedParams)}";
collectedParams)}";
String signingKey = "${Uri.encodeComponent( String signingKey =
config['secret'])}&$tokenSecret"; "${Uri.encodeComponent(options.clientSecret)}&$tokenSecret";
// After you create a base string and signing key, we need to hash this via HMAC-SHA1 // After you create a base string and signing key, we need to hash this via HMAC-SHA1
var hmac = new Hmac(sha1, signingKey.codeUnits); var hmac = new Hmac(sha1, signingKey.codeUnits);
// The returned signature should be the resulting hash, Base64-encoded // The returned signature should be the resulting hash, Base64-encoded
return BASE64.encode(hmac.convert(baseString.codeUnits).bytes); return base64.encode(hmac.convert(baseString.codeUnits).bytes);
} }
Future<HttpClientRequest> _prepRequest(String path, Future<http.Request> _prepRequest(String path,
{String method: "GET", {String method = "GET",
Map data: const {}, Map<String, String> data = const {},
String accessToken, String accessToken,
String tokenSecret: ""}) async { String tokenSecret = ''}) async {
Map headers = new Map.from(data); var headers = new Map<String, String>.from(data);
headers["oauth_version"] = "1.0"; headers["oauth_version"] = "1.0";
headers["oauth_consumer_key"] = config['key']; headers["oauth_consumer_key"] = options.clientId;
// The implementation of _randomString doesn't matter - just generate a 32-char // The implementation of _randomString doesn't matter - just generate a 32-char
// alphanumeric string. // alphanumeric string.
@ -68,10 +76,10 @@ class TwitterStrategy extends AuthStrategy {
headers["oauth_token"] = accessToken; headers["oauth_token"] = accessToken;
} }
var request = await _client.openUrl(method, Uri.parse("$_ENDPOINT$path")); var request = http.Request(method, baseUrl.replace(path: path));
headers['oauth_signature'] = _createSignature(method, headers['oauth_signature'] = _createSignature(
request.uri.toString().replaceAll("?${request.uri.query}", ""), headers, method, request.url.toString(), headers,
tokenSecret: tokenSecret); tokenSecret: tokenSecret);
var oauthString = headers.keys var oauthString = headers.keys
@ -79,85 +87,66 @@ class TwitterStrategy extends AuthStrategy {
.join(", "); .join(", ");
return request return request
..headers.set(HttpHeaders.AUTHORIZATION, "OAuth $oauthString"); ..headers.addAll(headers)
..headers['authorization'] = "OAuth $oauthString";
} }
_mapifyRequest(HttpClientRequest request) async { Future<Map<String, String>> _parseUrlEncoded(http.BaseRequest rq) async {
var rs = await request.close(); var response = await httpClient.send(rq);
var body = await rs.transform(UTF8.decoder).join(); var rs = await http.Response.fromStream(response);
var body = rs.body;
if (rs.statusCode != HttpStatus.OK) { if (rs.statusCode != 200) {
throw new AngelHttpException.notAuthenticated( throw new AngelHttpException.notAuthenticated(
message: 'Twitter authentication error: $body'); message: 'Twitter authentication error: $body');
} }
var pairs = body.split('&'); return Uri.splitQueryString(body);
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(
}
Future<Map<String, String>> createAccessToken(
String token, String verifier) async { String token, String verifier) async {
var request = await _prepRequest("/oauth/access_token", var request = await _prepRequest("oauth/access_token",
method: "POST", data: {"verifier": verifier}, accessToken: token); method: "POST", data: {"verifier": verifier}, accessToken: token);
request.bodyFields = {'oauth_verifier': verifier};
request.headers.contentType = return _parseUrlEncoded(request);
ContentType.parse("application/x-www-form-urlencoded");
request.writeln("oauth_verifier=$verifier");
return _mapifyRequest(request);
} }
Future<Map<String, String>> createRequestToken() async { Future<Map<String, String>> createRequestToken() async {
var request = await _prepRequest("/oauth/request_token", var request = await _prepRequest("oauth/request_token",
method: "POST", data: {"oauth_callback": config['callback']}); method: "POST",
data: {"oauth_callback": options.redirectUri.toString()});
// _mapifyRequest is a function that sends a request and parses its URL-encoded // _mapifyRequest is a function that sends a request and parses its URL-encoded
// response into a Map. This detail is not important. // response into a Map. This detail is not important.
return await _mapifyRequest(request); return await _parseUrlEncoded(request);
} }
@override @override
Future<bool> canLogout(RequestContext req, ResponseContext res) async => true; Future<User> authenticate(RequestContext req, ResponseContext res,
@override
authenticate(RequestContext req, ResponseContext res,
[AngelAuthOptions options]) async { [AngelAuthOptions options]) async {
if (options != null) { if (options != null) {
return await authenticateCallback(req, res, options); return await authenticateCallback(req, res, options);
} else { } else {
var result = await createRequestToken(); var result = await createRequestToken();
String token = result['oauth_token']; var token = result['oauth_token'];
res.redirect("$_ENDPOINT/oauth/authenticate?oauth_token=$token"); var url = baseUrl.replace(
return false; path: p.join(baseUrl.path, 'oauth/authenticate'),
queryParameters: {'oauth_token': token});
res.redirect(url);
return null;
} }
} }
Future authenticateCallback( Future<User> authenticateCallback(
RequestContext req, ResponseContext res, AngelAuthOptions options) async { RequestContext req, ResponseContext res, AngelAuthOptions options) async {
var token = req.query['oauth_token']; // TODO: Handle errors
var verifier = req.query['oauth_verifier']; print('Query: ${req.queryParameters}');
var loginData = await createAccessToken(token, verifier); var token = req.queryParameters['oauth_token'] as String;
var verifier = req.queryParameters['oauth_verifier'] as String;
var credentials = new TwitterCredentials( var loginData = await _createAccessToken(token, verifier);
consumerKey: config['key'], var twitter = Twitter(this.options.clientId, this.options.clientSecret,
consumerSecret: config['secret'], loginData['oauth_token'], loginData['oauth_token_secret']);
accessToken: loginData['oauth_token'], return await this.verifier(twitter, req, res);
accessTokenSecret: loginData['oauth_token_secret']
);
var twit = new Twit(credentials);
return await this.verifier(twit);
} }
} }

View file

@ -1,13 +1,15 @@
author: "Tobe O <thosakwe@gmail.com>" author: "Tobe O <thosakwe@gmail.com>"
description: "angel_auth strategy for Twitter login." description: "angel_auth strategy for Twitter login."
environment: environment:
sdk: ">=1.19.0" sdk: ">=2.0.0 <3.0.0"
homepage: "https://github.com/angel-dart/auth_twitter.git" homepage: "https://github.com/angel-dart/auth_twitter.git"
name: "angel_auth_twitter" name: "angel_auth_twitter"
version: "1.0.2" version: 2.0.0
dependencies: dependencies:
angel_auth: "^1.0.0-dev" angel_auth: ^2.0.0
random_string: "^0.0.1" http: ^0.12.0
twit: ^0.0.0 random_string: ^0.0.2
twitter: ^1.0.0
dev_dependencies: dev_dependencies:
angel_diagnostics: "^1.0.0-dev+5" logging: ^0.11.0
pedantic: ^1.0.0