:)
This commit is contained in:
parent
1160253fd1
commit
ce7efe3e29
7 changed files with 235 additions and 0 deletions
3
.analysis-options
Normal file
3
.analysis-options
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
analyzer:
|
||||||
|
exclude:
|
||||||
|
- .scripts-bin/**/*.dart
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -5,6 +5,7 @@
|
||||||
.packages
|
.packages
|
||||||
.project
|
.project
|
||||||
.pub/
|
.pub/
|
||||||
|
.scripts-bin/
|
||||||
build/
|
build/
|
||||||
**/packages/
|
**/packages/
|
||||||
|
|
||||||
|
@ -25,3 +26,5 @@ doc/api/
|
||||||
# Don't commit pubspec lock file
|
# Don't commit pubspec lock file
|
||||||
# (Library packages only! Remove pattern if developing an application package)
|
# (Library packages only! Remove pattern if developing an application package)
|
||||||
pubspec.lock
|
pubspec.lock
|
||||||
|
|
||||||
|
log.txt
|
14
.vscode/launch.json
vendored
Normal file
14
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"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": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -1,2 +1,4 @@
|
||||||
# auth_twitter
|
# auth_twitter
|
||||||
angel_auth strategy for Twitter login.
|
angel_auth strategy for Twitter login.
|
||||||
|
|
||||||
|
See the [example](example/server.dart);
|
44
example/server.dart
Normal file
44
example/server.dart
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
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';
|
||||||
|
|
||||||
|
const Map<String, String> TWITTER_CONFIG = const {
|
||||||
|
'callback': 'http://localhost:3000/auth/twitter/callback',
|
||||||
|
'key': 'qlrBWXneoSYZKS2bT4TGHaNaV',
|
||||||
|
'secret': 'n2oA0ZtR7TzYincpMYElRpyYovAQlhYizTkTm2x5QxjH6mLVyE'
|
||||||
|
};
|
||||||
|
|
||||||
|
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.id_str;
|
||||||
|
|
||||||
|
auth.deserializer = (id) async {
|
||||||
|
// Of course, in a real app, you would fetch
|
||||||
|
// user data, but not here.
|
||||||
|
return {'id': id};
|
||||||
|
};
|
||||||
|
|
||||||
|
auth.strategies.add(new TwitterStrategy(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('Hello, user #${req.user["id"]}!')
|
||||||
|
..end();
|
||||||
|
});
|
||||||
|
|
||||||
|
await new DiagnosticsServer(app, new File('log.txt')).startServer(null, 3000);
|
||||||
|
}
|
157
lib/angel_auth_twitter.dart
Normal file
157
lib/angel_auth_twitter.dart
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
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:random_string/random_string.dart' as rs;
|
||||||
|
|
||||||
|
const String _ENDPOINT = "https://api.twitter.com";
|
||||||
|
|
||||||
|
class TwitterStrategy extends AuthStrategy {
|
||||||
|
HttpClient _client = new HttpClient();
|
||||||
|
final Map<String, dynamic> config;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get name => 'twitter';
|
||||||
|
|
||||||
|
TwitterStrategy({this.config: const {}});
|
||||||
|
|
||||||
|
String _createSignature(
|
||||||
|
String method, String uriString, Map<String, String> params,
|
||||||
|
{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) {
|
||||||
|
encoded[Uri.encodeComponent(key)] = Uri.encodeComponent(params[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
String collectedParams =
|
||||||
|
encoded.keys.map((key) => "$key=${encoded[key]}").join("&");
|
||||||
|
|
||||||
|
String baseString =
|
||||||
|
"$method&${Uri.encodeComponent(uriString)}&${Uri.encodeComponent(
|
||||||
|
collectedParams)}";
|
||||||
|
|
||||||
|
String signingKey = "${Uri.encodeComponent(
|
||||||
|
config['secret'])}&$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);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<HttpClientRequest> _prepRequest(String path,
|
||||||
|
{String method: "GET",
|
||||||
|
Map data: const {},
|
||||||
|
String accessToken,
|
||||||
|
String tokenSecret: ""}) async {
|
||||||
|
Map headers = new Map.from(data);
|
||||||
|
headers["oauth_version"] = "1.0";
|
||||||
|
headers["oauth_consumer_key"] = config['key'];
|
||||||
|
|
||||||
|
// The implementation of _randomString doesn't matter - just generate a 32-char
|
||||||
|
// alphanumeric string.
|
||||||
|
headers["oauth_nonce"] = rs.randomAlphaNumeric(32);
|
||||||
|
headers["oauth_signature_method"] = "HMAC-SHA1";
|
||||||
|
headers["oauth_timestamp"] =
|
||||||
|
(new DateTime.now().millisecondsSinceEpoch / 1000).round().toString();
|
||||||
|
|
||||||
|
if (accessToken != null) {
|
||||||
|
headers["oauth_token"] = accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = await _client.openUrl(method, Uri.parse("$_ENDPOINT$path"));
|
||||||
|
|
||||||
|
headers['oauth_signature'] = _createSignature(method,
|
||||||
|
request.uri.toString().replaceAll("?${request.uri.query}", ""), headers,
|
||||||
|
tokenSecret: tokenSecret);
|
||||||
|
|
||||||
|
var oauthString = headers.keys
|
||||||
|
.map((name) => '$name="${Uri.encodeComponent(headers[name])}"')
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
|
return request
|
||||||
|
..headers.set(HttpHeaders.AUTHORIZATION, "OAuth $oauthString");
|
||||||
|
}
|
||||||
|
|
||||||
|
_mapifyRequest(HttpClientRequest request) async {
|
||||||
|
var rs = await request.close();
|
||||||
|
var body = await rs.transform(UTF8.decoder).join();
|
||||||
|
|
||||||
|
if (rs.statusCode != HttpStatus.OK) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, String>> createAccessToken(
|
||||||
|
String token, String verifier) async {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, String>> createRequestToken() async {
|
||||||
|
var request = await _prepRequest("/oauth/request_token",
|
||||||
|
method: "POST", data: {"oauth_callback": config['callback']});
|
||||||
|
|
||||||
|
// _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);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> canLogout(RequestContext req, ResponseContext res) async => true;
|
||||||
|
@override
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 oauthToken = loginData['oauth_token'];
|
||||||
|
var oauthTokenSecret = loginData['oauth_token_secret'];
|
||||||
|
|
||||||
|
var request = await _prepRequest('/1.1/account/verify_credentials.json',
|
||||||
|
accessToken: oauthToken, tokenSecret: oauthTokenSecret);
|
||||||
|
var rs = await request.close();
|
||||||
|
var body = await rs.transform(UTF8.decoder).join();
|
||||||
|
return new Extensible()..properties.addAll(JSON.decode(body));
|
||||||
|
}
|
||||||
|
}
|
12
pubspec.yaml
Normal file
12
pubspec.yaml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
author: "Tobe O <thosakwe@gmail.com>"
|
||||||
|
description: "angel_auth strategy for Twitter login."
|
||||||
|
environment:
|
||||||
|
sdk: ">=1.19.0"
|
||||||
|
homepage: "https://github.com/angel-dart/auth_twitter.git"
|
||||||
|
name: "angel_auth_twitter"
|
||||||
|
version: "1.0.0"
|
||||||
|
dependencies:
|
||||||
|
angel_auth: "^1.0.0-dev"
|
||||||
|
random_string: "^0.0.1"
|
||||||
|
dev_dependencies:
|
||||||
|
angel_diagnostics: "^1.0.0-dev+5"
|
Loading…
Reference in a new issue