From 410201acf635e15d6e68e5b3e7926ef5960bee04 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Thu, 8 Nov 2018 10:32:36 -0500 Subject: [PATCH] Angel 2 support --- analysis_options.yaml | 3 +- lib/angel_oauth2.dart | 2 +- lib/src/exception.dart | 17 ++++---- lib/src/server.dart | 68 +++++++++++++++++-------------- lib/src/token_type.dart | 2 +- pubspec.yaml | 14 +++---- test/auth_code_test.dart | 35 ++++++++++++---- test/client_credentials_test.dart | 20 +++++---- test/common.dart | 2 +- test/implicit_grant_test.dart | 5 ++- test/password_test.dart | 6 ++- 11 files changed, 105 insertions(+), 69 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 518eb901..eae1e42a 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,2 +1,3 @@ analyzer: - strong-mode: true \ No newline at end of file + strong-mode: + implicit-casts: false \ No newline at end of file diff --git a/lib/angel_oauth2.dart b/lib/angel_oauth2.dart index 6f67dab1..36e10f39 100644 --- a/lib/angel_oauth2.dart +++ b/lib/angel_oauth2.dart @@ -1,4 +1,4 @@ export 'src/exception.dart'; export 'src/response.dart'; export 'src/server.dart'; -export 'src/token_type.dart'; \ No newline at end of file +export 'src/token_type.dart'; diff --git a/lib/src/exception.dart b/lib/src/exception.dart index 3e140784..3b4da075 100644 --- a/lib/src/exception.dart +++ b/lib/src/exception.dart @@ -11,15 +11,15 @@ class AuthorizationException extends AngelHttpException { @override Map toJson() { - var m = { - 'error': errorResponse.code, - 'error_description': errorResponse.description, - }; + var m = { + 'error': errorResponse.code, + 'error_description': errorResponse.description, + }; - if (errorResponse.uri != null) - m['error_uri'] = errorResponse.uri.toString(); + if (errorResponse.uri != null) + m['error_uri'] = errorResponse.uri.toString(); - return m; + return m; } } @@ -59,4 +59,7 @@ class ErrorResponse { final String state; const ErrorResponse(this.code, this.description, this.state, {this.uri}); + + @override + String toString() => 'OAuth2 error ($code): $description'; } diff --git a/lib/src/server.dart b/lib/src/server.dart index 0e979988..0da45100 100644 --- a/lib/src/server.dart +++ b/lib/src/server.dart @@ -9,9 +9,9 @@ import 'token_type.dart'; typedef Future ExtensionGrant( RequestContext req, ResponseContext res); -String _getParam(RequestContext req, String name, String state, - {bool body: false}) { - var map = body == true ? req.body : req.query; +Future _getParam(RequestContext req, String name, String state, + {bool body: false}) async { + var map = body == true ? await req.parseBody() : await req.parseQuery(); var value = map.containsKey(name) ? map[name]?.toString() : null; if (value?.isNotEmpty != true) { @@ -28,8 +28,9 @@ String _getParam(RequestContext req, String name, String state, return value; } -Iterable _getScopes(RequestContext req, {bool body: false}) { - var map = body == true ? req.body : req.query; +Future> _getScopes(RequestContext req, + {bool body: false}) async { + var map = body == true ? await req.parseBody() : await req.parseQuery(); return map['scope']?.toString()?.split(' ') ?? []; } @@ -46,7 +47,6 @@ abstract class AuthorizationServer { /// Finds the [Client] application associated with the given [clientId]. FutureOr findClient(String clientId); - // TODO: Is this ever used??? /// Verify that a [client] is the one identified by the [clientSecret]. Future verifyClient(Client client, String clientSecret); @@ -101,7 +101,7 @@ abstract class AuthorizationServer { new ErrorResponse( ErrorResponse.unsupportedResponseType, 'Authorization code grants are not supported.', - req.query['state'] ?? '', + req.uri.queryParameters['state'] ?? '', ), statusCode: 400, ); @@ -113,12 +113,13 @@ abstract class AuthorizationServer { String refreshToken, Iterable scopes, RequestContext req, - ResponseContext res) { + ResponseContext res) async { + var body = await req.parseBody(); throw new AuthorizationException( new ErrorResponse( ErrorResponse.unsupportedResponseType, 'Refreshing authorization tokens is not supported.', - req.body['state'] ?? '', + body['state']?.toString() ?? '', ), statusCode: 400, ); @@ -131,12 +132,13 @@ abstract class AuthorizationServer { String password, Iterable scopes, RequestContext req, - ResponseContext res) { + ResponseContext res) async { + var body = await req.parseBody(); throw new AuthorizationException( new ErrorResponse( ErrorResponse.unsupportedResponseType, 'Resource owner password credentials grants are not supported.', - req.body['state'] ?? '', + body['state']?.toString() ?? '', ), statusCode: 400, ); @@ -144,12 +146,13 @@ abstract class AuthorizationServer { /// Performs a client credentials grant. Only use this in situations where the client is 100% trusted. Future clientCredentialsGrant( - Client client, RequestContext req, ResponseContext res) { + Client client, RequestContext req, ResponseContext res) async { + var body = await req.parseBody(); throw new AuthorizationException( new ErrorResponse( ErrorResponse.unsupportedResponseType, 'Client credentials grants are not supported.', - req.body['state'] ?? '', + body['state']?.toString() ?? '', ), statusCode: 400, ); @@ -161,13 +164,14 @@ abstract class AuthorizationServer { String state = ''; try { - state = req.query['state']?.toString() ?? ''; - var responseType = _getParam(req, 'response_type', state); + var query = await req.parseQuery(); + state = query['state']?.toString() ?? ''; + var responseType = await _getParam(req, 'response_type', state); if (responseType == 'code') { // Ensure client ID // TODO: Handle confidential clients - var clientId = _getParam(req, 'client_id', state); + var clientId = await _getParam(req, 'client_id', state); // Find client var client = await findClient(clientId); @@ -181,17 +185,17 @@ abstract class AuthorizationServer { } // Grab redirect URI - var redirectUri = _getParam(req, 'redirect_uri', state); + var redirectUri = await _getParam(req, 'redirect_uri', state); // Grab scopes - var scopes = _getScopes(req); + var scopes = await _getScopes(req); return await requestAuthorizationCode( client, redirectUri, scopes, state, req, res); } if (responseType == 'token') { - var clientId = _getParam(req, 'client_id', state); + var clientId = await _getParam(req, 'client_id', state); var client = await findClient(clientId); if (client == null) { @@ -202,10 +206,10 @@ abstract class AuthorizationServer { )); } - var redirectUri = _getParam(req, 'redirect_uri', state); + var redirectUri = await _getParam(req, 'redirect_uri', state); // Grab scopes - var scopes = _getScopes(req); + var scopes = await _getScopes(req); var token = await implicitGrant(client, redirectUri, scopes, state, req, res); @@ -284,11 +288,11 @@ abstract class AuthorizationServer { try { AuthorizationTokenResponse response; - await req.parse(); + var body = await req.parseBody(); - state = req.body['state'] ?? ''; + state = body['state']?.toString() ?? ''; - var grantType = _getParam(req, 'grant_type', state, body: true); + var grantType = await _getParam(req, 'grant_type', state, body: true); if (grantType != 'authorization_code') { var match = @@ -337,19 +341,21 @@ abstract class AuthorizationServer { } if (grantType == 'authorization_code') { - var code = _getParam(req, 'code', state, body: true); - var redirectUri = _getParam(req, 'redirect_uri', state, body: true); + var code = await _getParam(req, 'code', state, body: true); + var redirectUri = + await _getParam(req, 'redirect_uri', state, body: true); response = await exchangeAuthorizationCodeForToken( code, redirectUri, req, res); } else if (grantType == 'refresh_token') { - var refreshToken = _getParam(req, 'refresh_token', state, body: true); - var scopes = _getScopes(req); + var refreshToken = + await _getParam(req, 'refresh_token', state, body: true); + var scopes = await _getScopes(req); response = await refreshAuthorizationToken( client, refreshToken, scopes, req, res); } else if (grantType == 'password') { - var username = _getParam(req, 'username', state, body: true); - var password = _getParam(req, 'password', state, body: true); - var scopes = _getScopes(req); + var username = await _getParam(req, 'username', state, body: true); + var password = await _getParam(req, 'password', state, body: true); + var scopes = await _getScopes(req); response = await resourceOwnerPasswordCredentialsGrant( client, username, password, scopes, req, res); } else if (grantType == 'client_credentials') { diff --git a/lib/src/token_type.dart b/lib/src/token_type.dart index 3338828f..73748a90 100644 --- a/lib/src/token_type.dart +++ b/lib/src/token_type.dart @@ -1,4 +1,4 @@ /// The various types of OAuth2 authorization tokens. abstract class AuthorizationTokenType { static const String bearer = 'bearer', mac = 'mac'; -} \ No newline at end of file +} diff --git a/pubspec.yaml b/pubspec.yaml index 5eb6dce3..57f19f0c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,15 +1,15 @@ name: angel_oauth2 author: Tobe O -description: angel_auth strategy for in-house OAuth2 login. +description: A class containing handlers that can be used within Angel to build a spec-compliant OAuth 2.0 server. homepage: https://github.com/angel-dart/oauth2.git -version: 1.0.0+1 +version: 2.0.0 environment: - sdk: ">=1.8.0 <3.0.0" + sdk: ">=2.0.0-dev <3.0.0" dependencies: - angel_framework: ^1.0.0-dev + angel_framework: ^2.0.0-alpha angel_http_exception: ^1.0.0 - dart2_constant: ^1.0.0 dev_dependencies: - angel_test: ^1.1.0-alpha + angel_test: ^2.0.0-alpha oauth2: ^1.0.0 - test: ^0.12.0 + test: ^1.0.0 + uuid: ^1.0.0 diff --git a/test/auth_code_test.dart b/test/auth_code_test.dart index bb82e4b7..24ed0533 100644 --- a/test/auth_code_test.dart +++ b/test/auth_code_test.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'dart:collection'; import 'package:angel_framework/angel_framework.dart'; +import 'package:angel_framework/http.dart'; import 'package:angel_oauth2/angel_oauth2.dart'; import 'package:angel_test/angel_test.dart'; import 'package:dart2_constant/convert.dart'; @@ -15,9 +17,9 @@ main() { TestClient testClient; setUp(() async { - app = new Angel()..lazyParseBodies = true; + app = new Angel(); app.configuration['properties'] = app.configuration; - app.inject('authCodes', {}); + app.container.registerSingleton(new AuthCodes()); var server = new _Server(); @@ -96,7 +98,7 @@ main() { var response = await testClient.client.get(url); print('Body: ${response.body}'); - var authCode = json.decode(response.body)['code']; + var authCode = json.decode(response.body)['code'].toString(); var client = await grant.handleAuthorizationCode(authCode); expect(client.credentials.accessToken, authCode + '_access'); }); @@ -107,7 +109,7 @@ main() { var response = await testClient.client.get(url); print('Body: ${response.body}'); - var authCode = json.decode(response.body)['code']; + var authCode = json.decode(response.body)['code'].toString(); var client = await grant.handleAuthorizationCode(authCode); expect(client.credentials.accessToken, authCode + '_access'); expect(client.credentials.canRefresh, isTrue); @@ -141,8 +143,8 @@ class _Server extends AuthorizationServer { if (state == 'hello') return 'Hello ${pseudoApplication.id}:${pseudoApplication.secret}'; - var authCode = _uuid.v4(); - var authCodes = req.grab>('authCodes'); + var authCode = _uuid.v4() as String; + var authCodes = req.container.make(); authCodes[authCode] = state; res.headers['content-type'] = 'application/json'; @@ -157,10 +159,29 @@ class _Server extends AuthorizationServer { String redirectUri, RequestContext req, ResponseContext res) async { - var authCodes = req.grab>('authCodes'); + var authCodes = req.container.make(); var state = authCodes[authCode]; var refreshToken = state == 'can_refresh' ? '${authCode}_refresh' : null; return new AuthorizationTokenResponse('${authCode}_access', refreshToken: refreshToken); } } + +class AuthCodes extends MapBase with MapMixin { + var inner = {}; + + @override + String operator [](Object key) => inner[key]; + + @override + void operator []=(String key, String value) => inner[key] = value; + + @override + void clear() => inner.clear(); + + @override + Iterable get keys => inner.keys; + + @override + String remove(Object key) => inner.remove(key); +} diff --git a/test/client_credentials_test.dart b/test/client_credentials_test.dart index b82a6445..939591f5 100644 --- a/test/client_credentials_test.dart +++ b/test/client_credentials_test.dart @@ -11,7 +11,7 @@ main() { TestClient client; setUp(() async { - var app = new Angel()..lazyParseBodies = true; + var app = new Angel(); var oauth2 = new _AuthorizationServer(); app.group('/oauth2', (router) { @@ -42,14 +42,16 @@ main() { print('Response: ${response.body}'); - expect(response, allOf( - hasStatus(200), - hasContentType('application/json'), - hasValidBody(new Validator({ - 'token_type': equals('bearer'), - 'access_token': equals('foo'), - })), - )); + expect( + response, + allOf( + hasStatus(200), + hasContentType('application/json'), + hasValidBody(new Validator({ + 'token_type': equals('bearer'), + 'access_token': equals('foo'), + })), + )); }); test('force correct id', () async { diff --git a/test/common.dart b/test/common.dart index 62c8adc4..9ac194f8 100644 --- a/test/common.dart +++ b/test/common.dart @@ -17,4 +17,4 @@ class PseudoUser { final String username, password; const PseudoUser({this.username, this.password}); -} \ No newline at end of file +} diff --git a/test/implicit_grant_test.dart b/test/implicit_grant_test.dart index a15b9cdd..caed71d3 100644 --- a/test/implicit_grant_test.dart +++ b/test/implicit_grant_test.dart @@ -10,7 +10,7 @@ main() { TestClient client; setUp(() async { - var app = new Angel()..lazyParseBodies = true; + var app = new Angel(); var oauth2 = new _AuthorizationServer(); app.group('/oauth2', (router) { @@ -38,7 +38,8 @@ main() { response, allOf( hasStatus(302), - hasHeader('location', 'http://foo.com#access_token=foo&token_type=bearer&state=bar'), + hasHeader('location', + 'http://foo.com#access_token=foo&token_type=bearer&state=bar'), )); }); } diff --git a/test/password_test.dart b/test/password_test.dart index 9baea1d5..b96ecfa8 100644 --- a/test/password_test.dart +++ b/test/password_test.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:angel_framework/angel_framework.dart'; +import 'package:angel_framework/http.dart'; import 'package:angel_oauth2/angel_oauth2.dart'; import 'package:logging/logging.dart'; import 'package:oauth2/oauth2.dart' as oauth2; @@ -11,7 +12,7 @@ main() { Uri tokenEndpoint; setUp(() async { - app = new Angel()..lazyParseBodies = true; + app = new Angel(); var auth = new _AuthorizationServer(); app.group('/oauth2', (router) { @@ -120,11 +121,12 @@ class _AuthorizationServer orElse: () => null); if (user == null) { + var body = await req.parseBody(); throw new AuthorizationException( new ErrorResponse( ErrorResponse.accessDenied, 'Invalid username or password.', - req.body['state'] ?? '', + body['state']?.toString() ?? '', ), statusCode: 401, );