diff --git a/.gitignore b/.gitignore index 7c280441..2aa48381 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,5 @@ doc/api/ # Don't commit pubspec lock file # (Library packages only! Remove pattern if developing an application package) pubspec.lock + +log.txt \ No newline at end of file diff --git a/README.md b/README.md index c3b29d01..8e7ae431 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ # auth_oauth2 +[![version 0.0.0](https://img.shields.io/badge/pub-v0.0.0-red.svg)](https://pub.dartlang.org/packages/angel_auth_oauth2) + angel_auth strategy for OAuth2 login, i.e. Facebook. diff --git a/example/basic.dart b/example/basic.dart new file mode 100644 index 00000000..00ca715d --- /dev/null +++ b/example/basic.dart @@ -0,0 +1,54 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:angel_auth/angel_auth.dart'; +import 'package:angel_diagnostics/angel_diagnostics.dart'; +import 'package:angel_framework/angel_framework.dart'; +import 'package:angel_auth_oauth2/angel_auth_oauth2.dart'; +import 'package:oauth2/oauth2.dart' as oauth2; + +const Map OAUTH2_CONFIG = const { + 'callback': '', + 'key': '', + 'secret': '', + 'authorizationEndpoint': '', + 'tokenEndpoint': '' +}; + +main() async { + var app = new Angel()..use('/users', new MemoryService()); + + var auth = new AngelAuth(jwtKey: 'oauth2 example secret', allowCookie: false); + auth.deserializer = + (String idStr) => app.service('users').read(int.parse(idStr)); + auth.serializer = (User user) async => user.id; + + auth.strategies.add(new OAuth2Strategy('example_site', OAUTH2_CONFIG, + (oauth2.Client client) async { + var response = await client.get('/link/to/user/profile'); + return JSON.decode(response.body); + })); + + app.get('/auth/example_site', auth.authenticate('example_site')); + app.get( + '/auth/example_site/callback', + auth.authenticate('example_site', + new AngelAuthOptions(callback: (req, res, jwt) async { + // In real-life, you might include a pop-up callback script + res.write('Your JWT: $jwt'); + }))); + + await app.configure(auth); + await app.configure(logRequests(new File('log.txt'))); + await app.configure(profileRequests()); + + var server = await app.startServer(InternetAddress.LOOPBACK_IP_V4, 3000); + print('Listening on http://${server.address.address}:${server.port}'); +} + +class User extends MemoryModel { + String example_siteId; + + User({int id, this.example_siteId}) { + this.id = id; + } +} diff --git a/lib/angel_auth_oauth2.dart b/lib/angel_auth_oauth2.dart new file mode 100644 index 00000000..6de6f066 --- /dev/null +++ b/lib/angel_auth_oauth2.dart @@ -0,0 +1,114 @@ +library angel_auth_oauth2; + +import 'dart:async'; +import 'package:angel_auth/angel_auth.dart'; +import 'package:angel_framework/src/http/response_context.dart'; +import 'package:angel_framework/src/http/request_context.dart'; +import 'package:angel_validate/angel_validate.dart'; +import 'package:oauth2/oauth2.dart' as oauth2; + +/// Loads a user profile via OAuth2. +typedef Future ProfileLoader(oauth2.Client client); + +final Validator OAUTH2_OPTIONS_SCHEMA = new Validator({ + 'key*': isString, + 'secret*': isString, + 'authorizationEndpoint*': isString, + 'tokenEndpoint*': isString, + 'callback*': isString, + 'scopes': new isInstanceOf>() +}, defaultValues: { + 'scopes': [] +}, customErrorMessages: { + 'scopes': "'scopes' must be an Iterable of strings. You provided: {{value}}" +}); + +class AngelAuthOauth2Options { + String key, secret, authorizationEndpoint, tokenEndpoint, callback; + Iterable scopes; + + AngelAuthOauth2Options( + {this.key, + this.secret, + this.authorizationEndpoint, + this.tokenEndpoint, + this.callback, + Iterable scopes: const []}) { + this.scopes = scopes ?? []; + } + + factory AngelAuthOauth2Options.fromJson(Map json) => + new AngelAuthOauth2Options( + key: json['key'], + secret: json['secret'], + authorizationEndpoint: json['authorizationEndpoint'], + tokenEndpoint: json['tokenEndpoint'], + callback: json['callback'], + scopes: json['scopes'] ?? []); + + Map toJson() { + return { + 'key': key, + 'secret': secret, + 'authorizationEndpoint': authorizationEndpoint, + 'tokenEndpoint': tokenEndpoint, + 'callback': callback, + 'scopes': scopes.toList() + }; + } +} + +class OAuth2Strategy implements AuthStrategy { + String _name; + AngelAuthOauth2Options _options; + final ProfileLoader profileLoader; + + @override + String get name => _name; + + @override + set name(String value) => _name = name; + + /// [options] can be either a `Map` or an instance of [AngelAuthOauth2Options]. + OAuth2Strategy(this._name, options, this.profileLoader) { + if (options is AngelAuthOauth2Options) + _options = options; + else if (options is Map) + _options = new AngelAuthOauth2Options.fromJson( + OAUTH2_OPTIONS_SCHEMA.enforce(options)); + else + throw new ArgumentError('Invalid OAuth2 options: $options'); + } + + oauth2.AuthorizationCodeGrant createGrant() => + new oauth2.AuthorizationCodeGrant( + _options.key, + Uri.parse(_options.authorizationEndpoint), + Uri.parse(_options.tokenEndpoint), + secret: _options.secret); + + @override + Future authenticate(RequestContext req, ResponseContext res, + [AngelAuthOptions options]) async { + if (options != null) return authenticateCallback(req, res, options); + + var grant = createGrant(); + res.redirect(grant + .getAuthorizationUrl(Uri.parse(_options.callback), + scopes: _options.scopes) + .toString()); + return false; + } + + Future authenticateCallback(RequestContext req, ResponseContext res, + [AngelAuthOptions options]) async { + var grant = createGrant(); + await grant.getAuthorizationUrl(Uri.parse(_options.callback), + scopes: _options.scopes); + var client = await grant.handleAuthorizationResponse(req.query); + return await profileLoader(client); + } + + @override + Future canLogout(RequestContext req, ResponseContext res) async => true; +} diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 00000000..3fb18d80 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,13 @@ +name: angel_auth_oauth2 +description: angel_auth strategy for OAuth2 login, i.e. Facebook. +version: 0.0.0 +author: Tobe O +environment: + sdk: ">=1.19.0" +homepage: https://github.com/angel-dart/auth_oauth2.git +dependencies: + angel_auth: ^1.0.0-dev + angel_validate: ^1.0.0-beta + oauth2: ^1.0.0 +dev_dependencies: + angel_diagnostics: ^1.0.0-dev \ No newline at end of file