diff --git a/.idea/auth_oauth2.iml b/.idea/auth_oauth2.iml index 7fe256fb..eae13016 100644 --- a/.idea/auth_oauth2.iml +++ b/.idea/auth_oauth2.iml @@ -5,8 +5,6 @@ - - diff --git a/.idea/runConfigurations/Github_Auth_Server.xml b/.idea/runConfigurations/Github_Auth_Server.xml index c68cda20..d3ff1581 100644 --- a/.idea/runConfigurations/Github_Auth_Server.xml +++ b/.idea/runConfigurations/Github_Auth_Server.xml @@ -1,6 +1,6 @@ - diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..d08eb72d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,2 @@ +# 1.0.2 +Added `getParameters` to `AngelOAuth2Options`. \ No newline at end of file diff --git a/README.md b/README.md index edb4b7cd..e7b14ff7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # auth_oauth2 -[![version 1.0.1](https://img.shields.io/badge/pub-1.0.1-brightgreen.svg)](https://pub.dartlang.org/packages/angel_auth_oauth2) +[![Pub](https://img.shields.io/pub/v/angel_auth_oauth2.svg)](https://pub.dartlang.org/packages/angel_auth_oauth2) `package:angel_auth` strategy for OAuth2 login, i.e. Facebook or Github. @@ -13,7 +13,7 @@ configureServer(Angel app) async { var opts = new AngelOAuth2Options.fromJson(map); // Create in-place: - const AngelAuthOAuth2Options OAUTH2_CONFIG = const AngelAuthOAuth2Options( + var opts = const AngelAuthOAuth2Options( callback: '', key: '', secret: '', @@ -98,9 +98,31 @@ you can add it in the `AngelOAuth2Options` constructor: ```dart configureServer(Angel app) async { - const AngelOAuth2Options OPTS = const AngelOAuth2Options( + var opts = const AngelOAuth2Options( // ... delimiter: ',' ); } +``` + +## Handling non-JSON responses +Many OAuth2 providers do not follow the specification, and do not return +`application/json` responses. + +You can add a `getParameters` callback to parse the contents of any arbitrary +response: + +```dart +var opts = const AngelOAuth2Options( + // ... + getParameters: (contentType, body) { + if (contentType.type == 'application') { + if (contentType.subtype == 'x-www-form-urlencoded') + return Uri.splitQueryString(body); + else if (contentType.subtype == 'json') return JSON.decode(body); + } + + throw new FormatException('Invalid content-type $contentType; expected application/x-www-form-urlencoded or application/json.'); + } +); ``` \ No newline at end of file diff --git a/.analysis-options b/analysis_options.yaml similarity index 100% rename from .analysis-options rename to analysis_options.yaml diff --git a/example/github.dart b/example/main.dart similarity index 66% rename from example/github.dart rename to example/main.dart index cb92b891..d84141e5 100644 --- a/example/github.dart +++ b/example/main.dart @@ -1,31 +1,43 @@ 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_framework/common.dart'; import 'package:angel_auth_oauth2/angel_auth_oauth2.dart'; +import 'package:logging/logging.dart'; import 'package:oauth2/oauth2.dart' as oauth2; -const AngelAuthOAuth2Options OAUTH2_CONFIG = const AngelAuthOAuth2Options( +final AngelAuthOAuth2Options oAuth2Config = new AngelAuthOAuth2Options( callback: 'http://localhost:3000/auth/github/callback', key: '6caeaf5d4c04936ec34f', secret: '178360518cf9de4802e2346a4b6ebec525dc4427', authorizationEndpoint: 'http://github.com/login/oauth/authorize', - tokenEndpoint: 'https://github.com/login/oauth/access_token'); + tokenEndpoint: 'https://github.com/login/oauth/access_token', + getParameters: (contentType, body) { + if (contentType.type == 'application') { + if (contentType.subtype == 'x-www-form-urlencoded') + return Uri.splitQueryString(body); + else if (contentType.subtype == 'json') return JSON.decode(body); + } + + throw new FormatException('Invalid content-type $contentType; expected application/x-www-form-urlencoded or application/json.'); + }); main() async { var app = new Angel(); app.lazyParseBodies = true; app.use('/users', new MapService()); - var auth = new AngelAuth(jwtKey: 'oauth2 example secret', allowCookie: false); + var auth = + new AngelAuth(jwtKey: 'oauth2 example secret', allowCookie: false); + + auth.deserializer = + (id) => app.service('users').read(id).then((u) => User.parse(u)); - auth.deserializer = app.service('users').read; auth.serializer = (User user) async => user.id; auth.strategies.add( - new OAuth2Strategy('github', OAUTH2_CONFIG, (oauth2.Client client) async { + new OAuth2Strategy('github', oAuth2Config, (oauth2.Client client) async { var response = await client.get('https://api.github.com/user'); var ghUser = JSON.decode(response.body); var id = ghUser['id']; @@ -41,7 +53,7 @@ main() async { // Otherwise,create a user return await app .service('users') - .create({'githubId': id}).then(User.parse); + .create({'githubId': id}).then((u) => User.parse(u)); } })); @@ -57,10 +69,12 @@ main() async { res.write('Your JWT: $jwt'); }))); - await app.configure(auth); - await app.configure(logRequests()); + await app.configure(auth.configureServer); - var server = await app.startServer(InternetAddress.LOOPBACK_IP_V4, 3000); + app.logger = new Logger('angel')..onRecord.listen(print); + + var http = new AngelHttp(app); + var server = await http.startServer(InternetAddress.LOOPBACK_IP_V4, 3000); var url = 'http://${server.address.address}:${server.port}'; print('Listening on $url'); print('View user listing: $url/users'); diff --git a/lib/angel_auth_oauth2.dart b/lib/angel_auth_oauth2.dart index 226ef36a..54d0a824 100644 --- a/lib/angel_auth_oauth2.dart +++ b/lib/angel_auth_oauth2.dart @@ -2,21 +2,18 @@ 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_framework/angel_framework.dart'; import 'package:angel_validate/angel_validate.dart'; +import 'package:http_parser/http_parser.dart'; import 'package:oauth2/oauth2.dart' as oauth2; -/// Loads a user profile via OAuth2. -typedef Future OAuth2Verifier(oauth2.Client client); - final Validator OAUTH2_OPTIONS_SCHEMA = new Validator({ 'key*': isString, 'secret*': isString, - 'authorizationEndpoint*': isString, - 'tokenEndpoint*': isString, + 'authorizationEndpoint*': anyOf(isString, const isInstanceOf()), + 'tokenEndpoint*': anyOf(isString, const isInstanceOf()), 'callback*': isString, - 'scopes': new isInstanceOf>() + 'scopes': const isInstanceOf>() }, defaultValues: { 'scopes': [] }, customErrorMessages: { @@ -32,10 +29,10 @@ class AngelAuthOAuth2Options { final String secret; /// The remote endpoint that prompts external users for authentication credentials. - final String authorizationEndpoint; + final authorizationEndpoint; /// The remote endpoint that exchanges auth codes for access tokens. - final String tokenEndpoint; + final tokenEndpoint; /// The callback URL that the OAuth2 server should redirect authenticated users to. final String callback; @@ -44,6 +41,8 @@ class AngelAuthOAuth2Options { final String delimiter; final Iterable scopes; + final Map Function(MediaType, String) getParameters; + const AngelAuthOAuth2Options( {this.key, this.secret, @@ -51,7 +50,8 @@ class AngelAuthOAuth2Options { this.tokenEndpoint, this.callback, this.delimiter: ' ', - this.scopes: const []}); + this.scopes: const [], + this.getParameters}); factory AngelAuthOAuth2Options.fromJson(Map json) => new AngelAuthOAuth2Options( @@ -62,7 +62,7 @@ class AngelAuthOAuth2Options { callback: json['callback'], scopes: json['scopes'] ?? []); - Map toJson() { + Map toJson() { return { 'key': key, 'secret': secret, @@ -74,19 +74,14 @@ class AngelAuthOAuth2Options { } } -class OAuth2Strategy implements AuthStrategy { - String _name; +class OAuth2Strategy implements AuthStrategy { + final FutureOr Function(oauth2.Client) verifier; + String name; + AngelAuthOAuth2Options _options; - final OAuth2Verifier verifier; - - @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.verifier) { + OAuth2Strategy(this.name, options, this.verifier) { if (options is AngelAuthOAuth2Options) _options = options; else if (options is Map) @@ -102,7 +97,8 @@ class OAuth2Strategy implements AuthStrategy { Uri.parse(_options.authorizationEndpoint), Uri.parse(_options.tokenEndpoint), secret: _options.secret, - delimiter: _options.delimiter ?? ' '); + delimiter: _options.delimiter ?? ' ', + getParameters: _options.getParameters); @override Future authenticate(RequestContext req, ResponseContext res, diff --git a/pubspec.yaml b/pubspec.yaml index f34a9ce7..06c39ae7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: angel_auth_oauth2 description: angel_auth strategy for OAuth2 login, i.e. Facebook. -version: 1.0.1 +version: 1.0.2 author: Tobe O environment: sdk: ">=1.19.0" @@ -8,6 +8,4 @@ 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 + oauth2: ^1.0.0 \ No newline at end of file