2018-12-15 08:39:04 +00:00
|
|
|
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:logging/logging.dart';
|
|
|
|
import 'package:test/test.dart';
|
|
|
|
import 'common.dart';
|
|
|
|
|
|
|
|
main() {
|
|
|
|
Angel app;
|
2019-05-02 07:28:38 +00:00
|
|
|
Uri authorizationEndpoint, tokenEndpoint;
|
2018-12-15 08:39:04 +00:00
|
|
|
TestClient testClient;
|
|
|
|
|
|
|
|
setUp(() async {
|
2019-05-02 07:28:38 +00:00
|
|
|
app = Angel();
|
|
|
|
app.container.registerSingleton(AuthCodes());
|
2018-12-15 08:39:04 +00:00
|
|
|
|
2019-05-02 07:28:38 +00:00
|
|
|
var server = _Server();
|
2018-12-15 08:39:04 +00:00
|
|
|
|
|
|
|
app.group('/oauth2', (router) {
|
|
|
|
router
|
|
|
|
..get('/authorize', server.authorizationEndpoint)
|
|
|
|
..post('/token', server.tokenEndpoint);
|
|
|
|
});
|
|
|
|
|
2019-05-02 07:28:38 +00:00
|
|
|
app.logger = Logger('angel')
|
2018-12-15 08:39:04 +00:00
|
|
|
..onRecord.listen((rec) {
|
|
|
|
print(rec);
|
|
|
|
if (rec.error != null) print(rec.error);
|
|
|
|
if (rec.stackTrace != null) print(rec.stackTrace);
|
|
|
|
});
|
|
|
|
|
2019-05-02 07:28:38 +00:00
|
|
|
var http = AngelHttp(app);
|
2018-12-15 08:39:04 +00:00
|
|
|
var s = await http.startServer();
|
|
|
|
var url = 'http://${s.address.address}:${s.port}';
|
|
|
|
authorizationEndpoint = Uri.parse('$url/oauth2/authorize');
|
|
|
|
tokenEndpoint = Uri.parse('$url/oauth2/token');
|
|
|
|
|
|
|
|
testClient = await connectTo(app);
|
|
|
|
});
|
|
|
|
|
|
|
|
tearDown(() async {
|
|
|
|
await testClient.close();
|
|
|
|
});
|
|
|
|
|
|
|
|
group('get auth code', () {
|
|
|
|
test('with challenge + implied plain', () async {
|
|
|
|
var url = authorizationEndpoint.replace(queryParameters: {
|
|
|
|
'response_type': 'code',
|
|
|
|
'client_id': 'freddie mercury',
|
|
|
|
'redirect_uri': 'https://freddie.mercu.ry',
|
|
|
|
'code_challenge': 'foo',
|
|
|
|
});
|
|
|
|
var response = await testClient
|
|
|
|
.get(url.toString(), headers: {'accept': 'application/json'});
|
|
|
|
print(response.body);
|
|
|
|
expect(
|
|
|
|
response,
|
|
|
|
allOf(
|
|
|
|
hasStatus(200),
|
|
|
|
isJson({"code": "ok"}),
|
|
|
|
));
|
|
|
|
});
|
|
|
|
|
|
|
|
test('with challenge + plain', () async {
|
|
|
|
var url = authorizationEndpoint.replace(queryParameters: {
|
|
|
|
'response_type': 'code',
|
|
|
|
'client_id': 'freddie mercury',
|
|
|
|
'redirect_uri': 'https://freddie.mercu.ry',
|
|
|
|
'code_challenge': 'foo',
|
|
|
|
'code_challenge_method': 'plain',
|
|
|
|
});
|
|
|
|
var response = await testClient
|
|
|
|
.get(url.toString(), headers: {'accept': 'application/json'});
|
|
|
|
print(response.body);
|
|
|
|
expect(
|
|
|
|
response,
|
|
|
|
allOf(
|
|
|
|
hasStatus(200),
|
|
|
|
isJson({"code": "ok"}),
|
|
|
|
));
|
|
|
|
});
|
|
|
|
|
|
|
|
test('with challenge + s256', () async {
|
|
|
|
var url = authorizationEndpoint.replace(queryParameters: {
|
|
|
|
'response_type': 'code',
|
|
|
|
'client_id': 'freddie mercury',
|
|
|
|
'redirect_uri': 'https://freddie.mercu.ry',
|
|
|
|
'code_challenge': 'foo',
|
|
|
|
'code_challenge_method': 's256',
|
|
|
|
});
|
|
|
|
var response = await testClient
|
|
|
|
.get(url.toString(), headers: {'accept': 'application/json'});
|
|
|
|
print(response.body);
|
|
|
|
expect(
|
|
|
|
response,
|
|
|
|
allOf(
|
|
|
|
hasStatus(200),
|
|
|
|
isJson({"code": "ok"}),
|
|
|
|
));
|
|
|
|
});
|
|
|
|
|
|
|
|
test('with challenge + wrong method', () async {
|
|
|
|
var url = authorizationEndpoint.replace(queryParameters: {
|
|
|
|
'response_type': 'code',
|
|
|
|
'client_id': 'freddie mercury',
|
|
|
|
'redirect_uri': 'https://freddie.mercu.ry',
|
|
|
|
'code_challenge': 'foo',
|
|
|
|
'code_challenge_method': 'bar',
|
|
|
|
});
|
|
|
|
var response = await testClient
|
|
|
|
.get(url.toString(), headers: {'accept': 'application/json'});
|
|
|
|
print(response.body);
|
|
|
|
expect(
|
|
|
|
response,
|
|
|
|
allOf(
|
|
|
|
hasStatus(400),
|
|
|
|
isJson({
|
|
|
|
"error": "invalid_request",
|
|
|
|
"error_description":
|
|
|
|
"The `code_challenge_method` parameter must be either 'plain' or 's256'."
|
|
|
|
}),
|
|
|
|
));
|
|
|
|
});
|
|
|
|
|
|
|
|
test('with no challenge', () async {
|
|
|
|
var url = authorizationEndpoint.replace(queryParameters: {
|
|
|
|
'response_type': 'code',
|
|
|
|
'client_id': 'freddie mercury',
|
|
|
|
'redirect_uri': 'https://freddie.mercu.ry'
|
|
|
|
});
|
|
|
|
var response = await testClient
|
|
|
|
.get(url.toString(), headers: {'accept': 'application/json'});
|
|
|
|
print(response.body);
|
|
|
|
expect(
|
|
|
|
response,
|
|
|
|
allOf(
|
|
|
|
hasStatus(400),
|
|
|
|
isJson({
|
|
|
|
"error": "invalid_request",
|
|
|
|
"error_description": "Missing `code_challenge` parameter."
|
|
|
|
}),
|
|
|
|
));
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
group('get token', () {
|
|
|
|
test('with correct verifier', () async {
|
|
|
|
var url = tokenEndpoint.replace(
|
|
|
|
userInfo: '${pseudoApplication.id}:${pseudoApplication.secret}');
|
|
|
|
var response = await testClient.post(url.toString(), headers: {
|
|
|
|
'accept': 'application/json',
|
2019-05-03 07:29:59 +00:00
|
|
|
// 'authorization': 'Basic ' + base64Url.encode(ascii.encode(url.userInfo))
|
2018-12-15 08:39:04 +00:00
|
|
|
}, body: {
|
|
|
|
'grant_type': 'authorization_code',
|
|
|
|
'client_id': 'freddie mercury',
|
|
|
|
'redirect_uri': 'https://freddie.mercu.ry',
|
|
|
|
'code': 'ok',
|
|
|
|
'code_verifier': 'hello',
|
|
|
|
});
|
|
|
|
|
|
|
|
print(response.body);
|
|
|
|
expect(
|
|
|
|
response,
|
|
|
|
allOf(
|
|
|
|
hasStatus(200),
|
|
|
|
isJson({"token_type": "bearer", "access_token": "yes"}),
|
|
|
|
));
|
|
|
|
});
|
|
|
|
test('with incorrect verifier', () async {
|
|
|
|
var url = tokenEndpoint.replace(
|
|
|
|
userInfo: '${pseudoApplication.id}:${pseudoApplication.secret}');
|
|
|
|
var response = await testClient.post(url.toString(), headers: {
|
|
|
|
'accept': 'application/json',
|
2019-05-03 07:29:59 +00:00
|
|
|
// 'authorization': 'Basic ' + base64Url.encode(ascii.encode(url.userInfo))
|
2018-12-15 08:39:04 +00:00
|
|
|
}, body: {
|
|
|
|
'grant_type': 'authorization_code',
|
|
|
|
'client_id': 'freddie mercury',
|
|
|
|
'redirect_uri': 'https://freddie.mercu.ry',
|
|
|
|
'code': 'ok',
|
|
|
|
'code_verifier': 'foo',
|
|
|
|
});
|
|
|
|
|
|
|
|
print(response.body);
|
|
|
|
expect(
|
|
|
|
response,
|
|
|
|
allOf(
|
|
|
|
hasStatus(400),
|
|
|
|
isJson({
|
|
|
|
"error": "invalid_grant",
|
|
|
|
"error_description":
|
|
|
|
"The given `code_verifier` parameter is invalid."
|
|
|
|
}),
|
|
|
|
));
|
|
|
|
});
|
|
|
|
|
|
|
|
test('with missing verifier', () async {
|
|
|
|
var url = tokenEndpoint.replace(
|
|
|
|
userInfo: '${pseudoApplication.id}:${pseudoApplication.secret}');
|
|
|
|
var response = await testClient.post(url.toString(), headers: {
|
|
|
|
'accept': 'application/json',
|
2019-05-03 07:29:59 +00:00
|
|
|
// 'authorization': 'Basic ' + base64Url.encode(ascii.encode(url.userInfo))
|
2018-12-15 08:39:04 +00:00
|
|
|
}, body: {
|
|
|
|
'grant_type': 'authorization_code',
|
|
|
|
'client_id': 'freddie mercury',
|
|
|
|
'redirect_uri': 'https://freddie.mercu.ry',
|
|
|
|
'code': 'ok'
|
|
|
|
});
|
|
|
|
|
|
|
|
print(response.body);
|
|
|
|
expect(
|
|
|
|
response,
|
|
|
|
allOf(
|
|
|
|
hasStatus(400),
|
|
|
|
isJson({
|
|
|
|
"error": "invalid_request",
|
|
|
|
"error_description": "Missing `code_verifier` parameter."
|
|
|
|
}),
|
|
|
|
));
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
class _Server extends AuthorizationServer<PseudoApplication, Map> {
|
|
|
|
@override
|
|
|
|
FutureOr<PseudoApplication> findClient(String clientId) {
|
|
|
|
return pseudoApplication;
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<bool> verifyClient(
|
|
|
|
PseudoApplication client, String clientSecret) async {
|
|
|
|
return client.secret == clientSecret;
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future requestAuthorizationCode(
|
|
|
|
PseudoApplication client,
|
|
|
|
String redirectUri,
|
|
|
|
Iterable<String> scopes,
|
|
|
|
String state,
|
|
|
|
RequestContext req,
|
2019-05-03 07:24:24 +00:00
|
|
|
ResponseContext res,
|
|
|
|
bool implicit) async {
|
2018-12-15 08:39:04 +00:00
|
|
|
req.container.make<Pkce>();
|
|
|
|
return {'code': 'ok'};
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<AuthorizationTokenResponse> exchangeAuthorizationCodeForToken(
|
2019-05-02 07:28:38 +00:00
|
|
|
PseudoApplication client,
|
2018-12-15 08:39:04 +00:00
|
|
|
String authCode,
|
|
|
|
String redirectUri,
|
|
|
|
RequestContext req,
|
|
|
|
ResponseContext res) async {
|
|
|
|
var codeVerifier = await getPkceCodeVerifier(req);
|
2019-05-02 07:28:38 +00:00
|
|
|
var pkce = Pkce('plain', 'hello');
|
2018-12-15 08:39:04 +00:00
|
|
|
pkce.validate(codeVerifier);
|
2019-05-02 07:28:38 +00:00
|
|
|
return AuthorizationTokenResponse('yes');
|
2018-12-15 08:39:04 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|