From db505a470cda8c03389beecee35d690f00786b17 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Thu, 11 Apr 2019 10:28:51 -0400 Subject: [PATCH] 2.0.0 done --- CHANGELOG.md | 4 +- example/main.dart | 13 +++++- lib/angel_auth_twitter.dart | 91 +++++++++++++++++++++++++++++-------- 3 files changed, 85 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0435763e..ad8f7b0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ # 2.0.0 * Angel 2 + Dart 2 suppport. -* Use `package:twitter` instead of `package:twit`. \ No newline at end of file +* Use `package:twitter` instead of `package:twit`. +* Add `TwitterAuthorizationException`. +* Add `onError` callback. \ No newline at end of file diff --git a/example/main.dart b/example/main.dart index 619a83da..c4a5bce6 100644 --- a/example/main.dart +++ b/example/main.dart @@ -38,11 +38,19 @@ main() async { 'http://localhost:3000/auth/twitter/callback', ), (twit, req, res) async { - var response = - await twit.twitterClient.get('/account/verify_credentials.json'); + var response = await twit.twitterClient + .get('https://api.twitter.com/1.1/account/verify_credentials.json'); var userData = json.decode(response.body) as Map; return _User(userData['screen_name'] as String); }, + (e, req, res) async { + // When an error occurs, i.e. the user declines to approve the application. + if (e.isDenial) { + res.write("Why'd you say no???"); + } else { + res.write("oops: ${e.message}"); + } + }, ); app @@ -69,6 +77,7 @@ main() async { (req, res) { var user = req.container.make<_User>(); res.write('Your Twitter handle is ${user.handle}'); + return false; }, ]), ); diff --git a/lib/angel_auth_twitter.dart b/lib/angel_auth_twitter.dart index bf1e811f..ccda7fe3 100644 --- a/lib/angel_auth_twitter.dart +++ b/lib/angel_auth_twitter.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'package:angel_auth/angel_auth.dart'; import 'package:angel_framework/angel_framework.dart'; import 'package:http/http.dart' as http; @@ -17,6 +18,10 @@ class TwitterStrategy extends AuthStrategy { final FutureOr Function(Twitter, RequestContext, ResponseContext) verifier; + /// A callback that is triggered when an OAuth2 error occurs (i.e. the user declines to login); + final FutureOr Function( + TwitterAuthorizationException, RequestContext, ResponseContext) onError; + /// The root of Twitter's API. Defaults to `'https://api.twitter.com'`. final Uri baseUrl; @@ -25,7 +30,7 @@ class TwitterStrategy extends AuthStrategy { /// The underlying [oauth.Client] used to query Twitter. oauth.Client get client => _client; - TwitterStrategy(this.options, this.verifier, + TwitterStrategy(this.options, this.verifier, this.onError, {http.BaseClient client, Uri baseUrl}) : this.baseUrl = baseUrl ?? Uri.parse('https://api.twitter.com') { var tokens = oauth.Tokens( @@ -38,8 +43,16 @@ class TwitterStrategy extends AuthStrategy { var body = rs.body; if (rs.statusCode != 200) { - throw new AngelHttpException.notAuthenticated( - message: 'Twitter authentication error: $body'); + 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}'); + } } return Uri.splitQueryString(body); @@ -49,6 +62,9 @@ class TwitterStrategy extends AuthStrategy { Future> getAccessToken(String token, String verifier) { return _client.post( baseUrl.replace(path: p.join(baseUrl.path, 'oauth/access_token')), + headers: { + 'accept': 'application/json' + }, body: { 'oauth_token': token, 'oauth_verifier': verifier @@ -61,6 +77,9 @@ class TwitterStrategy extends AuthStrategy { Future> getRequestToken() { return _client.post( baseUrl.replace(path: p.join(baseUrl.path, 'oauth/request_token')), + headers: { + 'accept': 'application/json' + }, body: { "oauth_callback": options.redirectUri.toString() }).then(handleUrlEncodedResponse); @@ -69,27 +88,59 @@ class TwitterStrategy extends AuthStrategy { @override Future authenticate(RequestContext req, ResponseContext res, [AngelAuthOptions options]) async { - if (options != null) { - return await authenticateCallback(req, res, options); - } 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}); - res.redirect(url); + try { + if (options != null) { + var result = await authenticateCallback(req, res, options); + if (result is User) + return result; + else + return null; + } 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}); + res.redirect(url); + return null; + } + } on TwitterAuthorizationException catch (e) { + var result = await onError(e, req, res); + await req.app.executeHandler(result, req, res); + await res.close(); return null; } } - Future authenticateCallback( + Future authenticateCallback( RequestContext req, ResponseContext res, AngelAuthOptions options) async { - // TODO: Handle errors - var token = req.queryParameters['oauth_token'] as String; - var verifier = req.queryParameters['oauth_verifier'] as String; - var loginData = await getAccessToken(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); + 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); + var twitter = Twitter(this.options.clientId, this.options.clientSecret, + loginData['oauth_token'], loginData['oauth_token_secret']); + return await this.verifier(twitter, req, res); + } on TwitterAuthorizationException catch (e) { + return await onError(e, req, res); + } } } + +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'; +}