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:
|
analyzer:
|
||||||
strong-mode: true
|
strong-mode:
|
||||||
|
implicit-casts: false
|
|
@ -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';
|
||||||
}
|
}
|
||||||
|
|
|
@ -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') {
|
||||||
|
|
14
pubspec.yaml
14
pubspec.yaml
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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'),
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in a new issue