From db1b0235a8c22b0513849cb25c84c869860c3ed5 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Fri, 1 Mar 2019 19:03:48 -0500 Subject: [PATCH] 2.0 almost done --- .analysis-options | 3 - .vscode/launch.json | 14 ---- CHANGELOG.md | 3 + analysis_options.yaml | 4 + example/main.dart | 85 +++++++++++++++++++++ example/server.dart | 52 ------------- lib/angel_auth_twitter.dart | 143 +++++++++++++++++------------------- pubspec.yaml | 14 ++-- 8 files changed, 166 insertions(+), 152 deletions(-) delete mode 100644 .analysis-options delete mode 100644 .vscode/launch.json create mode 100644 CHANGELOG.md create mode 100644 analysis_options.yaml create mode 100644 example/main.dart delete mode 100644 example/server.dart diff --git a/.analysis-options b/.analysis-options deleted file mode 100644 index 8dbd41c6..00000000 --- a/.analysis-options +++ /dev/null @@ -1,3 +0,0 @@ -analyzer: - exclude: - - .scripts-bin/**/*.dart \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index d298ddc0..00000000 --- a/.vscode/launch.json +++ /dev/null @@ -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": [] - } - ] -} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..0435763e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +# 2.0.0 +* Angel 2 + Dart 2 suppport. +* Use `package:twitter` instead of `package:twit`. \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 00000000..c230cee7 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:pedantic/analysis_options.yaml +analyzer: + strong-mode: + implicit-casts: false \ No newline at end of file diff --git a/example/main.dart b/example/main.dart new file mode 100644 index 00000000..619a83da --- /dev/null +++ b/example/main.dart @@ -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 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}'); +} diff --git a/example/server.dart b/example/server.dart deleted file mode 100644 index e47b379a..00000000 --- a/example/server.dart +++ /dev/null @@ -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 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}'); -} diff --git a/lib/angel_auth_twitter.dart b/lib/angel_auth_twitter.dart index ef44080c..01ab490f 100644 --- a/lib/angel_auth_twitter.dart +++ b/lib/angel_auth_twitter.dart @@ -1,30 +1,39 @@ 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'; +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:twit/io.dart'; +import 'package:twitter/twitter.dart'; -const String _ENDPOINT = "https://api.twitter.com"; +class TwitterStrategy extends AuthStrategy { + /// 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 { - HttpClient _client = new HttpClient(); - final Map config; - final TwitterAuthVerifier verifier; + /// A callback that uses Twitter to authenticate a [User]. + /// + /// As always, return `null` if authentication fails. + final FutureOr Function(Twitter, RequestContext, ResponseContext) + verifier; - @override - String get name => 'twitter'; + /// The root of Twitter's API. Defaults to `'https://api.twitter.com'`. + 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 method, String uriString, Map params, - {String tokenSecret: ""}) { + {@required 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) { @@ -35,27 +44,26 @@ class TwitterStrategy extends AuthStrategy { encoded.keys.map((key) => "$key=${encoded[key]}").join("&"); String baseString = - "$method&${Uri.encodeComponent(uriString)}&${Uri.encodeComponent( - collectedParams)}"; + "$method&${Uri.encodeComponent(uriString)}&${Uri.encodeComponent(collectedParams)}"; - String signingKey = "${Uri.encodeComponent( - config['secret'])}&$tokenSecret"; + String signingKey = + "${Uri.encodeComponent(options.clientSecret)}&$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); + return base64.encode(hmac.convert(baseString.codeUnits).bytes); } - Future _prepRequest(String path, - {String method: "GET", - Map data: const {}, + Future _prepRequest(String path, + {String method = "GET", + Map data = const {}, String accessToken, - String tokenSecret: ""}) async { - Map headers = new Map.from(data); + String tokenSecret = ''}) async { + var headers = new Map.from(data); 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 // alphanumeric string. @@ -68,10 +76,10 @@ class TwitterStrategy extends AuthStrategy { 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, - request.uri.toString().replaceAll("?${request.uri.query}", ""), headers, + headers['oauth_signature'] = _createSignature( + method, request.url.toString(), headers, tokenSecret: tokenSecret); var oauthString = headers.keys @@ -79,85 +87,66 @@ class TwitterStrategy extends AuthStrategy { .join(", "); return request - ..headers.set(HttpHeaders.AUTHORIZATION, "OAuth $oauthString"); + ..headers.addAll(headers) + ..headers['authorization'] = "OAuth $oauthString"; } - _mapifyRequest(HttpClientRequest request) async { - var rs = await request.close(); - var body = await rs.transform(UTF8.decoder).join(); + Future> _parseUrlEncoded(http.BaseRequest rq) async { + var response = await httpClient.send(rq); + var rs = await http.Response.fromStream(response); + var body = rs.body; - if (rs.statusCode != HttpStatus.OK) { + if (rs.statusCode != 200) { throw new AngelHttpException.notAuthenticated( 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; + return Uri.splitQueryString(body); } - Future> createAccessToken( + Future> _createAccessToken( 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); - - request.headers.contentType = - ContentType.parse("application/x-www-form-urlencoded"); - request.writeln("oauth_verifier=$verifier"); - - return _mapifyRequest(request); + request.bodyFields = {'oauth_verifier': verifier}; + return _parseUrlEncoded(request); } Future> createRequestToken() async { - var request = await _prepRequest("/oauth/request_token", - method: "POST", data: {"oauth_callback": config['callback']}); + var request = await _prepRequest("oauth/request_token", + method: "POST", + data: {"oauth_callback": options.redirectUri.toString()}); // _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); + return await _parseUrlEncoded(request); } @override - Future canLogout(RequestContext req, ResponseContext res) async => true; - - @override - authenticate(RequestContext req, ResponseContext res, + Future 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; + 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; } } - Future authenticateCallback( + 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); - - var credentials = new TwitterCredentials( - consumerKey: config['key'], - consumerSecret: config['secret'], - accessToken: loginData['oauth_token'], - accessTokenSecret: loginData['oauth_token_secret'] - ); - - var twit = new Twit(credentials); - return await this.verifier(twit); + // 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); } } diff --git a/pubspec.yaml b/pubspec.yaml index 3b28bf23..05820b95 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,13 +1,15 @@ author: "Tobe O " description: "angel_auth strategy for Twitter login." environment: - sdk: ">=1.19.0" + sdk: ">=2.0.0 <3.0.0" homepage: "https://github.com/angel-dart/auth_twitter.git" name: "angel_auth_twitter" -version: "1.0.2" +version: 2.0.0 dependencies: - angel_auth: "^1.0.0-dev" - random_string: "^0.0.1" - twit: ^0.0.0 + angel_auth: ^2.0.0 + http: ^0.12.0 + random_string: ^0.0.2 + twitter: ^1.0.0 dev_dependencies: - angel_diagnostics: "^1.0.0-dev+5" + logging: ^0.11.0 + pedantic: ^1.0.0