2.0 almost done
This commit is contained in:
parent
69826aed59
commit
db1b0235a8
8 changed files with 166 additions and 152 deletions
|
@ -1,3 +0,0 @@
|
||||||
analyzer:
|
|
||||||
exclude:
|
|
||||||
- .scripts-bin/**/*.dart
|
|
14
.vscode/launch.json
vendored
14
.vscode/launch.json
vendored
|
@ -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
3
CHANGELOG.md
Normal 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
4
analysis_options.yaml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
include: package:pedantic/analysis_options.yaml
|
||||||
|
analyzer:
|
||||||
|
strong-mode:
|
||||||
|
implicit-casts: false
|
85
example/main.dart
Normal file
85
example/main.dart
Normal 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}');
|
||||||
|
}
|
|
@ -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}');
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
14
pubspec.yaml
14
pubspec.yaml
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue