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:
strong-mode: true
strong-mode:
implicit-casts: false

View file

@ -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';
}

View file

@ -9,9 +9,9 @@ import 'token_type.dart';
typedef Future<AuthorizationTokenResponse> 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<String> _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<String> _getScopes(RequestContext req, {bool body: false}) {
var map = body == true ? req.body : req.query;
Future<Iterable<String>> _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<Client, User> {
/// Finds the [Client] application associated with the given [clientId].
FutureOr<Client> findClient(String clientId);
// TODO: Is this ever used???
/// Verify that a [client] is the one identified by the [clientSecret].
Future<bool> verifyClient(Client client, String clientSecret);
@ -101,7 +101,7 @@ abstract class AuthorizationServer<Client, User> {
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<Client, User> {
String refreshToken,
Iterable<String> 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<Client, User> {
String password,
Iterable<String> 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<Client, User> {
/// Performs a client credentials grant. Only use this in situations where the client is 100% trusted.
Future<AuthorizationTokenResponse> 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<Client, User> {
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<Client, User> {
}
// 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<Client, User> {
));
}
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<Client, User> {
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<Client, User> {
}
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') {

View file

@ -1,15 +1,15 @@
name: angel_oauth2
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
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

View file

@ -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', <String, String>{});
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<PseudoApplication, Map> {
if (state == 'hello')
return 'Hello ${pseudoApplication.id}:${pseudoApplication.secret}';
var authCode = _uuid.v4();
var authCodes = req.grab<Map<String, String>>('authCodes');
var authCode = _uuid.v4() as String;
var authCodes = req.container.make<AuthCodes>();
authCodes[authCode] = state;
res.headers['content-type'] = 'application/json';
@ -157,10 +159,29 @@ class _Server extends AuthorizationServer<PseudoApplication, Map> {
String redirectUri,
RequestContext req,
ResponseContext res) async {
var authCodes = req.grab<Map<String, String>>('authCodes');
var authCodes = req.container.make<AuthCodes>();
var state = authCodes[authCode];
var refreshToken = state == 'can_refresh' ? '${authCode}_refresh' : null;
return new AuthorizationTokenResponse('${authCode}_access',
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;
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 {

View file

@ -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'),
));
});
}

View file

@ -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,
);