Angel 2 support
This commit is contained in:
parent
bfc8a32fd1
commit
410201acf6
11 changed files with 105 additions and 69 deletions
|
@ -1,2 +1,3 @@
|
|||
analyzer:
|
||||
strong-mode: true
|
||||
strong-mode:
|
||||
implicit-casts: false
|
|
@ -1,4 +1,4 @@
|
|||
export 'src/exception.dart';
|
||||
export 'src/response.dart';
|
||||
export 'src/server.dart';
|
||||
export 'src/token_type.dart';
|
||||
export 'src/token_type.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';
|
||||
}
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/// The various types of OAuth2 authorization tokens.
|
||||
abstract class AuthorizationTokenType {
|
||||
static const String bearer = 'bearer', mac = 'mac';
|
||||
}
|
||||
}
|
||||
|
|
14
pubspec.yaml
14
pubspec.yaml
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -17,4 +17,4 @@ class PseudoUser {
|
|||
final String username, password;
|
||||
|
||||
const PseudoUser({this.username, this.password});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'),
|
||||
));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue