Angel 2 support

This commit is contained in:
Tobe O 2018-11-08 10:32:36 -05:00
parent bfc8a32fd1
commit 410201acf6
11 changed files with 105 additions and 69 deletions

View file

@ -1,2 +1,3 @@
analyzer: analyzer:
strong-mode: true strong-mode:
implicit-casts: false

View file

@ -11,15 +11,15 @@ class AuthorizationException extends AngelHttpException {
@override @override
Map toJson() { Map toJson() {
var m = { var m = {
'error': errorResponse.code, 'error': errorResponse.code,
'error_description': errorResponse.description, 'error_description': errorResponse.description,
}; };
if (errorResponse.uri != null) if (errorResponse.uri != null)
m['error_uri'] = errorResponse.uri.toString(); m['error_uri'] = errorResponse.uri.toString();
return m; return m;
} }
} }
@ -59,4 +59,7 @@ class ErrorResponse {
final String state; final String state;
const ErrorResponse(this.code, this.description, this.state, {this.uri}); const ErrorResponse(this.code, this.description, this.state, {this.uri});
@override
String toString() => 'OAuth2 error ($code): $description';
} }

View file

@ -9,9 +9,9 @@ import 'token_type.dart';
typedef Future<AuthorizationTokenResponse> ExtensionGrant( typedef Future<AuthorizationTokenResponse> ExtensionGrant(
RequestContext req, ResponseContext res); RequestContext req, ResponseContext res);
String _getParam(RequestContext req, String name, String state, Future<String> _getParam(RequestContext req, String name, String state,
{bool body: false}) { {bool body: false}) async {
var map = body == true ? req.body : req.query; var map = body == true ? await req.parseBody() : await req.parseQuery();
var value = map.containsKey(name) ? map[name]?.toString() : null; var value = map.containsKey(name) ? map[name]?.toString() : null;
if (value?.isNotEmpty != true) { if (value?.isNotEmpty != true) {
@ -28,8 +28,9 @@ String _getParam(RequestContext req, String name, String state,
return value; return value;
} }
Iterable<String> _getScopes(RequestContext req, {bool body: false}) { Future<Iterable<String>> _getScopes(RequestContext req,
var map = body == true ? req.body : req.query; {bool body: false}) async {
var map = body == true ? await req.parseBody() : await req.parseQuery();
return map['scope']?.toString()?.split(' ') ?? []; return map['scope']?.toString()?.split(' ') ?? [];
} }
@ -46,7 +47,6 @@ abstract class AuthorizationServer<Client, User> {
/// Finds the [Client] application associated with the given [clientId]. /// Finds the [Client] application associated with the given [clientId].
FutureOr<Client> findClient(String clientId); FutureOr<Client> findClient(String clientId);
// TODO: Is this ever used???
/// Verify that a [client] is the one identified by the [clientSecret]. /// Verify that a [client] is the one identified by the [clientSecret].
Future<bool> verifyClient(Client client, String clientSecret); Future<bool> verifyClient(Client client, String clientSecret);
@ -101,7 +101,7 @@ abstract class AuthorizationServer<Client, User> {
new ErrorResponse( new ErrorResponse(
ErrorResponse.unsupportedResponseType, ErrorResponse.unsupportedResponseType,
'Authorization code grants are not supported.', 'Authorization code grants are not supported.',
req.query['state'] ?? '', req.uri.queryParameters['state'] ?? '',
), ),
statusCode: 400, statusCode: 400,
); );
@ -113,12 +113,13 @@ abstract class AuthorizationServer<Client, User> {
String refreshToken, String refreshToken,
Iterable<String> scopes, Iterable<String> scopes,
RequestContext req, RequestContext req,
ResponseContext res) { ResponseContext res) async {
var body = await req.parseBody();
throw new AuthorizationException( throw new AuthorizationException(
new ErrorResponse( new ErrorResponse(
ErrorResponse.unsupportedResponseType, ErrorResponse.unsupportedResponseType,
'Refreshing authorization tokens is not supported.', 'Refreshing authorization tokens is not supported.',
req.body['state'] ?? '', body['state']?.toString() ?? '',
), ),
statusCode: 400, statusCode: 400,
); );
@ -131,12 +132,13 @@ abstract class AuthorizationServer<Client, User> {
String password, String password,
Iterable<String> scopes, Iterable<String> scopes,
RequestContext req, RequestContext req,
ResponseContext res) { ResponseContext res) async {
var body = await req.parseBody();
throw new AuthorizationException( throw new AuthorizationException(
new ErrorResponse( new ErrorResponse(
ErrorResponse.unsupportedResponseType, ErrorResponse.unsupportedResponseType,
'Resource owner password credentials grants are not supported.', 'Resource owner password credentials grants are not supported.',
req.body['state'] ?? '', body['state']?.toString() ?? '',
), ),
statusCode: 400, statusCode: 400,
); );
@ -144,12 +146,13 @@ abstract class AuthorizationServer<Client, User> {
/// Performs a client credentials grant. Only use this in situations where the client is 100% trusted. /// Performs a client credentials grant. Only use this in situations where the client is 100% trusted.
Future<AuthorizationTokenResponse> clientCredentialsGrant( Future<AuthorizationTokenResponse> clientCredentialsGrant(
Client client, RequestContext req, ResponseContext res) { Client client, RequestContext req, ResponseContext res) async {
var body = await req.parseBody();
throw new AuthorizationException( throw new AuthorizationException(
new ErrorResponse( new ErrorResponse(
ErrorResponse.unsupportedResponseType, ErrorResponse.unsupportedResponseType,
'Client credentials grants are not supported.', 'Client credentials grants are not supported.',
req.body['state'] ?? '', body['state']?.toString() ?? '',
), ),
statusCode: 400, statusCode: 400,
); );
@ -161,13 +164,14 @@ abstract class AuthorizationServer<Client, User> {
String state = ''; String state = '';
try { try {
state = req.query['state']?.toString() ?? ''; var query = await req.parseQuery();
var responseType = _getParam(req, 'response_type', state); state = query['state']?.toString() ?? '';
var responseType = await _getParam(req, 'response_type', state);
if (responseType == 'code') { if (responseType == 'code') {
// Ensure client ID // Ensure client ID
// TODO: Handle confidential clients // TODO: Handle confidential clients
var clientId = _getParam(req, 'client_id', state); var clientId = await _getParam(req, 'client_id', state);
// Find client // Find client
var client = await findClient(clientId); var client = await findClient(clientId);
@ -181,17 +185,17 @@ abstract class AuthorizationServer<Client, User> {
} }
// Grab redirect URI // Grab redirect URI
var redirectUri = _getParam(req, 'redirect_uri', state); var redirectUri = await _getParam(req, 'redirect_uri', state);
// Grab scopes // Grab scopes
var scopes = _getScopes(req); var scopes = await _getScopes(req);
return await requestAuthorizationCode( return await requestAuthorizationCode(
client, redirectUri, scopes, state, req, res); client, redirectUri, scopes, state, req, res);
} }
if (responseType == 'token') { if (responseType == 'token') {
var clientId = _getParam(req, 'client_id', state); var clientId = await _getParam(req, 'client_id', state);
var client = await findClient(clientId); var client = await findClient(clientId);
if (client == null) { if (client == null) {
@ -202,10 +206,10 @@ abstract class AuthorizationServer<Client, User> {
)); ));
} }
var redirectUri = _getParam(req, 'redirect_uri', state); var redirectUri = await _getParam(req, 'redirect_uri', state);
// Grab scopes // Grab scopes
var scopes = _getScopes(req); var scopes = await _getScopes(req);
var token = var token =
await implicitGrant(client, redirectUri, scopes, state, req, res); await implicitGrant(client, redirectUri, scopes, state, req, res);
@ -284,11 +288,11 @@ abstract class AuthorizationServer<Client, User> {
try { try {
AuthorizationTokenResponse response; 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') { if (grantType != 'authorization_code') {
var match = var match =
@ -337,19 +341,21 @@ abstract class AuthorizationServer<Client, User> {
} }
if (grantType == 'authorization_code') { if (grantType == 'authorization_code') {
var code = _getParam(req, 'code', state, body: true); var code = await _getParam(req, 'code', state, body: true);
var redirectUri = _getParam(req, 'redirect_uri', state, body: true); var redirectUri =
await _getParam(req, 'redirect_uri', state, body: true);
response = await exchangeAuthorizationCodeForToken( response = await exchangeAuthorizationCodeForToken(
code, redirectUri, req, res); code, redirectUri, req, res);
} else if (grantType == 'refresh_token') { } else if (grantType == 'refresh_token') {
var refreshToken = _getParam(req, 'refresh_token', state, body: true); var refreshToken =
var scopes = _getScopes(req); await _getParam(req, 'refresh_token', state, body: true);
var scopes = await _getScopes(req);
response = await refreshAuthorizationToken( response = await refreshAuthorizationToken(
client, refreshToken, scopes, req, res); client, refreshToken, scopes, req, res);
} else if (grantType == 'password') { } else if (grantType == 'password') {
var username = _getParam(req, 'username', state, body: true); var username = await _getParam(req, 'username', state, body: true);
var password = _getParam(req, 'password', state, body: true); var password = await _getParam(req, 'password', state, body: true);
var scopes = _getScopes(req); var scopes = await _getScopes(req);
response = await resourceOwnerPasswordCredentialsGrant( response = await resourceOwnerPasswordCredentialsGrant(
client, username, password, scopes, req, res); client, username, password, scopes, req, res);
} else if (grantType == 'client_credentials') { } else if (grantType == 'client_credentials') {

View file

@ -1,15 +1,15 @@
name: angel_oauth2 name: angel_oauth2
author: Tobe O <thosakwe@gmail.com> author: Tobe O <thosakwe@gmail.com>
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 homepage: https://github.com/angel-dart/oauth2.git
version: 1.0.0+1 version: 2.0.0
environment: environment:
sdk: ">=1.8.0 <3.0.0" sdk: ">=2.0.0-dev <3.0.0"
dependencies: dependencies:
angel_framework: ^1.0.0-dev angel_framework: ^2.0.0-alpha
angel_http_exception: ^1.0.0 angel_http_exception: ^1.0.0
dart2_constant: ^1.0.0
dev_dependencies: dev_dependencies:
angel_test: ^1.1.0-alpha angel_test: ^2.0.0-alpha
oauth2: ^1.0.0 oauth2: ^1.0.0
test: ^0.12.0 test: ^1.0.0
uuid: ^1.0.0

View file

@ -1,5 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:collection';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel_framework/angel_framework.dart';
import 'package:angel_framework/http.dart';
import 'package:angel_oauth2/angel_oauth2.dart'; import 'package:angel_oauth2/angel_oauth2.dart';
import 'package:angel_test/angel_test.dart'; import 'package:angel_test/angel_test.dart';
import 'package:dart2_constant/convert.dart'; import 'package:dart2_constant/convert.dart';
@ -15,9 +17,9 @@ main() {
TestClient testClient; TestClient testClient;
setUp(() async { setUp(() async {
app = new Angel()..lazyParseBodies = true; app = new Angel();
app.configuration['properties'] = app.configuration; app.configuration['properties'] = app.configuration;
app.inject('authCodes', <String, String>{}); app.container.registerSingleton(new AuthCodes());
var server = new _Server(); var server = new _Server();
@ -96,7 +98,7 @@ main() {
var response = await testClient.client.get(url); var response = await testClient.client.get(url);
print('Body: ${response.body}'); 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); var client = await grant.handleAuthorizationCode(authCode);
expect(client.credentials.accessToken, authCode + '_access'); expect(client.credentials.accessToken, authCode + '_access');
}); });
@ -107,7 +109,7 @@ main() {
var response = await testClient.client.get(url); var response = await testClient.client.get(url);
print('Body: ${response.body}'); 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); var client = await grant.handleAuthorizationCode(authCode);
expect(client.credentials.accessToken, authCode + '_access'); expect(client.credentials.accessToken, authCode + '_access');
expect(client.credentials.canRefresh, isTrue); expect(client.credentials.canRefresh, isTrue);
@ -141,8 +143,8 @@ class _Server extends AuthorizationServer<PseudoApplication, Map> {
if (state == 'hello') if (state == 'hello')
return 'Hello ${pseudoApplication.id}:${pseudoApplication.secret}'; return 'Hello ${pseudoApplication.id}:${pseudoApplication.secret}';
var authCode = _uuid.v4(); var authCode = _uuid.v4() as String;
var authCodes = req.grab<Map<String, String>>('authCodes'); var authCodes = req.container.make<AuthCodes>();
authCodes[authCode] = state; authCodes[authCode] = state;
res.headers['content-type'] = 'application/json'; res.headers['content-type'] = 'application/json';
@ -157,10 +159,29 @@ class _Server extends AuthorizationServer<PseudoApplication, Map> {
String redirectUri, String redirectUri,
RequestContext req, RequestContext req,
ResponseContext res) async { ResponseContext res) async {
var authCodes = req.grab<Map<String, String>>('authCodes'); var authCodes = req.container.make<AuthCodes>();
var state = authCodes[authCode]; var state = authCodes[authCode];
var refreshToken = state == 'can_refresh' ? '${authCode}_refresh' : null; var refreshToken = state == 'can_refresh' ? '${authCode}_refresh' : null;
return new AuthorizationTokenResponse('${authCode}_access', return new AuthorizationTokenResponse('${authCode}_access',
refreshToken: refreshToken); refreshToken: refreshToken);
} }
} }
class AuthCodes extends MapBase<String, String> with MapMixin<String, String> {
var inner = <String, String>{};
@override
String operator [](Object key) => inner[key];
@override
void operator []=(String key, String value) => inner[key] = value;
@override
void clear() => inner.clear();
@override
Iterable<String> get keys => inner.keys;
@override
String remove(Object key) => inner.remove(key);
}

View file

@ -11,7 +11,7 @@ main() {
TestClient client; TestClient client;
setUp(() async { setUp(() async {
var app = new Angel()..lazyParseBodies = true; var app = new Angel();
var oauth2 = new _AuthorizationServer(); var oauth2 = new _AuthorizationServer();
app.group('/oauth2', (router) { app.group('/oauth2', (router) {
@ -42,14 +42,16 @@ main() {
print('Response: ${response.body}'); print('Response: ${response.body}');
expect(response, allOf( expect(
hasStatus(200), response,
hasContentType('application/json'), allOf(
hasValidBody(new Validator({ hasStatus(200),
'token_type': equals('bearer'), hasContentType('application/json'),
'access_token': equals('foo'), hasValidBody(new Validator({
})), 'token_type': equals('bearer'),
)); 'access_token': equals('foo'),
})),
));
}); });
test('force correct id', () async { test('force correct id', () async {

View file

@ -10,7 +10,7 @@ main() {
TestClient client; TestClient client;
setUp(() async { setUp(() async {
var app = new Angel()..lazyParseBodies = true; var app = new Angel();
var oauth2 = new _AuthorizationServer(); var oauth2 = new _AuthorizationServer();
app.group('/oauth2', (router) { app.group('/oauth2', (router) {
@ -38,7 +38,8 @@ main() {
response, response,
allOf( allOf(
hasStatus(302), 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'),
)); ));
}); });
} }

View file

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel_framework/angel_framework.dart';
import 'package:angel_framework/http.dart';
import 'package:angel_oauth2/angel_oauth2.dart'; import 'package:angel_oauth2/angel_oauth2.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:oauth2/oauth2.dart' as oauth2; import 'package:oauth2/oauth2.dart' as oauth2;
@ -11,7 +12,7 @@ main() {
Uri tokenEndpoint; Uri tokenEndpoint;
setUp(() async { setUp(() async {
app = new Angel()..lazyParseBodies = true; app = new Angel();
var auth = new _AuthorizationServer(); var auth = new _AuthorizationServer();
app.group('/oauth2', (router) { app.group('/oauth2', (router) {
@ -120,11 +121,12 @@ class _AuthorizationServer
orElse: () => null); orElse: () => null);
if (user == null) { if (user == null) {
var body = await req.parseBody();
throw new AuthorizationException( throw new AuthorizationException(
new ErrorResponse( new ErrorResponse(
ErrorResponse.accessDenied, ErrorResponse.accessDenied,
'Invalid username or password.', 'Invalid username or password.',
req.body['state'] ?? '', body['state']?.toString() ?? '',
), ),
statusCode: 401, statusCode: 401,
); );