Published oauth2, cache, cors, auth_oauth2

This commit is contained in:
thomashii 2021-05-30 08:46:13 +08:00
parent e79826bad0
commit db49c82fc0
42 changed files with 488 additions and 431 deletions

View file

@ -0,0 +1,12 @@
Primary Authors
===============
* __[Thomas Hii](dukefirehawk.apps@gmail.com)__
Thomas is the current maintainer of the code base. He has refactored and migrated the
code base to support NNBD.
* __[Tobe O](thosakwe@gmail.com)__
Tobe has written much of the original code prior to NNBD migration. He has moved on and
is no longer involved with the project.

View file

@ -1,3 +1,9 @@
# 4.0.0
* Migrated to support Dart SDK 2.12.x NNBD
# 3.0.0
* Migrated to work with Dart SDK 2.12.x Non NNBD
# 2.1.0 # 2.1.0
* Angel 2 + Dart 2 update * Angel 2 + Dart 2 update
* Support for handling errors + rejections. * Support for handling errors + rejections.

View file

@ -1,8 +1,11 @@
# auth_oauth2 # angel3_auth_oauth2
[![version](https://img.shields.io/badge/pub-v4.0.0-brightgreen)](https://pub.dartlang.org/packages/angel3_auth_oauth2)
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
[![Gitter](https://img.shields.io/gitter/room/angel_dart/discussion)](https://gitter.im/angel_dart/discussion)
[![Pub](https://img.shields.io/pub/v/angel_auth_oauth2.svg)](https://pub.dartlang.org/packages/angel_auth_oauth2) [![License](https://img.shields.io/github/license/dukefirehawk/angel)](https://github.com/dukefirehawk/angel/tree/angel3/packages/auth_oauth2/LICENSE)
`package:angel_auth` strategy for OAuth2 login, i.e. Facebook or Github. `package:angel3_auth` strategy for OAuth2 login, i.e. Facebook or Github.
# Usage # Usage
First, create an options object: First, create an options object:
@ -77,7 +80,7 @@ Set up two routes:
In the case of the callback route, you may want to display an HTML page that closes In the case of the callback route, you may want to display an HTML page that closes
a popup window. In this case, use `confirmPopupAuthentication`, which is bundled with a popup window. In this case, use `confirmPopupAuthentication`, which is bundled with
`package:angel_auth`, as a `callback` function: `package:angel3_auth`, as a `callback` function:
```dart ```dart
configureServer(Angel app) async { configureServer(Angel app) async {

View file

@ -1,8 +1,8 @@
import 'dart:convert'; import 'dart:convert';
import 'package:angel_auth/angel_auth.dart'; import 'package:angel3_auth/angel3_auth.dart';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel3_framework/angel3_framework.dart';
import 'package:angel_framework/http.dart'; import 'package:angel3_framework/http.dart';
import 'package:angel_auth_oauth2/angel_auth_oauth2.dart'; import 'package:angel3_auth_oauth2/angel3_auth_oauth2.dart';
import 'package:http_parser/http_parser.dart'; import 'package:http_parser/http_parser.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
@ -63,7 +63,7 @@ void main() async {
(client, req, res) async { (client, req, res) async {
var response = await client.get(Uri.parse('https://api.github.com/user')); var response = await client.get(Uri.parse('https://api.github.com/user'));
var ghUser = json.decode(response.body); var ghUser = json.decode(response.body);
var id = ghUser['id'] as int; var id = ghUser['id'] as int?;
var matchingUsers = await mappedUserService.index({ var matchingUsers = await mappedUserService.index({
'query': {'github_id': id} 'query': {'github_id': id}
@ -85,7 +85,7 @@ void main() async {
}, },
// We have to pass this parser function when working with Github. // We have to pass this parser function when working with Github.
getParameters: parseParamsFromGithub, //getParameters: parseParamsFromGithub,
); );
// Mount some routes // Mount some routes
@ -98,7 +98,7 @@ void main() async {
// //
// Use `confirmPopupAuthentication`, which is bundled with // Use `confirmPopupAuthentication`, which is bundled with
// `package:angel_auth`. // `package:angel_auth`.
var user = req.container.make<User>(); var user = req.container!.make<User>()!;
res.write('Your user info: ${user.toJson()}\n\n'); res.write('Your user info: ${user.toJson()}\n\n');
res.write('Your JWT: $jwt'); res.write('Your JWT: $jwt');
await res.close(); await res.close();
@ -113,14 +113,14 @@ void main() async {
class User extends Model { class User extends Model {
@override @override
String id; String? id;
int githubId; int? githubId;
User({this.id, this.githubId}); User({this.id, this.githubId});
static User parse(Map map) => static User parse(Map map) =>
User(id: map['id'] as String, githubId: map['github_id'] as int); User(id: map['id'] as String?, githubId: map['github_id'] as int?);
static Map<String, dynamic> serialize(User user) => user.toJson(); static Map<String, dynamic> serialize(User user) => user.toJson();

View file

@ -1,8 +1,8 @@
library angel_auth_oauth2; library angel3_auth_oauth2;
import 'dart:async'; import 'dart:async';
import 'package:angel_auth/angel_auth.dart'; import 'package:angel3_auth/angel3_auth.dart';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel3_framework/angel3_framework.dart';
import 'package:http_parser/http_parser.dart'; import 'package:http_parser/http_parser.dart';
import 'package:oauth2/oauth2.dart' as oauth2; import 'package:oauth2/oauth2.dart' as oauth2;
@ -28,34 +28,34 @@ class OAuth2Strategy<User> implements AuthStrategy<User> {
final Uri tokenEndpoint; final Uri tokenEndpoint;
/// An optional callback used to parse the response from a server who does not follow the OAuth 2.0 spec. /// An optional callback used to parse the response from a server who does not follow the OAuth 2.0 spec.
final Map<String, dynamic> Function(MediaType, String) getParameters; final Map<String, dynamic> Function(MediaType?, String)? getParameters;
/// An optional delimiter used to send requests to server who does not follow the OAuth 2.0 spec. /// An optional delimiter used to send requests to server who does not follow the OAuth 2.0 spec.
final String delimiter; final String delimiter;
Uri _redirect; Uri? _redirect;
OAuth2Strategy(this.options, this.authorizationEndpoint, this.tokenEndpoint, OAuth2Strategy(this.options, this.authorizationEndpoint, this.tokenEndpoint,
this.verifier, this.onError, this.verifier, this.onError,
{this.getParameters, this.delimiter = ' '}); {this.getParameters, this.delimiter = ' '});
oauth2.AuthorizationCodeGrant _createGrant() => oauth2.AuthorizationCodeGrant _createGrant() => oauth2.AuthorizationCodeGrant(
new oauth2.AuthorizationCodeGrant(options.clientId, authorizationEndpoint, options.clientId, authorizationEndpoint, tokenEndpoint,
tokenEndpoint,
secret: options.clientSecret, secret: options.clientSecret,
delimiter: delimiter, delimiter: delimiter,
getParameters: getParameters); getParameters: getParameters);
@override @override
FutureOr<User> authenticate(RequestContext req, ResponseContext res, FutureOr<User?> authenticate(RequestContext req, ResponseContext res,
[AngelAuthOptions<User> options]) async { [AngelAuthOptions<User>? options]) async {
if (options != null) { if (options != null) {
var result = await authenticateCallback(req, res, options); var result = await authenticateCallback(req, res, options);
if (result is User) if (result is User) {
return result; return result;
else } else {
return null; return null;
} }
}
if (_redirect == null) { if (_redirect == null) {
var grant = _createGrant(); var grant = _createGrant();
@ -65,20 +65,20 @@ class OAuth2Strategy<User> implements AuthStrategy<User> {
); );
} }
res.redirect(_redirect); await res.redirect(_redirect);
return null; return null;
} }
/// The endpoint that is invoked by the third-party after successful authentication. /// The endpoint that is invoked by the third-party after successful authentication.
Future<dynamic> authenticateCallback(RequestContext req, ResponseContext res, Future<dynamic> authenticateCallback(RequestContext req, ResponseContext res,
[AngelAuthOptions options]) async { [AngelAuthOptions? options]) async {
var grant = _createGrant(); var grant = _createGrant();
grant.getAuthorizationUrl(this.options.redirectUri, grant.getAuthorizationUrl(this.options.redirectUri,
scopes: this.options.scopes); scopes: this.options.scopes);
try { try {
var client = var client =
await grant.handleAuthorizationResponse(req.uri.queryParameters); await grant.handleAuthorizationResponse(req.uri!.queryParameters);
return await verifier(client, req, res); return await verifier(client, req, res);
} on oauth2.AuthorizationException catch (e) { } on oauth2.AuthorizationException catch (e) {
return await onError(e, req, res); return await onError(e, req, res);

View file

@ -1,24 +1,14 @@
name: angel_auth_oauth2 name: angel3_auth_oauth2
version: 4.0.0
description: angel_auth strategy for OAuth2 login, i.e. Facebook, Github, etc. description: angel_auth strategy for OAuth2 login, i.e. Facebook, Github, etc.
version: 3.0.0 homepage: https://github.com/dukefirehawk/angel/tree/angel3/packages/auth_oauth2
#author: Tobe O <thosakwe@gmail.com>
publish_to: none
environment: environment:
sdk: ">=2.10.0 <3.0.0" sdk: '>=2.12.0 <3.0.0'
homepage: https://github.com/angel-dart/auth_oauth2.git
dependencies: dependencies:
angel_auth: angel3_auth: ^4.0.0
git: angel3_framework: ^4.0.0
url: https://github.com/dukefirehawk/angel.git
ref: sdk-2.12.x
path: packages/auth
angel_framework:
git:
url: https://github.com/dukefirehawk/angel.git
ref: sdk-2.12.x
path: packages/framework
http_parser: ^4.0.0 http_parser: ^4.0.0
oauth2: ^2.0.0 oauth2: ^2.0.0
dev_dependencies: dev_dependencies:
logging: ^1.0.0 logging: ^1.0.1
pedantic: ^1.0.0 pedantic: ^1.11.0

12
packages/cache/AUTHORS.md vendored Normal file
View file

@ -0,0 +1,12 @@
Primary Authors
===============
* __[Thomas Hii](dukefirehawk.apps@gmail.com)__
Thomas is the current maintainer of the code base. He has refactored and migrated the
code base to support NNBD.
* __[Tobe O](thosakwe@gmail.com)__
Tobe has written much of the original code prior to NNBD migration. He has moved on and
is no longer involved with the project.

View file

@ -1,3 +1,9 @@
# 4.0.0
* Migrated to support Dart SDK 2.12.x NNBD
# 3.0.0
* Migrated to work with Dart SDK 2.12.x Non NNBD
# 2.0.1 # 2.0.1
* Add `ignoreQueryAndFragment` to `ResponseCache`. * Add `ignoreQueryAndFragment` to `ResponseCache`.
* Rename `CacheService.ignoreQuery` to `ignoreParams`. * Rename `CacheService.ignoreQuery` to `ignoreParams`.

View file

@ -1,6 +1,9 @@
# cache # angel3_cache
[![Pub](https://img.shields.io/pub/v/angel_cache.svg)](https://pub.dartlang.org/packages/angel_cache) [![version](https://img.shields.io/badge/pub-v4.0.0-brightgreen)](https://pub.dartlang.org/packages/angel3_cache)
[![build status](https://travis-ci.org/angel-dart/cache.svg)](https://travis-ci.org/angel-dart/cache) [![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
[![Gitter](https://img.shields.io/gitter/room/angel_dart/discussion)](https://gitter.im/angel_dart/discussion)
[![License](https://img.shields.io/github/license/dukefirehawk/angel)](https://github.com/dukefirehawk/angel/tree/angel3/packages/cache/LICENSE)
Support for server-side caching in [Angel](https://angel-dart.github.io). Support for server-side caching in [Angel](https://angel-dart.github.io).
@ -17,13 +20,13 @@ This can improve the performance of sending objects that are complex to serializ
You can pass a [shouldCache] callback to determine which values should be cached. You can pass a [shouldCache] callback to determine which values should be cached.
```dart ```dart
main() async { void main() async {
var app = new Angel()..lazyParseBodies = true; var app = Angel()..lazyParseBodies = true;
app.use( app.use(
'/api/todos', '/api/todos',
new CacheService( CacheService(
database: new AnonymousService( database: AnonymousService(
index: ([params]) { index: ([params]) {
print('Fetched directly from the underlying service at ${new DateTime.now()}!'); print('Fetched directly from the underlying service at ${new DateTime.now()}!');
return ['foo', 'bar', 'baz']; return ['foo', 'bar', 'baz'];
@ -51,10 +54,10 @@ To initialize a simple cache:
```dart ```dart
Future configureServer(Angel app) async { Future configureServer(Angel app) async {
// Simple instance. // Simple instance.
var cache = new ResponseCache(); var cache = ResponseCache();
// You can also pass an invalidation timeout. // You can also pass an invalidation timeout.
var cache = new ResponseCache(timeout: const Duration(days: 2)); var cache = ResponseCache(timeout: const Duration(days: 2));
// Close the cache when the application closes. // Close the cache when the application closes.
app.shutdownHooks.add((_) => cache.close()); app.shutdownHooks.add((_) => cache.close());
@ -62,8 +65,8 @@ Future configureServer(Angel app) async {
// Use `patterns` to specify which resources should be cached. // Use `patterns` to specify which resources should be cached.
cache.patterns.addAll([ cache.patterns.addAll([
'robots.txt', 'robots.txt',
new RegExp(r'\.(png|jpg|gif|txt)$'), RegExp(r'\.(png|jpg|gif|txt)$'),
new Glob('public/**/*'), Glob('public/**/*'),
]); ]);
// REQUIRED: The middleware that serves cached responses // REQUIRED: The middleware that serves cached responses
@ -85,7 +88,7 @@ to ensure no arbitrary attacker can hack your cache:
Future configureServer(Angel app) async { Future configureServer(Angel app) async {
app.addRoute('PURGE', '*', (req, res) { app.addRoute('PURGE', '*', (req, res) {
if (req.ip != '127.0.0.1') if (req.ip != '127.0.0.1')
throw new AngelHttpException.forbidden(); throw AngelHttpException.forbidden();
return cache.purge(req.uri.path); return cache.purge(req.uri.path);
}); });
} }

View file

@ -1,6 +1,6 @@
import 'package:angel_cache/angel_cache.dart'; import 'package:angel3_cache/angel3_cache.dart';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel3_framework/angel3_framework.dart';
import 'package:angel_framework/http.dart'; import 'package:angel3_framework/http.dart';
main() async { main() async {
var app = Angel(); var app = Angel();
@ -13,7 +13,7 @@ main() async {
print( print(
'Fetched directly from the underlying service at ${new DateTime.now()}!'); 'Fetched directly from the underlying service at ${new DateTime.now()}!');
return ['foo', 'bar', 'baz']; return ['foo', 'bar', 'baz'];
}, read: (id, [params]) { }, read: (dynamic id, [params]) {
return {id: '$id at ${new DateTime.now()}'}; return {id: '$id at ${new DateTime.now()}'};
}), }),
), ),

View file

@ -1,6 +1,6 @@
import 'package:angel_cache/angel_cache.dart'; import 'package:angel3_cache/angel3_cache.dart';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel3_framework/angel3_framework.dart';
import 'package:angel_framework/http.dart'; import 'package:angel3_framework/http.dart';
import 'package:glob/glob.dart'; import 'package:glob/glob.dart';
main() async { main() async {
@ -23,8 +23,8 @@ main() async {
app.addRoute('PURGE', '*', (req, res) { app.addRoute('PURGE', '*', (req, res) {
if (req.ip != '127.0.0.1') throw AngelHttpException.forbidden(); if (req.ip != '127.0.0.1') throw AngelHttpException.forbidden();
cache.purge(req.uri.path); cache.purge(req.uri!.path);
print('Purged ${req.uri.path}'); print('Purged ${req.uri!.path}');
}); });
// The response finalizer that actually saves the content // The response finalizer that actually saves the content

View file

@ -1,6 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:io' show HttpDate; import 'dart:io' show HttpDate;
import 'package:angel_framework/angel_framework.dart'; import 'package:angel3_framework/angel3_framework.dart';
import 'package:pool/pool.dart'; import 'package:pool/pool.dart';
/// A flexible response cache for Angel. /// A flexible response cache for Angel.
@ -14,7 +14,7 @@ class ResponseCache {
final List<Pattern> patterns = []; final List<Pattern> patterns = [];
/// An optional timeout, after which a given response will be removed from the cache, and the contents refreshed. /// An optional timeout, after which a given response will be removed from the cache, and the contents refreshed.
final Duration timeout; final Duration? timeout;
final Map<String, _CachedResponse> _cache = {}; final Map<String, _CachedResponse> _cache = {};
final Map<String, Pool> _writeLocks = {}; final Map<String, Pool> _writeLocks = {};
@ -38,21 +38,21 @@ class ResponseCache {
Future<bool> ifModifiedSince(RequestContext req, ResponseContext res) async { Future<bool> ifModifiedSince(RequestContext req, ResponseContext res) async {
if (req.method != 'GET' && req.method != 'HEAD') return true; if (req.method != 'GET' && req.method != 'HEAD') return true;
if (req.headers.ifModifiedSince != null) { if (req.headers!.ifModifiedSince != null) {
var modifiedSince = req.headers.ifModifiedSince; var modifiedSince = req.headers!.ifModifiedSince;
// Check if there is a cache entry. // Check if there is a cache entry.
for (var pattern in patterns) { for (var pattern in patterns) {
if (pattern.allMatches(_getEffectivePath(req)).isNotEmpty && if (pattern.allMatches(_getEffectivePath(req)).isNotEmpty &&
_cache.containsKey(_getEffectivePath(req))) { _cache.containsKey(_getEffectivePath(req))) {
var response = _cache[_getEffectivePath(req)]; var response = _cache[_getEffectivePath(req)]!;
//print('timestamp ${response.timestamp} vs since ${modifiedSince}'); //print('timestamp ${response.timestamp} vs since ${modifiedSince}');
if (response.timestamp.compareTo(modifiedSince) <= 0) { if (response.timestamp.compareTo(modifiedSince!) <= 0) {
if (timeout != null) { if (timeout != null) {
// If the cache timeout has been met, don't send the cached response. // If the cache timeout has been met, don't send the cached response.
if (new DateTime.now().toUtc().difference(response.timestamp) >= if (new DateTime.now().toUtc().difference(response.timestamp) >=
timeout) return true; timeout!) return true;
} }
res.statusCode = 304; res.statusCode = 304;
@ -66,7 +66,7 @@ class ResponseCache {
} }
String _getEffectivePath(RequestContext req) => String _getEffectivePath(RequestContext req) =>
ignoreQueryAndFragment == true ? req.uri.path : req.uri.toString(); ignoreQueryAndFragment == true ? req.uri!.path : req.uri.toString();
/// Serves content from the cache, if applicable. /// Serves content from the cache, if applicable.
Future<bool> handleRequest(RequestContext req, ResponseContext res) async { Future<bool> handleRequest(RequestContext req, ResponseContext res) async {
@ -77,7 +77,7 @@ class ResponseCache {
// Check if there is a cache entry. // Check if there is a cache entry.
// //
// If `if-modified-since` is present, this check has already been performed. // If `if-modified-since` is present, this check has already been performed.
if (req.headers.ifModifiedSince == null) { if (req.headers!.ifModifiedSince == null) {
for (var pattern in patterns) { for (var pattern in patterns) {
if (pattern.allMatches(_getEffectivePath(req)).isNotEmpty) { if (pattern.allMatches(_getEffectivePath(req)).isNotEmpty) {
var now = new DateTime.now().toUtc(); var now = new DateTime.now().toUtc();
@ -87,10 +87,10 @@ class ResponseCache {
if (timeout != null) { if (timeout != null) {
// If the cache timeout has been met, don't send the cached response. // If the cache timeout has been met, don't send the cached response.
if (now.difference(response.timestamp) >= timeout) return true; if (now.difference(response!.timestamp) >= timeout!) return true;
} }
_setCachedHeaders(response.timestamp, req, res); _setCachedHeaders(response!.timestamp, req, res);
res res
..headers.addAll(response.headers) ..headers.addAll(response.headers)
..add(response.body) ..add(response.body)
@ -123,8 +123,8 @@ class ResponseCache {
if (timeout == null) return true; if (timeout == null) return true;
// Otherwise, don't invalidate unless the timeout has been exceeded. // Otherwise, don't invalidate unless the timeout has been exceeded.
var response = _cache[_getEffectivePath(req)]; var response = _cache[_getEffectivePath(req)]!;
if (now.difference(response.timestamp) < timeout) return true; if (now.difference(response.timestamp) < timeout!) return true;
// If the cache entry should be invalidated, then invalidate it. // If the cache entry should be invalidated, then invalidate it.
purge(_getEffectivePath(req)); purge(_getEffectivePath(req));
@ -135,7 +135,7 @@ class ResponseCache {
_writeLocks.putIfAbsent(_getEffectivePath(req), () => new Pool(1)); _writeLocks.putIfAbsent(_getEffectivePath(req), () => new Pool(1));
await writeLock.withResource(() { await writeLock.withResource(() {
_cache[_getEffectivePath(req)] = new _CachedResponse( _cache[_getEffectivePath(req)] = new _CachedResponse(
new Map.from(res.headers), res.buffer.toBytes(), now); new Map.from(res.headers), res.buffer!.toBytes(), now);
}); });
_setCachedHeaders(now, req, res); _setCachedHeaders(now, req, res);
@ -154,7 +154,7 @@ class ResponseCache {
..['last-modified'] = HttpDate.format(modified); ..['last-modified'] = HttpDate.format(modified);
if (timeout != null) { if (timeout != null) {
var expiry = new DateTime.now().add(timeout); var expiry = new DateTime.now().add(timeout!);
res.headers['expires'] = HttpDate.format(expiry); res.headers['expires'] = HttpDate.format(expiry);
} }
} }

View file

@ -1,7 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel3_framework/angel3_framework.dart';
import 'package:meta/meta.dart';
/// An Angel [Service] that caches data from another service. /// An Angel [Service] that caches data from another service.
/// ///
@ -21,22 +20,20 @@ class CacheService<Id, Data> extends Service<Id, Data> {
/// If you want to return a cached result more-often-than-not, you may want to enable this. /// If you want to return a cached result more-often-than-not, you may want to enable this.
final bool ignoreParams; final bool ignoreParams;
final Duration timeout; final Duration? timeout;
final Map<Id, _CachedItem<Data>> _cache = {}; final Map<Id, _CachedItem<Data>> _cache = {};
_CachedItem<List<Data>> _indexed; _CachedItem<List<Data>>? _indexed;
CacheService( CacheService(
{@required this.database, {required this.database,
@required this.cache, required this.cache,
this.ignoreParams: false, this.ignoreParams: false,
this.timeout}) { this.timeout}) {}
assert(database != null);
}
Future<T> _getCached<T>( Future<T> _getCached<T>(
Map<String, dynamic> params, Map<String, dynamic>? params,
_CachedItem get(), _CachedItem? get(),
FutureOr<T> getFresh(), FutureOr<T> getFresh(),
FutureOr<T> getCached(), FutureOr<T> getCached(),
FutureOr<T> save(T data, DateTime now)) async { FutureOr<T> save(T data, DateTime now)) async {
@ -46,7 +43,7 @@ class CacheService<Id, Data> extends Service<Id, Data> {
if (cached != null) { if (cached != null) {
// If the entry has expired, don't send from the cache // If the entry has expired, don't send from the cache
var expired = var expired =
timeout != null && now.difference(cached.timestamp) >= timeout; timeout != null && now.difference(cached.timestamp) >= timeout!;
if (timeout == null || !expired) { if (timeout == null || !expired) {
// Read from the cache if necessary // Read from the cache if necessary
@ -54,7 +51,7 @@ class CacheService<Id, Data> extends Service<Id, Data> {
(params != null && (params != null &&
cached.params != null && cached.params != null &&
const MapEquality().equals( const MapEquality().equals(
params['query'] as Map, cached.params['query'] as Map)); params['query'] as Map?, cached.params['query'] as Map?));
if (queryEqual) { if (queryEqual) {
return await getCached(); return await getCached();
} }
@ -69,7 +66,7 @@ class CacheService<Id, Data> extends Service<Id, Data> {
} }
@override @override
Future<List<Data>> index([Map<String, dynamic> params]) { Future<List<Data>> index([Map<String, dynamic>? params]) {
return _getCached( return _getCached(
params, params,
() => _indexed, () => _indexed,
@ -83,7 +80,7 @@ class CacheService<Id, Data> extends Service<Id, Data> {
} }
@override @override
Future<Data> read(Id id, [Map<String, dynamic> params]) async { Future<Data> read(Id id, [Map<String, dynamic>? params]) async {
return _getCached<Data>( return _getCached<Data>(
params, params,
() => _cache[id], () => _cache[id],
@ -97,27 +94,27 @@ class CacheService<Id, Data> extends Service<Id, Data> {
} }
@override @override
Future<Data> create(data, [Map<String, dynamic> params]) { Future<Data> create(data, [Map<String, dynamic>? params]) {
_indexed = null; _indexed = null;
return database.create(data, params); return database.create(data, params);
} }
@override @override
Future<Data> modify(Id id, Data data, [Map<String, dynamic> params]) { Future<Data> modify(Id id, Data data, [Map<String, dynamic>? params]) {
_indexed = null; _indexed = null;
_cache.remove(id); _cache.remove(id);
return database.modify(id, data, params); return database.modify(id, data, params);
} }
@override @override
Future<Data> update(Id id, Data data, [Map<String, dynamic> params]) { Future<Data> update(Id id, Data data, [Map<String, dynamic>? params]) {
_indexed = null; _indexed = null;
_cache.remove(id); _cache.remove(id);
return database.modify(id, data, params); return database.modify(id, data, params);
} }
@override @override
Future<Data> remove(Id id, [Map<String, dynamic> params]) { Future<Data> remove(Id id, [Map<String, dynamic>? params]) {
_indexed = null; _indexed = null;
_cache.remove(id); _cache.remove(id);
return database.remove(id, params); return database.remove(id, params);
@ -127,7 +124,7 @@ class CacheService<Id, Data> extends Service<Id, Data> {
class _CachedItem<Data> { class _CachedItem<Data> {
final params; final params;
final DateTime timestamp; final DateTime timestamp;
final Data data; final Data? data;
_CachedItem(this.params, this.timestamp, [this.data]); _CachedItem(this.params, this.timestamp, [this.data]);

View file

@ -1,5 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel3_framework/angel3_framework.dart';
/// A middleware that enables the caching of response serialization. /// A middleware that enables the caching of response serialization.
/// ///
@ -7,8 +7,8 @@ import 'package:angel_framework/angel_framework.dart';
/// ///
/// You can pass a [shouldCache] callback to determine which values should be cached. /// You can pass a [shouldCache] callback to determine which values should be cached.
RequestHandler cacheSerializationResults( RequestHandler cacheSerializationResults(
{Duration timeout, {Duration? timeout,
FutureOr<bool> Function(RequestContext, ResponseContext, Object) FutureOr<bool> Function(RequestContext, ResponseContext, Object)?
shouldCache}) { shouldCache}) {
return (RequestContext req, ResponseContext res) async { return (RequestContext req, ResponseContext res) async {
var oldSerializer = res.serializer; var oldSerializer = res.serializer;
@ -20,7 +20,7 @@ RequestHandler cacheSerializationResults(
// return cache.putIfAbsent(value, () => oldSerializer(value)); // return cache.putIfAbsent(value, () => oldSerializer(value));
//} //}
return oldSerializer(value); return oldSerializer!(value);
}; };
return true; return true;

View file

@ -1,26 +1,17 @@
name: angel_cache name: angel3_cache
version: 3.0.0 version: 4.0.0
homepage: https://github.com/angel-dart/cache description: Support for server-side caching in Angel3 Framework
description: Support for server-side caching in Angel. homepage: https://github.com/dukefirehawk/angel/tree/angel3/packages/cache
author: Tobe O <thosakwe@gmail.com>
publish_to: none
environment: environment:
sdk: ">=2.10.0 <3.0.0" sdk: '>=2.12.0 <3.0.0'
dependencies: dependencies:
angel_framework: angel3_framework: ^4.0.0
git: collection: ^1.15.0
url: https://github.com/dukefirehawk/angel.git meta: ^1.4.0
ref: sdk-2.12.x pool: ^1.5.0
path: packages/framework
collection: ^1.0.0
meta: ^1.0.0
pool: ^1.0.0
dev_dependencies: dev_dependencies:
angel_test: angel3_test: ^4.0.0
git: glob: ^2.0.1
url: https://github.com/dukefirehawk/angel.git http: ^0.13.3
ref: sdk-2.12.x test: ^1.17.5
path: packages/test pedantic: ^1.11.0
glob: ^2.0.0
http: ^0.13.0
test: ^1.16.5

View file

@ -1,17 +1,17 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:angel_cache/angel_cache.dart'; import 'package:angel3_cache/angel3_cache.dart';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel3_framework/angel3_framework.dart';
import 'package:angel_test/angel_test.dart'; import 'package:angel3_test/angel3_test.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:glob/glob.dart'; import 'package:glob/glob.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
main() async { main() async {
group('no timeout', () { group('no timeout', () {
TestClient client; late TestClient client;
DateTime lastModified; late DateTime lastModified;
http.Response response1, response2; late http.Response response1, response2;
setUp(() async { setUp(() async {
var app = new Angel(); var app = new Angel();
@ -29,8 +29,8 @@ main() async {
}); });
app.addRoute('PURGE', '*', (req, res) { app.addRoute('PURGE', '*', (req, res) {
cache.purge(req.uri.path); cache.purge(req.uri!.path);
print('Purged ${req.uri.path}'); print('Purged ${req.uri!.path}');
}); });
app.responseFinalizers.add(cache.responseFinalizer); app.responseFinalizers.add(cache.responseFinalizer);
@ -38,14 +38,15 @@ main() async {
var oldHandler = app.errorHandler; var oldHandler = app.errorHandler;
app.errorHandler = (e, req, res) { app.errorHandler = (e, req, res) {
if (e.error == null) return oldHandler(e, req, res); if (e.error == null) return oldHandler(e, req, res);
return Zone.current.handleUncaughtError(e.error, e.stackTrace); return Zone.current
.handleUncaughtError(e.error as Object, e.stackTrace!);
}; };
client = await connectTo(app); client = await connectTo(app);
response1 = await client.get(Uri.parse('/date.txt')); response1 = await client.get(Uri.parse('/date.txt'));
response2 = await client.get(Uri.parse('/date.txt')); response2 = await client.get(Uri.parse('/date.txt'));
print(response2.headers); print(response2.headers);
lastModified = HttpDate.parse(response2.headers['last-modified']); lastModified = HttpDate.parse(response2.headers['last-modified']!);
print('Response 1 status: ${response1.statusCode}'); print('Response 1 status: ${response1.statusCode}');
print('Response 2 status: ${response2.statusCode}'); print('Response 2 status: ${response2.statusCode}');
print('Response 1 body: ${response1.body}'); print('Response 1 body: ${response1.body}');

12
packages/cors/AUTHORS.md Normal file
View file

@ -0,0 +1,12 @@
Primary Authors
===============
* __[Thomas Hii](dukefirehawk.apps@gmail.com)__
Thomas is the current maintainer of the code base. He has refactored and migrated the
code base to support NNBD.
* __[Tobe O](thosakwe@gmail.com)__
Tobe has written much of the original code prior to NNBD migration. He has moved on and
is no longer involved with the project.

View file

@ -1,2 +1,8 @@
# 4.0.0
* Migrated to support Dart SDK 2.12.x NNBD
# 3.0.0
* Migrated to work with Dart SDK 2.12.x Non NNBD
# 2.0.0 # 2.0.0
* Updates for Dart 2 and Angel 2. * Updates for Dart 2 and Angel 2.

View file

@ -1,6 +1,10 @@
# cors # angel3_cors
[![Pub](https://img.shields.io/pub/v/angel_cors.svg)](https://pub.dartlang.org/packages/angel_cors) [![version](https://img.shields.io/badge/pub-v4.0.0-brightgreen)](https://pub.dartlang.org/packages/angel3_cors)
[![build status](https://travis-ci.org/angel-dart/cors.svg)](https://travis-ci.org/angel-dart/cors) [![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
[![Gitter](https://img.shields.io/gitter/room/angel_dart/discussion)](https://gitter.im/angel_dart/discussion)
[![License](https://img.shields.io/github/license/dukefirehawk/angel)](https://github.com/dukefirehawk/angel/tree/angel3/packages/cors/LICENSE)
Angel CORS middleware. Angel CORS middleware.
Port of [the original Express CORS middleware](https://github.com/expressjs/cors). Port of [the original Express CORS middleware](https://github.com/expressjs/cors).

View file

@ -1,6 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:angel_cors/angel_cors.dart'; import 'package:angel3_cors/angel3_cors.dart';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel3_framework/angel3_framework.dart';
Future configureServer(Angel app) async { Future configureServer(Angel app) async {
// The default options will allow CORS for any request. // The default options will allow CORS for any request.
@ -43,7 +43,7 @@ Future configureServer(Angel app) async {
app.fallback(dynamicCors((req, res) { app.fallback(dynamicCors((req, res) {
return CorsOptions( return CorsOptions(
origin: [ origin: [
req.headers.value('origin') ?? 'https://pub.dartlang.org', req.headers!.value('origin') ?? 'https://pub.dartlang.org',
RegExp(r'\.com$'), RegExp(r'\.com$'),
], ],
); );

View file

@ -1,16 +1,16 @@
/// Angel CORS middleware. /// Angel CORS middleware.
library angel_cors; library angel3_cors;
import 'dart:async'; import 'dart:async';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel3_framework/angel3_framework.dart';
import 'src/cors_options.dart'; import 'src/cors_options.dart';
export 'src/cors_options.dart'; export 'src/cors_options.dart';
/// Determines if a request origin is CORS-able. /// Determines if a request origin is CORS-able.
typedef bool _CorsFilter(String origin); typedef _CorsFilter = bool Function(String origin);
bool _isOriginAllowed(String origin, [allowedOrigin]) { bool _isOriginAllowed(String? origin, [allowedOrigin]) {
allowedOrigin ??= []; allowedOrigin ??= [];
if (allowedOrigin is Iterable) { if (allowedOrigin is Iterable) {
return allowedOrigin.any((x) => _isOriginAllowed(origin, x)); return allowedOrigin.any((x) => _isOriginAllowed(origin, x));
@ -38,12 +38,12 @@ Future<bool> Function(RequestContext, ResponseContext) dynamicCors(
/// Applies the given [CorsOptions]. /// Applies the given [CorsOptions].
Future<bool> Function(RequestContext, ResponseContext) cors( Future<bool> Function(RequestContext, ResponseContext) cors(
[CorsOptions options]) { [CorsOptions? options]) {
options ??= CorsOptions(); options ??= CorsOptions();
return (req, res) async { return (req, res) async {
// access-control-allow-credentials // access-control-allow-credentials
if (options.credentials == true) { if (options!.credentials == true) {
res.headers['access-control-allow-credentials'] = 'true'; res.headers['access-control-allow-credentials'] = 'true';
} }
@ -51,9 +51,9 @@ Future<bool> Function(RequestContext, ResponseContext) cors(
if (req.method == 'OPTIONS' && options.allowedHeaders.isNotEmpty) { if (req.method == 'OPTIONS' && options.allowedHeaders.isNotEmpty) {
res.headers['access-control-allow-headers'] = res.headers['access-control-allow-headers'] =
options.allowedHeaders.join(','); options.allowedHeaders.join(',');
} else if (req.headers['access-control-request-headers'] != null) { } else if (req.headers!['access-control-request-headers'] != null) {
res.headers['access-control-allow-headers'] = res.headers['access-control-allow-headers'] =
req.headers.value('access-control-request-headers'); req.headers!.value('access-control-request-headers')!;
} }
// access-control-expose-headers // access-control-expose-headers
@ -80,11 +80,11 @@ Future<bool> Function(RequestContext, ResponseContext) cors(
..headers['access-control-allow-origin'] = options.origin as String ..headers['access-control-allow-origin'] = options.origin as String
..headers['vary'] = 'origin'; ..headers['vary'] = 'origin';
} else { } else {
bool isAllowed = var isAllowed =
_isOriginAllowed(req.headers.value('origin'), options.origin); _isOriginAllowed(req.headers!.value('origin'), options.origin);
res.headers['access-control-allow-origin'] = res.headers['access-control-allow-origin'] =
isAllowed ? req.headers.value('origin') : false.toString(); isAllowed ? req.headers!.value('origin')! : false.toString();
if (isAllowed) { if (isAllowed) {
res.headers['vary'] = 'origin'; res.headers['vary'] = 'origin';
@ -92,7 +92,8 @@ Future<bool> Function(RequestContext, ResponseContext) cors(
} }
if (req.method != 'OPTIONS') return true; if (req.method != 'OPTIONS') return true;
res.statusCode = options.successStatus ?? 204; //res.statusCode = options.successStatus ?? 204;
res.statusCode = options.successStatus;
res.contentLength = 0; res.contentLength = 0;
await res.close(); await res.close();
return options.preflightContinue; return options.preflightContinue;

View file

@ -22,7 +22,7 @@ class CorsOptions {
/// Configures the **Access-Control-Max-Age** CORS header. Set to an integer to pass the header, otherwise it is omitted. /// Configures the **Access-Control-Max-Age** CORS header. Set to an integer to pass the header, otherwise it is omitted.
/// ///
/// Default: `null` /// Default: `null`
final int maxAge; final int? maxAge;
/// The status code to be sent on successful `OPTIONS` requests, if [preflightContinue] is `false`. /// The status code to be sent on successful `OPTIONS` requests, if [preflightContinue] is `false`.
final int successStatus; final int successStatus;
@ -50,7 +50,7 @@ class CorsOptions {
CorsOptions( CorsOptions(
{Iterable<String> allowedHeaders = const [], {Iterable<String> allowedHeaders = const [],
this.credentials, this.credentials = false,
this.maxAge, this.maxAge,
Iterable<String> methods = const [ Iterable<String> methods = const [
'GET', 'GET',
@ -64,10 +64,10 @@ class CorsOptions {
this.successStatus = 204, this.successStatus = 204,
this.preflightContinue = false, this.preflightContinue = false,
Iterable<String> exposedHeaders = const []}) { Iterable<String> exposedHeaders = const []}) {
if (allowedHeaders != null) this.allowedHeaders.addAll(allowedHeaders); this.allowedHeaders.addAll(allowedHeaders);
if (methods != null) this.methods.addAll(methods); this.methods.addAll(methods);
if (exposedHeaders != null) this.exposedHeaders.addAll(exposedHeaders); this.exposedHeaders.addAll(exposedHeaders);
} }
} }

View file

@ -1,23 +1,13 @@
author: Tobe O <thosakwe@gmail.com> name: angel3_cors
version: 4.0.0
description: Angel CORS middleware. Port of expressjs/cors to the Angel framework. description: Angel CORS middleware. Port of expressjs/cors to the Angel framework.
homepage: https://github.com/dukefirehawk/angel/tree/angel3/packages/cors
environment: environment:
sdk: ">=2.10.0 <3.0.0" sdk: '>=2.12.0 <3.0.0'
homepage: https://github.com/angel-dart/cors.git
name: angel_cors
version: 3.0.0
publish_to: none
dependencies: dependencies:
angel_framework: angel3_framework: ^4.0.0
git:
url: https://github.com/dukefirehawk/angel.git
ref: sdk-2.12.x
path: packages/framework
dev_dependencies: dev_dependencies:
angel_test: angel3_test: ^4.0.0
git: http: ^0.13.3
url: https://github.com/dukefirehawk/angel.git
ref: sdk-2.12.x
path: packages/test
http: ^0.13.0
pedantic: ^1.11.0 pedantic: ^1.11.0
test: ^1.16.5 test: ^1.17.5

View file

@ -1,13 +1,13 @@
import 'package:angel_framework/angel_framework.dart'; import 'package:angel3_framework/angel3_framework.dart';
import 'package:angel_framework/http.dart'; import 'package:angel3_framework/http.dart';
import 'package:angel_cors/angel_cors.dart'; import 'package:angel3_cors/angel3_cors.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:test/test.dart'; import 'package:test/test.dart';
main() { void main() {
Angel app; Angel? app;
AngelHttp server; late AngelHttp server;
http.Client client; http.Client? client;
setUp(() async { setUp(() async {
app = Angel() app = Angel()
@ -24,7 +24,7 @@ main() {
cors(CorsOptions( cors(CorsOptions(
origin: ['foo.bar', 'baz.quux'], origin: ['foo.bar', 'baz.quux'],
)), )),
(req, res) => req.headers['origin'] (req, res) => req.headers!['origin']
])) ]))
..get( ..get(
'/origins', '/origins',
@ -32,7 +32,7 @@ main() {
cors(CorsOptions( cors(CorsOptions(
origin: 'foo.bar', origin: 'foo.bar',
)), )),
(req, res) => req.headers['origin'] (req, res) => req.headers!['origin']
])) ]))
..get( ..get(
'/originr', '/originr',
@ -40,7 +40,7 @@ main() {
cors(CorsOptions( cors(CorsOptions(
origin: RegExp(r'^foo\.[^x]+$'), origin: RegExp(r'^foo\.[^x]+$'),
)), )),
(req, res) => req.headers['origin'] (req, res) => req.headers!['origin']
])) ]))
..get( ..get(
'/originp', '/originp',
@ -48,7 +48,7 @@ main() {
cors(CorsOptions( cors(CorsOptions(
origin: (String s) => s.endsWith('.bar'), origin: (String s) => s.endsWith('.bar'),
)), )),
(req, res) => req.headers['origin'] (req, res) => req.headers!['origin']
])) ]))
..options('/status', cors(CorsOptions(successStatus: 418))) ..options('/status', cors(CorsOptions(successStatus: 418)))
..fallback(cors(CorsOptions())) ..fallback(cors(CorsOptions()))
@ -57,7 +57,7 @@ main() {
}) })
..fallback((req, res) => throw AngelHttpException.notFound()); ..fallback((req, res) => throw AngelHttpException.notFound());
server = AngelHttp(app); server = AngelHttp(app!);
await server.startServer('127.0.0.1', 0); await server.startServer('127.0.0.1', 0);
client = http.Client(); client = http.Client();
}); });
@ -70,93 +70,93 @@ main() {
test('status 204 by default', () async { test('status 204 by default', () async {
var rq = http.Request('OPTIONS', server.uri.replace(path: '/max_age')); var rq = http.Request('OPTIONS', server.uri.replace(path: '/max_age'));
var response = await client.send(rq).then(http.Response.fromStream); var response = await client!.send(rq).then(http.Response.fromStream);
expect(response.statusCode, 204); expect(response.statusCode, 204);
}); });
test('content length 0 by default', () async { test('content length 0 by default', () async {
var rq = http.Request('OPTIONS', server.uri.replace(path: '/max_age')); var rq = http.Request('OPTIONS', server.uri.replace(path: '/max_age'));
var response = await client.send(rq).then(http.Response.fromStream); var response = await client!.send(rq).then(http.Response.fromStream);
expect(response.contentLength, 0); expect(response.contentLength, 0);
}); });
test('custom successStatus', () async { test('custom successStatus', () async {
var rq = http.Request('OPTIONS', server.uri.replace(path: '/status')); var rq = http.Request('OPTIONS', server.uri.replace(path: '/status'));
var response = await client.send(rq).then(http.Response.fromStream); var response = await client!.send(rq).then(http.Response.fromStream);
expect(response.statusCode, 418); expect(response.statusCode, 418);
}); });
test('max age', () async { test('max age', () async {
var rq = http.Request('OPTIONS', server.uri.replace(path: '/max_age')); var rq = http.Request('OPTIONS', server.uri.replace(path: '/max_age'));
var response = await client.send(rq).then(http.Response.fromStream); var response = await client!.send(rq).then(http.Response.fromStream);
expect(response.headers['access-control-max-age'], '250'); expect(response.headers['access-control-max-age'], '250');
}); });
test('methods', () async { test('methods', () async {
var rq = http.Request('OPTIONS', server.uri.replace(path: '/methods')); var rq = http.Request('OPTIONS', server.uri.replace(path: '/methods'));
var response = await client.send(rq).then(http.Response.fromStream); var response = await client!.send(rq).then(http.Response.fromStream);
expect(response.headers['access-control-allow-methods'], 'GET,POST'); expect(response.headers['access-control-allow-methods'], 'GET,POST');
}); });
test('dynamicCors.credentials', () async { test('dynamicCors.credentials', () async {
var rq = var rq =
http.Request('OPTIONS', server.uri.replace(path: '/credentials_d')); http.Request('OPTIONS', server.uri.replace(path: '/credentials_d'));
var response = await client.send(rq).then(http.Response.fromStream); var response = await client!.send(rq).then(http.Response.fromStream);
expect(response.headers['access-control-allow-credentials'], 'true'); expect(response.headers['access-control-allow-credentials'], 'true');
}); });
test('credentials', () async { test('credentials', () async {
var rq = http.Request('OPTIONS', server.uri.replace(path: '/credentials')); var rq = http.Request('OPTIONS', server.uri.replace(path: '/credentials'));
var response = await client.send(rq).then(http.Response.fromStream); var response = await client!.send(rq).then(http.Response.fromStream);
expect(response.headers['access-control-allow-credentials'], 'true'); expect(response.headers['access-control-allow-credentials'], 'true');
}); });
test('exposed headers', () async { test('exposed headers', () async {
var rq = http.Request('OPTIONS', server.uri.replace(path: '/headers')); var rq = http.Request('OPTIONS', server.uri.replace(path: '/headers'));
var response = await client.send(rq).then(http.Response.fromStream); var response = await client!.send(rq).then(http.Response.fromStream);
expect(response.headers['access-control-expose-headers'], 'x-foo,x-bar'); expect(response.headers['access-control-expose-headers'], 'x-foo,x-bar');
}); });
test('invalid origin', () async { test('invalid origin', () async {
var response = await client.get(server.uri.replace(path: '/originl'), var response = await client!.get(server.uri.replace(path: '/originl'),
headers: {'origin': 'foreign'}); headers: {'origin': 'foreign'});
expect(response.headers['access-control-allow-origin'], 'false'); expect(response.headers['access-control-allow-origin'], 'false');
}); });
test('list origin', () async { test('list origin', () async {
var response = await client.get(server.uri.replace(path: '/originl'), var response = await client!.get(server.uri.replace(path: '/originl'),
headers: {'origin': 'foo.bar'}); headers: {'origin': 'foo.bar'});
expect(response.headers['access-control-allow-origin'], 'foo.bar'); expect(response.headers['access-control-allow-origin'], 'foo.bar');
expect(response.headers['vary'], 'origin'); expect(response.headers['vary'], 'origin');
response = await client.get(server.uri.replace(path: '/originl'), response = await client!.get(server.uri.replace(path: '/originl'),
headers: {'origin': 'baz.quux'}); headers: {'origin': 'baz.quux'});
expect(response.headers['access-control-allow-origin'], 'baz.quux'); expect(response.headers['access-control-allow-origin'], 'baz.quux');
expect(response.headers['vary'], 'origin'); expect(response.headers['vary'], 'origin');
}); });
test('string origin', () async { test('string origin', () async {
var response = await client.get(server.uri.replace(path: '/origins'), var response = await client!.get(server.uri.replace(path: '/origins'),
headers: {'origin': 'foo.bar'}); headers: {'origin': 'foo.bar'});
expect(response.headers['access-control-allow-origin'], 'foo.bar'); expect(response.headers['access-control-allow-origin'], 'foo.bar');
expect(response.headers['vary'], 'origin'); expect(response.headers['vary'], 'origin');
}); });
test('regex origin', () async { test('regex origin', () async {
var response = await client.get(server.uri.replace(path: '/originr'), var response = await client!.get(server.uri.replace(path: '/originr'),
headers: {'origin': 'foo.bar'}); headers: {'origin': 'foo.bar'});
expect(response.headers['access-control-allow-origin'], 'foo.bar'); expect(response.headers['access-control-allow-origin'], 'foo.bar');
expect(response.headers['vary'], 'origin'); expect(response.headers['vary'], 'origin');
}); });
test('predicate origin', () async { test('predicate origin', () async {
var response = await client.get(server.uri.replace(path: '/originp'), var response = await client!.get(server.uri.replace(path: '/originp'),
headers: {'origin': 'foo.bar'}); headers: {'origin': 'foo.bar'});
expect(response.headers['access-control-allow-origin'], 'foo.bar'); expect(response.headers['access-control-allow-origin'], 'foo.bar');
expect(response.headers['vary'], 'origin'); expect(response.headers['vary'], 'origin');
}); });
test('POST works', () async { test('POST works', () async {
final response = await client.post(server.uri); final response = await client!.post(server.uri);
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
print('Response: ${response.body}'); print('Response: ${response.body}');
print('Headers: ${response.headers}'); print('Headers: ${response.headers}');
@ -164,7 +164,7 @@ main() {
}); });
test('mirror headers', () async { test('mirror headers', () async {
final response = await client final response = await client!
.post(server.uri, headers: {'access-control-request-headers': 'foo'}); .post(server.uri, headers: {'access-control-request-headers': 'foo'});
expect(response.statusCode, equals(200)); expect(response.statusCode, equals(200));
print('Response: ${response.body}'); print('Response: ${response.body}');

View file

@ -0,0 +1,12 @@
Primary Authors
===============
* __[Thomas Hii](dukefirehawk.apps@gmail.com)__
Thomas is the current maintainer of the code base. He has refactored and migrated the
code base to support NNBD.
* __[Tobe O](thosakwe@gmail.com)__
Tobe has written much of the original code prior to NNBD migration. He has moved on and
is no longer involved with the project.

View file

@ -1,3 +1,9 @@
# 4.0.0
* Migrated to support Dart SDK 2.12.x NNBD
# 3.0.0
* Migrated to work with Dart SDK 2.12.x Non NNBD
# 2.3.0 # 2.3.0
* Remove `implicitGrant`, and inline it into `requestAuthorizationCode`. * Remove `implicitGrant`, and inline it into `requestAuthorizationCode`.

View file

@ -1,6 +1,9 @@
# oauth2 # angel3_oauth2
[![Pub](https://img.shields.io/pub/v/angel_oauth2.svg)](https://pub.dartlang.org/packages/angel_oauth2) [![version](https://img.shields.io/badge/pub-v4.0.0-brightgreen)](https://pub.dartlang.org/packages/angel3_oauth2)
[![build status](https://travis-ci.org/angel-dart/oauth2.svg)](https://travis-ci.org/angel-dart/oauth2) [![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
[![Gitter](https://img.shields.io/gitter/room/angel_dart/discussion)](https://gitter.im/angel_dart/discussion)
[![License](https://img.shields.io/github/license/dukefirehawk/angel)](https://github.com/dukefirehawk/angel/tree/angel3/packages/oauth2/LICENSE)
A class containing handlers that can be used within A class containing handlers that can be used within
[Angel](https://angel-dart.github.io/) to build a spec-compliant [Angel](https://angel-dart.github.io/) to build a spec-compliant
@ -16,8 +19,8 @@ In your `pubspec.yaml`:
```yaml ```yaml
dependencies: dependencies:
angel_framework: ^2.0.0-alpha angel3_framework: ^4.0.0
angel_oauth2: ^2.0.0 angel3_oauth2: ^4.0.0
``` ```
# Usage # Usage
@ -28,7 +31,7 @@ Your server needs to have definitions of at least two types:
Define a server class as such: Define a server class as such:
```dart ```dart
import 'package:angel_oauth2/angel_oauth2.dart' as oauth2; import 'package:angel3_oauth2/angel3_oauth2.dart' as oauth2;
class MyServer extends oauth2.AuthorizationServer<Client, User> {} class MyServer extends oauth2.AuthorizationServer<Client, User> {}
``` ```

View file

@ -3,7 +3,7 @@ import 'dart:async';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel_framework/angel_framework.dart';
import 'package:angel_oauth2/angel_oauth2.dart'; import 'package:angel_oauth2/angel_oauth2.dart';
main() async { void main() async {
var app = Angel(); var app = Angel();
var oauth2 = _ExampleAuthorizationServer(); var oauth2 = _ExampleAuthorizationServer();
var _rgxBearer = RegExp(r'^[Bb]earer ([^\n\s]+)$'); var _rgxBearer = RegExp(r'^[Bb]earer ([^\n\s]+)$');
@ -17,7 +17,7 @@ main() async {
// Assume that all other requests must be authenticated... // Assume that all other requests must be authenticated...
app.fallback((req, res) { app.fallback((req, res) {
var authToken = var authToken =
req.headers.value('authorization')?.replaceAll(_rgxBearer, '')?.trim(); req.headers!.value('authorization')?.replaceAll(_rgxBearer, '').trim();
if (authToken == null) { if (authToken == null) {
throw AngelHttpException.forbidden(); throw AngelHttpException.forbidden();
@ -38,13 +38,13 @@ class User {}
class _ExampleAuthorizationServer class _ExampleAuthorizationServer
extends AuthorizationServer<ThirdPartyApp, User> { extends AuthorizationServer<ThirdPartyApp, User> {
@override @override
FutureOr<ThirdPartyApp> findClient(String clientId) { FutureOr<ThirdPartyApp> findClient(String? clientId) {
// TODO: Add your code to find the app associated with a client ID. // TODO: Add your code to find the app associated with a client ID.
throw UnimplementedError(); throw UnimplementedError();
} }
@override @override
FutureOr<bool> verifyClient(ThirdPartyApp client, String clientSecret) { FutureOr<bool> verifyClient(ThirdPartyApp client, String? clientSecret) {
// TODO: Add your code to verify a client secret, if given one. // TODO: Add your code to verify a client secret, if given one.
throw UnimplementedError(); throw UnimplementedError();
} }
@ -52,7 +52,7 @@ class _ExampleAuthorizationServer
@override @override
FutureOr requestAuthorizationCode( FutureOr requestAuthorizationCode(
ThirdPartyApp client, ThirdPartyApp client,
String redirectUri, String? redirectUri,
Iterable<String> scopes, Iterable<String> scopes,
String state, String state,
RequestContext req, RequestContext req,
@ -64,9 +64,9 @@ class _ExampleAuthorizationServer
@override @override
FutureOr<AuthorizationTokenResponse> exchangeAuthorizationCodeForToken( FutureOr<AuthorizationTokenResponse> exchangeAuthorizationCodeForToken(
ThirdPartyApp client, ThirdPartyApp? client,
String authCode, String? authCode,
String redirectUri, String? redirectUri,
RequestContext req, RequestContext req,
ResponseContext res) { ResponseContext res) {
// TODO: Here, you'll convert the auth code into a full-fledged token. // TODO: Here, you'll convert the auth code into a full-fledged token.

View file

@ -1,11 +1,11 @@
import 'package:angel_http_exception/angel_http_exception.dart'; import 'package:angel3_http_exception/angel3_http_exception.dart';
/// An Angel-friendly wrapper around OAuth2 [ErrorResponse] instances. /// An Angel-friendly wrapper around OAuth2 [ErrorResponse] instances.
class AuthorizationException extends AngelHttpException { class AuthorizationException extends AngelHttpException {
final ErrorResponse errorResponse; final ErrorResponse errorResponse;
AuthorizationException(this.errorResponse, AuthorizationException(this.errorResponse,
{StackTrace stackTrace, int statusCode, error}) {StackTrace? stackTrace, int? statusCode, error})
: super(error ?? errorResponse, : super(error ?? errorResponse,
stackTrace: stackTrace, message: '', statusCode: statusCode ?? 400); stackTrace: stackTrace, message: '', statusCode: statusCode ?? 400);
@ -16,8 +16,9 @@ class AuthorizationException extends AngelHttpException {
'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;
} }
@ -78,10 +79,10 @@ class ErrorResponse {
final String description; final String description;
/// An optional [Uri] directing users to more information about the error. /// An optional [Uri] directing users to more information about the error.
final Uri uri; final Uri? uri;
/// The exact value received from the client, if a "state" parameter was present in the client authorization request. /// The exact value received from the client, if a "state" parameter was present in the client authorization request.
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});

View file

@ -9,7 +9,7 @@ class Pkce {
final String codeChallengeMethod; final String codeChallengeMethod;
/// The proof key that is used to secure public clients. /// The proof key that is used to secure public clients.
final String codeChallenge; final String? codeChallenge;
Pkce(this.codeChallengeMethod, this.codeChallenge) { Pkce(this.codeChallengeMethod, this.codeChallenge) {
assert(codeChallengeMethod == 'plain' || codeChallengeMethod == 's256', assert(codeChallengeMethod == 'plain' || codeChallengeMethod == 's256',
@ -17,7 +17,7 @@ class Pkce {
} }
/// Attempts to parse a [codeChallenge] and [codeChallengeMethod] from a [Map]. /// Attempts to parse a [codeChallenge] and [codeChallengeMethod] from a [Map].
factory Pkce.fromJson(Map data, {String state, Uri uri}) { factory Pkce.fromJson(Map data, {String? state, Uri? uri}) {
var codeChallenge = data['code_challenge']?.toString(); var codeChallenge = data['code_challenge']?.toString();
var codeChallengeMethod = var codeChallengeMethod =
data['code_challenge_method']?.toString() ?? 'plain'; data['code_challenge_method']?.toString() ?? 'plain';
@ -44,7 +44,7 @@ class Pkce {
bool get isS256 => codeChallengeMethod == 's256'; bool get isS256 => codeChallengeMethod == 's256';
/// Determines if a given [codeVerifier] is valid. /// Determines if a given [codeVerifier] is valid.
void validate(String codeVerifier, {String state, Uri uri}) { void validate(String codeVerifier, {String? state, Uri? uri}) {
String foreignChallenge; String foreignChallenge;
if (isS256) { if (isS256) {
@ -57,7 +57,7 @@ class Pkce {
if (foreignChallenge != codeChallenge) { if (foreignChallenge != codeChallenge) {
throw AuthorizationException( throw AuthorizationException(
ErrorResponse(ErrorResponse.invalidGrant, ErrorResponse(ErrorResponse.invalidGrant,
"The given `code_verifier` parameter is invalid.", state, 'The given `code_verifier` parameter is invalid.', state,
uri: uri), uri: uri),
); );
} }

View file

@ -4,13 +4,13 @@ class AuthorizationTokenResponse {
final String accessToken; final String accessToken;
/// An optional key that can be used to refresh the [accessToken] past its expiration. /// An optional key that can be used to refresh the [accessToken] past its expiration.
final String refreshToken; final String? refreshToken;
/// An optional, but recommended integer that signifies the time left until the [accessToken] expires. /// An optional, but recommended integer that signifies the time left until the [accessToken] expires.
final int expiresIn; final int? expiresIn;
/// Optional, if identical to the scope requested by the client; otherwise, required. /// Optional, if identical to the scope requested by the client; otherwise, required.
final Iterable<String> scope; final Iterable<String>? scope;
const AuthorizationTokenResponse(this.accessToken, const AuthorizationTokenResponse(this.accessToken,
{this.refreshToken, this.expiresIn, this.scope}); {this.refreshToken, this.expiresIn, this.scope});
@ -19,7 +19,7 @@ class AuthorizationTokenResponse {
var map = <String, dynamic>{'access_token': accessToken}; var map = <String, dynamic>{'access_token': accessToken};
if (refreshToken?.isNotEmpty == true) map['refresh_token'] = refreshToken; if (refreshToken?.isNotEmpty == true) map['refresh_token'] = refreshToken;
if (expiresIn != null) map['expires_in'] = expiresIn; if (expiresIn != null) map['expires_in'] = expiresIn;
if (scope != null) map['scope'] = scope.toList(); if (scope != null) map['scope'] = scope!.toList();
return map; return map;
} }
} }
@ -40,12 +40,12 @@ class DeviceCodeResponse {
/// OPTIONAL. A verification URI that includes the [userCode] (or /// OPTIONAL. A verification URI that includes the [userCode] (or
/// other information with the same function as the [userCode]), /// other information with the same function as the [userCode]),
/// designed for non-textual transmission. /// designed for non-textual transmission.
final Uri verificationUriComplete; final Uri? verificationUriComplete;
/// OPTIONAL. The minimum amount of time in seconds that the client /// OPTIONAL. The minimum amount of time in seconds that the client
/// SHOULD wait between polling requests to the token endpoint. If no /// SHOULD wait between polling requests to the token endpoint. If no
/// value is provided, clients MUST use 5 as the default. /// value is provided, clients MUST use 5 as the default.
final int interval; final int? interval;
/// The lifetime, in *seconds* of the [deviceCode] and [userCode]. /// The lifetime, in *seconds* of the [deviceCode] and [userCode].
final int expiresIn; final int expiresIn;

View file

@ -1,16 +1,16 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel3_framework/angel3_framework.dart';
import 'exception.dart'; import 'exception.dart';
import 'pkce.dart'; import 'pkce.dart';
import 'response.dart'; import 'response.dart';
import 'token_type.dart'; import 'token_type.dart';
/// A request handler that performs an arbitrary authorization token grant. /// A request handler that performs an arbitrary authorization token grant.
typedef FutureOr<AuthorizationTokenResponse> ExtensionGrant( typedef ExtensionGrant = FutureOr<AuthorizationTokenResponse> Function(
RequestContext req, ResponseContext res); RequestContext req, ResponseContext res);
Future<String> _getParam(RequestContext req, String name, String state, Future<String?> _getParam(RequestContext req, String name, String state,
{bool body = false, bool throwIfEmpty = true}) async { {bool body = false, bool throwIfEmpty = true}) async {
Map<String, dynamic> data; Map<String, dynamic> data;
@ -46,7 +46,7 @@ Future<Iterable<String>> _getScopes(RequestContext req,
data = req.queryParameters; data = req.queryParameters;
} }
return data['scope']?.toString()?.split(' ') ?? []; return data['scope']?.toString().split(' ') ?? [];
} }
/// An OAuth2 authorization server, which issues access tokens to third parties. /// An OAuth2 authorization server, which issues access tokens to third parties.
@ -60,14 +60,14 @@ abstract class AuthorizationServer<Client, User> {
Map<String, ExtensionGrant> get extensionGrants => {}; Map<String, ExtensionGrant> get extensionGrants => {};
/// 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);
/// Verify that a [client] is the one identified by the [clientSecret]. /// Verify that a [client] is the one identified by the [clientSecret].
FutureOr<bool> verifyClient(Client client, String clientSecret); FutureOr<bool> verifyClient(Client client, String? clientSecret);
/// Retrieves the PKCE `code_verifier` parameter from a [RequestContext], or throws. /// Retrieves the PKCE `code_verifier` parameter from a [RequestContext], or throws.
Future<String> getPkceCodeVerifier(RequestContext req, Future<String> getPkceCodeVerifier(RequestContext req,
{bool body = true, String state, Uri uri}) async { {bool body = true, String? state, Uri? uri}) async {
var data = body var data = body
? await req.parseBody().then((_) => req.bodyAsMap) ? await req.parseBody().then((_) => req.bodyAsMap)
: req.queryParameters; : req.queryParameters;
@ -75,14 +75,14 @@ abstract class AuthorizationServer<Client, User> {
if (codeVerifier == null) { if (codeVerifier == null) {
throw AuthorizationException(ErrorResponse(ErrorResponse.invalidRequest, throw AuthorizationException(ErrorResponse(ErrorResponse.invalidRequest,
"Missing `code_verifier` parameter.", state, 'Missing `code_verifier` parameter.', state,
uri: uri)); uri: uri));
} else if (codeVerifier is! String) { } else if (codeVerifier is! String) {
throw AuthorizationException(ErrorResponse(ErrorResponse.invalidRequest, throw AuthorizationException(ErrorResponse(ErrorResponse.invalidRequest,
"The `code_verifier` parameter must be a string.", state, 'The `code_verifier` parameter must be a string.', state,
uri: uri)); uri: uri));
} else { } else {
return codeVerifier as String; return codeVerifier;
} }
} }
@ -95,7 +95,7 @@ abstract class AuthorizationServer<Client, User> {
/// the same. /// the same.
FutureOr<void> requestAuthorizationCode( FutureOr<void> requestAuthorizationCode(
Client client, Client client,
String redirectUri, String? redirectUri,
Iterable<String> scopes, Iterable<String> scopes,
String state, String state,
RequestContext req, RequestContext req,
@ -113,16 +113,16 @@ abstract class AuthorizationServer<Client, User> {
/// Exchanges an authorization code for an authorization token. /// Exchanges an authorization code for an authorization token.
FutureOr<AuthorizationTokenResponse> exchangeAuthorizationCodeForToken( FutureOr<AuthorizationTokenResponse> exchangeAuthorizationCodeForToken(
Client client, Client? client,
String authCode, String? authCode,
String redirectUri, String? redirectUri,
RequestContext req, RequestContext req,
ResponseContext res) { ResponseContext res) {
throw AuthorizationException( throw AuthorizationException(
ErrorResponse( ErrorResponse(
ErrorResponse.unsupportedResponseType, ErrorResponse.unsupportedResponseType,
'Authorization code grants are not supported.', 'Authorization code grants are not supported.',
req.uri.queryParameters['state'] ?? '', req.uri!.queryParameters['state'] ?? '',
), ),
statusCode: 400, statusCode: 400,
); );
@ -130,8 +130,8 @@ abstract class AuthorizationServer<Client, User> {
/// Refresh an authorization token. /// Refresh an authorization token.
FutureOr<AuthorizationTokenResponse> refreshAuthorizationToken( FutureOr<AuthorizationTokenResponse> refreshAuthorizationToken(
Client client, Client? client,
String refreshToken, String? refreshToken,
Iterable<String> scopes, Iterable<String> scopes,
RequestContext req, RequestContext req,
ResponseContext res) async { ResponseContext res) async {
@ -148,9 +148,9 @@ abstract class AuthorizationServer<Client, User> {
/// Issue an authorization token to a user after authenticating them via [username] and [password]. /// Issue an authorization token to a user after authenticating them via [username] and [password].
FutureOr<AuthorizationTokenResponse> resourceOwnerPasswordCredentialsGrant( FutureOr<AuthorizationTokenResponse> resourceOwnerPasswordCredentialsGrant(
Client client, Client? client,
String username, String? username,
String password, String? password,
Iterable<String> scopes, Iterable<String> scopes,
RequestContext req, RequestContext req,
ResponseContext res) async { ResponseContext res) async {
@ -167,7 +167,7 @@ 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.
FutureOr<AuthorizationTokenResponse> clientCredentialsGrant( FutureOr<AuthorizationTokenResponse> clientCredentialsGrant(
Client client, RequestContext req, ResponseContext res) async { Client? client, RequestContext req, ResponseContext res) async {
var body = await req.parseBody().then((_) => req.bodyAsMap); var body = await req.parseBody().then((_) => req.bodyAsMap);
throw AuthorizationException( throw AuthorizationException(
ErrorResponse( ErrorResponse(
@ -196,7 +196,7 @@ abstract class AuthorizationServer<Client, User> {
/// Produces an authorization token from a given device code. /// Produces an authorization token from a given device code.
FutureOr<AuthorizationTokenResponse> exchangeDeviceCodeForToken( FutureOr<AuthorizationTokenResponse> exchangeDeviceCodeForToken(
Client client, Client client,
String deviceCode, String? deviceCode,
String state, String state,
RequestContext req, RequestContext req,
ResponseContext res) async { ResponseContext res) async {
@ -213,7 +213,7 @@ abstract class AuthorizationServer<Client, User> {
/// Returns the [Uri] that a client can be redirected to in the case of an implicit grant. /// Returns the [Uri] that a client can be redirected to in the case of an implicit grant.
Uri completeImplicitGrant(AuthorizationTokenResponse token, Uri redirectUri, Uri completeImplicitGrant(AuthorizationTokenResponse token, Uri redirectUri,
{String state}) { {String? state}) {
var queryParameters = <String, String>{}; var queryParameters = <String, String>{};
queryParameters.addAll({ queryParameters.addAll({
@ -223,17 +223,18 @@ abstract class AuthorizationServer<Client, User> {
if (state != null) queryParameters['state'] = state; if (state != null) queryParameters['state'] = state;
if (token.expiresIn != null) if (token.expiresIn != null) {
queryParameters['expires_in'] = token.expiresIn.toString(); queryParameters['expires_in'] = token.expiresIn.toString();
}
if (token.scope != null) queryParameters['scope'] = token.scope.join(' '); if (token.scope != null) queryParameters['scope'] = token.scope!.join(' ');
var fragment = var fragment =
queryParameters.keys.fold<StringBuffer>(StringBuffer(), (buf, k) { queryParameters.keys.fold<StringBuffer>(StringBuffer(), (buf, k) {
if (buf.isNotEmpty) buf.write('&'); if (buf.isNotEmpty) buf.write('&');
return buf return buf
..write( ..write(
'$k=' + Uri.encodeComponent(queryParameters[k]), '$k=' + Uri.encodeComponent(queryParameters[k]!),
); );
}).toString(); }).toString();
@ -244,14 +245,14 @@ abstract class AuthorizationServer<Client, User> {
/// of grant the client is requesting. /// of grant the client is requesting.
Future<void> authorizationEndpoint( Future<void> authorizationEndpoint(
RequestContext req, ResponseContext res) async { RequestContext req, ResponseContext res) async {
String state = ''; var state = '';
try { try {
var query = req.queryParameters; var query = req.queryParameters;
state = query['state']?.toString() ?? ''; state = query['state']?.toString() ?? '';
var responseType = await _getParam(req, 'response_type', state); var responseType = await _getParam(req, 'response_type', state);
req.container.registerLazySingleton<Pkce>((_) { req.container!.registerLazySingleton<Pkce>((_) {
return Pkce.fromJson(req.queryParameters, state: state); return Pkce.fromJson(req.queryParameters, state: state);
}); });
@ -260,7 +261,7 @@ abstract class AuthorizationServer<Client, User> {
var clientId = await _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)!;
if (client == null) { if (client == null) {
throw AuthorizationException(ErrorResponse( throw AuthorizationException(ErrorResponse(
@ -309,16 +310,16 @@ abstract class AuthorizationServer<Client, User> {
/// A request handler that either exchanges authorization codes for authorization tokens, /// A request handler that either exchanges authorization codes for authorization tokens,
/// or refreshes authorization tokens. /// or refreshes authorization tokens.
Future tokenEndpoint(RequestContext req, ResponseContext res) async { Future tokenEndpoint(RequestContext req, ResponseContext res) async {
String state = ''; var state = '';
Client client; Client? client;
try { try {
AuthorizationTokenResponse response; AuthorizationTokenResponse? response;
var body = await req.parseBody().then((_) => req.bodyAsMap); var body = await req.parseBody().then((_) => req.bodyAsMap);
state = body['state']?.toString() ?? ''; state = body['state']?.toString() ?? '';
req.container.registerLazySingleton<Pkce>((_) { req.container!.registerLazySingleton<Pkce>((_) {
return Pkce.fromJson(req.bodyAsMap, state: state); return Pkce.fromJson(req.bodyAsMap, state: state);
}); });
@ -328,11 +329,11 @@ abstract class AuthorizationServer<Client, User> {
if (grantType != 'urn:ietf:params:oauth:grant-type:device_code' && if (grantType != 'urn:ietf:params:oauth:grant-type:device_code' &&
grantType != null) { grantType != null) {
var match = var match =
_rgxBasic.firstMatch(req.headers.value('authorization') ?? ''); _rgxBasic.firstMatch(req.headers!.value('authorization') ?? '');
if (match != null) { if (match != null) {
match = _rgxBasicAuth match = _rgxBasicAuth
.firstMatch(String.fromCharCodes(base64Url.decode(match[1]))); .firstMatch(String.fromCharCodes(base64Url.decode(match[1]!)));
} }
if (match == null) { if (match == null) {
@ -402,7 +403,7 @@ abstract class AuthorizationServer<Client, User> {
); );
} }
} else if (extensionGrants.containsKey(grantType)) { } else if (extensionGrants.containsKey(grantType)) {
response = await extensionGrants[grantType](req, res); response = await extensionGrants[grantType!]!(req, res);
} else if (grantType == null) { } else if (grantType == null) {
// This is a device code grant. // This is a device code grant.
var clientId = await _getParam(req, 'client_id', state, body: true); var clientId = await _getParam(req, 'client_id', state, body: true);

View file

@ -1,23 +1,19 @@
name: angel_oauth2 name: angel3_oauth2
author: Tobe O <thosakwe@gmail.com> version: 4.0.0
description: A class containing handlers that can be used within Angel to build a spec-compliant OAuth 2.0 server. 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/dukefirehawk/angel/tree/angel3/packages/oauth2
version: 2.3.0
environment: environment:
sdk: ">=2.10.0 <2.12.0" sdk: '>=2.12.0 <3.0.0'
dependencies: dependencies:
angel_framework: #^2.0.0-rc.0 angel3_framework: ^4.0.0
path: ../framework angel3_http_exception: ^3.0.0
angel_http_exception: #^1.0.0 crypto: ^3.0.1
path: ../http_exception collection: ^1.15.0-nullsafety.4
crypto: ^2.0.0
dev_dependencies: dev_dependencies:
angel_validate: #^2.0.0-alpha angel3_validate: ^4.0.0
path: ../validate angel3_test: ^4.0.0
angel_test: #^2.0.0-alpha logging: ^1.0.1
path: ../test oauth2: ^2.0.0
logging: pedantic: ^1.11.0
oauth2: ^1.0.0 test: ^1.17.5
pedantic: ^1.0.0 uuid: ^3.0.4
test: ^1.15.7
uuid: ^2.0.0

View file

@ -1,25 +1,25 @@
import 'dart:async'; import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'dart:convert'; import 'dart:convert';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel3_framework/angel3_framework.dart';
import 'package:angel_framework/http.dart'; import 'package:angel3_framework/http.dart';
import 'package:angel_oauth2/angel_oauth2.dart'; import 'package:angel3_oauth2/angel3_oauth2.dart';
import 'package:angel_test/angel_test.dart'; import 'package:angel3_test/angel3_test.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;
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import 'common.dart'; import 'common.dart';
main() { void main() {
Angel app; Angel app;
Uri authorizationEndpoint, tokenEndpoint, redirectUri; late Uri authorizationEndpoint, tokenEndpoint, redirectUri;
TestClient testClient; late TestClient testClient;
setUp(() async { setUp(() async {
app = Angel(); app = Angel();
app.configuration['properties'] = app.configuration; app.configuration['properties'] = app.configuration;
app.container.registerSingleton(AuthCodes()); app.container!.registerSingleton(AuthCodes());
var server = _Server(); var server = _Server();
@ -62,7 +62,7 @@ main() {
test('show authorization form', () async { test('show authorization form', () async {
var grant = createGrant(); var grant = createGrant();
var url = grant.getAuthorizationUrl(redirectUri, state: 'hello'); var url = grant.getAuthorizationUrl(redirectUri, state: 'hello');
var response = await testClient.client.get(url); var response = await testClient.client!.get(url);
print('Body: ${response.body}'); print('Body: ${response.body}');
expect( expect(
response.body, response.body,
@ -73,7 +73,7 @@ main() {
test('preserves state', () async { test('preserves state', () async {
var grant = createGrant(); var grant = createGrant();
var url = grant.getAuthorizationUrl(redirectUri, state: 'goodbye'); var url = grant.getAuthorizationUrl(redirectUri, state: 'goodbye');
var response = await testClient.client.get(url); var response = await testClient.client!.get(url);
print('Body: ${response.body}'); print('Body: ${response.body}');
expect(json.decode(response.body)['state'], 'goodbye'); expect(json.decode(response.body)['state'], 'goodbye');
}); });
@ -81,7 +81,7 @@ main() {
test('sends auth code', () async { test('sends auth code', () async {
var grant = createGrant(); var grant = createGrant();
var url = grant.getAuthorizationUrl(redirectUri); var url = grant.getAuthorizationUrl(redirectUri);
var response = await testClient.client.get(url); var response = await testClient.client!.get(url);
print('Body: ${response.body}'); print('Body: ${response.body}');
expect( expect(
json.decode(response.body), json.decode(response.body),
@ -95,7 +95,7 @@ main() {
test('exchange code for token', () async { test('exchange code for token', () async {
var grant = createGrant(); var grant = createGrant();
var url = grant.getAuthorizationUrl(redirectUri); var url = grant.getAuthorizationUrl(redirectUri);
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'].toString(); var authCode = json.decode(response.body)['code'].toString();
@ -106,7 +106,7 @@ main() {
test('can send refresh token', () async { test('can send refresh token', () async {
var grant = createGrant(); var grant = createGrant();
var url = grant.getAuthorizationUrl(redirectUri, state: 'can_refresh'); var url = grant.getAuthorizationUrl(redirectUri, state: 'can_refresh');
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'].toString(); var authCode = json.decode(response.body)['code'].toString();
@ -122,20 +122,20 @@ class _Server extends AuthorizationServer<PseudoApplication, Map> {
final Uuid _uuid = Uuid(); final Uuid _uuid = Uuid();
@override @override
FutureOr<PseudoApplication> findClient(String clientId) { FutureOr<PseudoApplication>? findClient(String? clientId) {
return clientId == pseudoApplication.id ? pseudoApplication : null; return clientId == pseudoApplication.id ? pseudoApplication : null;
} }
@override @override
Future<bool> verifyClient( Future<bool> verifyClient(
PseudoApplication client, String clientSecret) async { PseudoApplication client, String? clientSecret) async {
return client.secret == clientSecret; return client.secret == clientSecret;
} }
@override @override
Future requestAuthorizationCode( Future requestAuthorizationCode(
PseudoApplication client, PseudoApplication client,
String redirectUri, String? redirectUri,
Iterable<String> scopes, Iterable<String> scopes,
String state, String state,
RequestContext req, RequestContext req,
@ -147,28 +147,29 @@ class _Server extends AuthorizationServer<PseudoApplication, Map> {
client, redirectUri, scopes, state, req, res, implicit); client, redirectUri, scopes, state, req, res, implicit);
} }
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();
var authCodes = req.container.make<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';
var result = {'code': authCode}; var result = {'code': authCode};
if (state?.isNotEmpty == true) result['state'] = state; if (state.isNotEmpty == true) result['state'] = state;
return result; return result;
} }
@override @override
Future<AuthorizationTokenResponse> exchangeAuthorizationCodeForToken( Future<AuthorizationTokenResponse> exchangeAuthorizationCodeForToken(
PseudoApplication client, PseudoApplication? client,
String authCode, String? authCode,
String redirectUri, String? redirectUri,
RequestContext req, RequestContext req,
ResponseContext res) async { ResponseContext res) async {
var authCodes = req.container.make<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 AuthorizationTokenResponse('${authCode}_access', return AuthorizationTokenResponse('${authCode}_access',
refreshToken: refreshToken); refreshToken: refreshToken);
@ -179,7 +180,7 @@ class AuthCodes extends MapBase<String, String> with MapMixin<String, String> {
var inner = <String, String>{}; var inner = <String, String>{};
@override @override
String operator [](Object key) => inner[key]; String? operator [](Object? key) => inner[key as String];
@override @override
void operator []=(String key, String value) => inner[key] = value; void operator []=(String key, String value) => inner[key] = value;
@ -191,5 +192,5 @@ class AuthCodes extends MapBase<String, String> with MapMixin<String, String> {
Iterable<String> get keys => inner.keys; Iterable<String> get keys => inner.keys;
@override @override
String remove(Object key) => inner.remove(key); String? remove(Object? key) => inner.remove(key);
} }

View file

@ -1,13 +1,13 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel3_framework/angel3_framework.dart';
import 'package:angel_test/angel_test.dart'; import 'package:angel3_test/angel3_test.dart';
import 'package:angel_oauth2/angel_oauth2.dart'; import 'package:angel3_oauth2/angel3_oauth2.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'common.dart'; import 'common.dart';
void main() { void main() {
TestClient client; late TestClient client;
setUp(() async { setUp(() async {
var app = Angel(); var app = Angel();
@ -30,7 +30,7 @@ void main() {
test('authenticate via client credentials', () async { test('authenticate via client credentials', () async {
var response = await client.post( var response = await client.post(
'/oauth2/token', Uri.parse('oauth2/token'),
headers: { headers: {
'Authorization': 'Basic ' + base64Url.encode('foo:bar'.codeUnits), 'Authorization': 'Basic ' + base64Url.encode('foo:bar'.codeUnits),
}, },
@ -58,7 +58,7 @@ void main() {
test('force correct id', () async { test('force correct id', () async {
var response = await client.post( var response = await client.post(
'/oauth2/token', Uri.parse('/oauth2/token'),
headers: { headers: {
'Authorization': 'Basic ' + base64Url.encode('fooa:bar'.codeUnits), 'Authorization': 'Basic ' + base64Url.encode('fooa:bar'.codeUnits),
}, },
@ -73,7 +73,7 @@ void main() {
test('force correct secret', () async { test('force correct secret', () async {
var response = await client.post( var response = await client.post(
'/oauth2/token', Uri.parse('/oauth2/token'),
headers: { headers: {
'Authorization': 'Basic ' + base64Url.encode('foo:bara'.codeUnits), 'Authorization': 'Basic ' + base64Url.encode('foo:bara'.codeUnits),
}, },
@ -90,19 +90,21 @@ void main() {
class _AuthorizationServer class _AuthorizationServer
extends AuthorizationServer<PseudoApplication, PseudoUser> { extends AuthorizationServer<PseudoApplication, PseudoUser> {
@override @override
PseudoApplication findClient(String clientId) { PseudoApplication? findClient(String? clientId) {
return clientId == pseudoApplication.id ? pseudoApplication : null; return clientId == pseudoApplication.id ? pseudoApplication : null;
} }
@override @override
Future<bool> verifyClient( Future<bool> verifyClient(
PseudoApplication client, String clientSecret) async { PseudoApplication client, String? clientSecret) async {
return client.secret == clientSecret; return client.secret == clientSecret;
} }
@override @override
Future<AuthorizationTokenResponse> clientCredentialsGrant( Future<AuthorizationTokenResponse> clientCredentialsGrant(
PseudoApplication client, RequestContext req, ResponseContext res) async { PseudoApplication? client,
RequestContext req,
ResponseContext res) async {
return AuthorizationTokenResponse('foo'); return AuthorizationTokenResponse('foo');
} }
} }

View file

@ -14,7 +14,7 @@ const List<PseudoUser> pseudoUsers = [
]; ];
class PseudoUser { class PseudoUser {
final String username, password; final String? username, password;
const PseudoUser({this.username, this.password}); const PseudoUser({this.username, this.password});
} }

View file

@ -1,14 +1,14 @@
import 'dart:async'; import 'dart:async';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel3_framework/angel3_framework.dart';
import 'package:angel_test/angel_test.dart'; import 'package:angel3_test/angel3_test.dart';
import 'package:angel_oauth2/angel_oauth2.dart'; import 'package:angel3_oauth2/angel3_oauth2.dart';
import 'package:angel_validate/angel_validate.dart'; import 'package:angel3_validate/angel3_validate.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'common.dart'; import 'common.dart';
main() { void main() {
TestClient client; late TestClient client;
setUp(() async { setUp(() async {
var app = Angel(); var app = Angel();
@ -38,7 +38,7 @@ main() {
group('get initial code', () { group('get initial code', () {
test('invalid client id', () async { test('invalid client id', () async {
var response = await client.post('/oauth2/token', body: { var response = await client.post(Uri.parse('/oauth2/token'), body: {
'client_id': 'barr', 'client_id': 'barr',
}); });
print(response.body); print(response.body);
@ -46,7 +46,7 @@ main() {
}); });
test('valid client id, no scopes', () async { test('valid client id, no scopes', () async {
var response = await client.post('/oauth2/token', body: { var response = await client.post(Uri.parse('/oauth2/token'), body: {
'client_id': 'foo', 'client_id': 'foo',
}); });
print(response.body); print(response.body);
@ -55,16 +55,16 @@ main() {
allOf( allOf(
hasStatus(200), hasStatus(200),
isJson({ isJson({
"device_code": "foo", 'device_code': 'foo',
"user_code": "bar", 'user_code': 'bar',
"verification_uri": "https://regiostech.com?scopes", 'verification_uri': 'https://regiostech.com?scopes',
"expires_in": 3600 'expires_in': 3600
}), }),
)); ));
}); });
test('valid client id, with scopes', () async { test('valid client id, with scopes', () async {
var response = await client.post('/oauth2/token', body: { var response = await client.post(Uri.parse('/oauth2/token'), body: {
'client_id': 'foo', 'client_id': 'foo',
'scope': 'bar baz quux', 'scope': 'bar baz quux',
}); });
@ -74,11 +74,11 @@ main() {
allOf( allOf(
hasStatus(200), hasStatus(200),
isJson({ isJson({
"device_code": "foo", 'device_code': 'foo',
"user_code": "bar", 'user_code': 'bar',
"verification_uri": Uri.parse("https://regiostech.com").replace( 'verification_uri': Uri.parse('https://regiostech.com').replace(
queryParameters: {'scopes': 'bar,baz,quux'}).toString(), queryParameters: {'scopes': 'bar,baz,quux'}).toString(),
"expires_in": 3600 'expires_in': 3600
}), }),
)); ));
}); });
@ -86,7 +86,7 @@ main() {
group('get token', () { group('get token', () {
test('valid device code + timing', () async { test('valid device code + timing', () async {
var response = await client.post('/oauth2/token', body: { var response = await client.post(Uri.parse('/oauth2/token'), body: {
'grant_type': 'urn:ietf:params:oauth:grant-type:device_code', 'grant_type': 'urn:ietf:params:oauth:grant-type:device_code',
'client_id': 'foo', 'client_id': 'foo',
'device_code': 'bar', 'device_code': 'bar',
@ -97,7 +97,7 @@ main() {
response, response,
allOf( allOf(
hasStatus(200), hasStatus(200),
isJson({"token_type": "bearer", "access_token": "foo"}), isJson({'token_type': 'bearer', 'access_token': 'foo'}),
)); ));
}); });
@ -108,7 +108,7 @@ main() {
// The logic for throwing errors and turning them into responses // The logic for throwing errors and turning them into responses
// has already been tested. // has already been tested.
test('failure', () async { test('failure', () async {
var response = await client.post('/oauth2/token', body: { var response = await client.post(Uri.parse('/oauth2/token'), body: {
'grant_type': 'urn:ietf:params:oauth:grant-type:device_code', 'grant_type': 'urn:ietf:params:oauth:grant-type:device_code',
'client_id': 'foo', 'client_id': 'foo',
'device_code': 'brute', 'device_code': 'brute',
@ -120,9 +120,9 @@ main() {
allOf( allOf(
hasStatus(400), hasStatus(400),
isJson({ isJson({
"error": "slow_down", 'error': 'slow_down',
"error_description": 'error_description':
"Ho, brother! Ho, whoa, whoa, whoa now! You got too much dip on your chip!" 'Ho, brother! Ho, whoa, whoa, whoa now! You got too much dip on your chip!'
}), }),
)); ));
}); });
@ -132,13 +132,13 @@ main() {
class _AuthorizationServer class _AuthorizationServer
extends AuthorizationServer<PseudoApplication, PseudoUser> { extends AuthorizationServer<PseudoApplication, PseudoUser> {
@override @override
PseudoApplication findClient(String clientId) { PseudoApplication? findClient(String? clientId) {
return clientId == pseudoApplication.id ? pseudoApplication : null; return clientId == pseudoApplication.id ? pseudoApplication : null;
} }
@override @override
Future<bool> verifyClient( Future<bool> verifyClient(
PseudoApplication client, String clientSecret) async { PseudoApplication client, String? clientSecret) async {
return client.secret == clientSecret; return client.secret == clientSecret;
} }
@ -156,14 +156,14 @@ class _AuthorizationServer
@override @override
FutureOr<AuthorizationTokenResponse> exchangeDeviceCodeForToken( FutureOr<AuthorizationTokenResponse> exchangeDeviceCodeForToken(
PseudoApplication client, PseudoApplication client,
String deviceCode, String? deviceCode,
String state, String state,
RequestContext req, RequestContext req,
ResponseContext res) { ResponseContext res) {
if (deviceCode == 'brute') { if (deviceCode == 'brute') {
throw AuthorizationException(ErrorResponse( throw AuthorizationException(ErrorResponse(
ErrorResponse.slowDown, ErrorResponse.slowDown,
"Ho, brother! Ho, whoa, whoa, whoa now! You got too much dip on your chip!", 'Ho, brother! Ho, whoa, whoa, whoa now! You got too much dip on your chip!',
state)); state));
} }

View file

@ -1,13 +1,13 @@
import 'dart:async'; import 'dart:async';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel3_framework/angel3_framework.dart';
import 'package:angel_test/angel_test.dart'; import 'package:angel3_test/angel3_test.dart';
import 'package:angel_oauth2/angel_oauth2.dart'; import 'package:angel3_oauth2/angel3_oauth2.dart';
import 'package:angel_validate/angel_validate.dart'; import 'package:angel3_validate/angel3_validate.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'common.dart'; import 'common.dart';
main() { void main() {
TestClient client; late TestClient client;
setUp(() async { setUp(() async {
var app = Angel(); var app = Angel();
@ -30,7 +30,8 @@ main() {
test('authenticate via implicit grant', () async { test('authenticate via implicit grant', () async {
var response = await client.get( var response = await client.get(
'/oauth2/authorize?response_type=token&client_id=foo&redirect_uri=http://foo.com&state=bar', Uri.parse(
'/oauth2/authorize?response_type=token&client_id=foo&redirect_uri=http://foo.com&state=bar'),
); );
print('Headers: ${response.headers}'); print('Headers: ${response.headers}');
@ -47,27 +48,27 @@ main() {
class _AuthorizationServer class _AuthorizationServer
extends AuthorizationServer<PseudoApplication, PseudoUser> { extends AuthorizationServer<PseudoApplication, PseudoUser> {
@override @override
PseudoApplication findClient(String clientId) { PseudoApplication? findClient(String? clientId) {
return clientId == pseudoApplication.id ? pseudoApplication : null; return clientId == pseudoApplication.id ? pseudoApplication : null;
} }
@override @override
Future<bool> verifyClient( Future<bool> verifyClient(
PseudoApplication client, String clientSecret) async { PseudoApplication client, String? clientSecret) async {
return client.secret == clientSecret; return client.secret == clientSecret;
} }
@override @override
Future<void> requestAuthorizationCode( Future<void> requestAuthorizationCode(
PseudoApplication client, PseudoApplication client,
String redirectUri, String? redirectUri,
Iterable<String> scopes, Iterable<String> scopes,
String state, String state,
RequestContext req, RequestContext req,
ResponseContext res, ResponseContext res,
bool implicit) async { bool implicit) async {
var tok = AuthorizationTokenResponse('foo'); var tok = AuthorizationTokenResponse('foo');
var uri = completeImplicitGrant(tok, Uri.parse(redirectUri), state: state); var uri = completeImplicitGrant(tok, Uri.parse(redirectUri!), state: state);
return res.redirect(uri); return res.redirect(uri);
} }
} }

View file

@ -1,15 +1,16 @@
import 'dart:async'; import 'dart:async';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel3_framework/angel3_framework.dart';
import 'package:angel_framework/http.dart'; import 'package:angel3_framework/http.dart';
import 'package:angel_oauth2/angel_oauth2.dart'; import 'package:angel3_oauth2/angel3_oauth2.dart';
import 'package:collection/collection.dart' show IterableExtension;
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:oauth2/oauth2.dart' as oauth2; import 'package:oauth2/oauth2.dart' as oauth2;
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'common.dart'; import 'common.dart';
main() { void main() {
Angel app; late Angel app;
Uri tokenEndpoint; late Uri tokenEndpoint;
setUp(() async { setUp(() async {
app = Angel(); app = Angel();
@ -50,7 +51,7 @@ main() {
}); });
test('force correct username+password', () async { test('force correct username+password', () async {
oauth2.Client client; oauth2.Client? client;
try { try {
client = await oauth2.resourceOwnerPasswordGrant( client = await oauth2.resourceOwnerPasswordGrant(
@ -88,20 +89,20 @@ main() {
class _AuthorizationServer class _AuthorizationServer
extends AuthorizationServer<PseudoApplication, PseudoUser> { extends AuthorizationServer<PseudoApplication, PseudoUser> {
@override @override
PseudoApplication findClient(String clientId) { PseudoApplication? findClient(String? clientId) {
return clientId == pseudoApplication.id ? pseudoApplication : null; return clientId == pseudoApplication.id ? pseudoApplication : null;
} }
@override @override
Future<bool> verifyClient( Future<bool> verifyClient(
PseudoApplication client, String clientSecret) async { PseudoApplication client, String? clientSecret) async {
return client.secret == clientSecret; return client.secret == clientSecret;
} }
@override @override
Future<AuthorizationTokenResponse> refreshAuthorizationToken( Future<AuthorizationTokenResponse> refreshAuthorizationToken(
PseudoApplication client, PseudoApplication? client,
String refreshToken, String? refreshToken,
Iterable<String> scopes, Iterable<String> scopes,
RequestContext req, RequestContext req,
ResponseContext res) async { ResponseContext res) async {
@ -110,15 +111,14 @@ class _AuthorizationServer
@override @override
Future<AuthorizationTokenResponse> resourceOwnerPasswordCredentialsGrant( Future<AuthorizationTokenResponse> resourceOwnerPasswordCredentialsGrant(
PseudoApplication client, PseudoApplication? client,
String username, String? username,
String password, String? password,
Iterable<String> scopes, Iterable<String> scopes,
RequestContext req, RequestContext req,
ResponseContext res) async { ResponseContext res) async {
var user = pseudoUsers.firstWhere( var user = pseudoUsers.firstWhereOrNull(
(u) => u.username == username && u.password == password, (u) => u.username == username && u.password == password);
orElse: () => null);
if (user == null) { if (user == null) {
var body = await req.parseBody().then((_) => req.bodyAsMap); var body = await req.parseBody().then((_) => req.bodyAsMap);

View file

@ -1,21 +1,21 @@
import 'dart:async'; import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel3_framework/angel3_framework.dart';
import 'package:angel_framework/http.dart'; import 'package:angel3_framework/http.dart';
import 'package:angel_oauth2/angel_oauth2.dart'; import 'package:angel3_oauth2/angel3_oauth2.dart';
import 'package:angel_test/angel_test.dart'; import 'package:angel3_test/angel3_test.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'common.dart'; import 'common.dart';
main() { void main() {
Angel app; Angel app;
Uri authorizationEndpoint, tokenEndpoint; late Uri authorizationEndpoint, tokenEndpoint;
TestClient testClient; late TestClient testClient;
setUp(() async { setUp(() async {
app = Angel(); app = Angel();
app.container.registerSingleton(AuthCodes()); app.container!.registerSingleton(AuthCodes());
var server = _Server(); var server = _Server();
@ -53,14 +53,14 @@ main() {
'redirect_uri': 'https://freddie.mercu.ry', 'redirect_uri': 'https://freddie.mercu.ry',
'code_challenge': 'foo', 'code_challenge': 'foo',
}); });
var response = await testClient var response =
.get(url.toString(), headers: {'accept': 'application/json'}); await testClient.get(url, headers: {'accept': 'application/json'});
print(response.body); print(response.body);
expect( expect(
response, response,
allOf( allOf(
hasStatus(200), hasStatus(200),
isJson({"code": "ok"}), isJson({'code': 'ok'}),
)); ));
}); });
@ -72,14 +72,14 @@ main() {
'code_challenge': 'foo', 'code_challenge': 'foo',
'code_challenge_method': 'plain', 'code_challenge_method': 'plain',
}); });
var response = await testClient var response =
.get(url.toString(), headers: {'accept': 'application/json'}); await testClient.get(url, headers: {'accept': 'application/json'});
print(response.body); print(response.body);
expect( expect(
response, response,
allOf( allOf(
hasStatus(200), hasStatus(200),
isJson({"code": "ok"}), isJson({'code': 'ok'}),
)); ));
}); });
@ -91,14 +91,14 @@ main() {
'code_challenge': 'foo', 'code_challenge': 'foo',
'code_challenge_method': 's256', 'code_challenge_method': 's256',
}); });
var response = await testClient var response =
.get(url.toString(), headers: {'accept': 'application/json'}); await testClient.get(url, headers: {'accept': 'application/json'});
print(response.body); print(response.body);
expect( expect(
response, response,
allOf( allOf(
hasStatus(200), hasStatus(200),
isJson({"code": "ok"}), isJson({'code': 'ok'}),
)); ));
}); });
@ -110,16 +110,16 @@ main() {
'code_challenge': 'foo', 'code_challenge': 'foo',
'code_challenge_method': 'bar', 'code_challenge_method': 'bar',
}); });
var response = await testClient var response =
.get(url.toString(), headers: {'accept': 'application/json'}); await testClient.get(url, headers: {'accept': 'application/json'});
print(response.body); print(response.body);
expect( expect(
response, response,
allOf( allOf(
hasStatus(400), hasStatus(400),
isJson({ isJson({
"error": "invalid_request", 'error': 'invalid_request',
"error_description": 'error_description':
"The `code_challenge_method` parameter must be either 'plain' or 's256'." "The `code_challenge_method` parameter must be either 'plain' or 's256'."
}), }),
)); ));
@ -131,16 +131,16 @@ main() {
'client_id': 'freddie mercury', 'client_id': 'freddie mercury',
'redirect_uri': 'https://freddie.mercu.ry' 'redirect_uri': 'https://freddie.mercu.ry'
}); });
var response = await testClient var response =
.get(url.toString(), headers: {'accept': 'application/json'}); await testClient.get(url, headers: {'accept': 'application/json'});
print(response.body); print(response.body);
expect( expect(
response, response,
allOf( allOf(
hasStatus(400), hasStatus(400),
isJson({ isJson({
"error": "invalid_request", 'error': 'invalid_request',
"error_description": "Missing `code_challenge` parameter." 'error_description': 'Missing `code_challenge` parameter.'
}), }),
)); ));
}); });
@ -150,7 +150,7 @@ main() {
test('with correct verifier', () async { test('with correct verifier', () async {
var url = tokenEndpoint.replace( var url = tokenEndpoint.replace(
userInfo: '${pseudoApplication.id}:${pseudoApplication.secret}'); userInfo: '${pseudoApplication.id}:${pseudoApplication.secret}');
var response = await testClient.post(url.toString(), headers: { var response = await testClient.post(url, headers: {
'accept': 'application/json', 'accept': 'application/json',
// 'authorization': 'Basic ' + base64Url.encode(ascii.encode(url.userInfo)) // 'authorization': 'Basic ' + base64Url.encode(ascii.encode(url.userInfo))
}, body: { }, body: {
@ -166,13 +166,13 @@ main() {
response, response,
allOf( allOf(
hasStatus(200), hasStatus(200),
isJson({"token_type": "bearer", "access_token": "yes"}), isJson({'token_type': 'bearer', 'access_token': 'yes'}),
)); ));
}); });
test('with incorrect verifier', () async { test('with incorrect verifier', () async {
var url = tokenEndpoint.replace( var url = tokenEndpoint.replace(
userInfo: '${pseudoApplication.id}:${pseudoApplication.secret}'); userInfo: '${pseudoApplication.id}:${pseudoApplication.secret}');
var response = await testClient.post(url.toString(), headers: { var response = await testClient.post(url, headers: {
'accept': 'application/json', 'accept': 'application/json',
// 'authorization': 'Basic ' + base64Url.encode(ascii.encode(url.userInfo)) // 'authorization': 'Basic ' + base64Url.encode(ascii.encode(url.userInfo))
}, body: { }, body: {
@ -189,9 +189,9 @@ main() {
allOf( allOf(
hasStatus(400), hasStatus(400),
isJson({ isJson({
"error": "invalid_grant", 'error': 'invalid_grant',
"error_description": 'error_description':
"The given `code_verifier` parameter is invalid." 'The given `code_verifier` parameter is invalid.'
}), }),
)); ));
}); });
@ -199,7 +199,7 @@ main() {
test('with missing verifier', () async { test('with missing verifier', () async {
var url = tokenEndpoint.replace( var url = tokenEndpoint.replace(
userInfo: '${pseudoApplication.id}:${pseudoApplication.secret}'); userInfo: '${pseudoApplication.id}:${pseudoApplication.secret}');
var response = await testClient.post(url.toString(), headers: { var response = await testClient.post(url, headers: {
'accept': 'application/json', 'accept': 'application/json',
// 'authorization': 'Basic ' + base64Url.encode(ascii.encode(url.userInfo)) // 'authorization': 'Basic ' + base64Url.encode(ascii.encode(url.userInfo))
}, body: { }, body: {
@ -215,8 +215,8 @@ main() {
allOf( allOf(
hasStatus(400), hasStatus(400),
isJson({ isJson({
"error": "invalid_request", 'error': 'invalid_request',
"error_description": "Missing `code_verifier` parameter." 'error_description': 'Missing `code_verifier` parameter.'
}), }),
)); ));
}); });
@ -225,34 +225,34 @@ main() {
class _Server extends AuthorizationServer<PseudoApplication, Map> { class _Server extends AuthorizationServer<PseudoApplication, Map> {
@override @override
FutureOr<PseudoApplication> findClient(String clientId) { FutureOr<PseudoApplication> findClient(String? clientId) {
return pseudoApplication; return pseudoApplication;
} }
@override @override
Future<bool> verifyClient( Future<bool> verifyClient(
PseudoApplication client, String clientSecret) async { PseudoApplication client, String? clientSecret) async {
return client.secret == clientSecret; return client.secret == clientSecret;
} }
@override @override
Future requestAuthorizationCode( Future requestAuthorizationCode(
PseudoApplication client, PseudoApplication client,
String redirectUri, String? redirectUri,
Iterable<String> scopes, Iterable<String> scopes,
String state, String state,
RequestContext req, RequestContext req,
ResponseContext res, ResponseContext res,
bool implicit) async { bool implicit) async {
req.container.make<Pkce>(); req.container!.make<Pkce>();
return {'code': 'ok'}; return {'code': 'ok'};
} }
@override @override
Future<AuthorizationTokenResponse> exchangeAuthorizationCodeForToken( Future<AuthorizationTokenResponse> exchangeAuthorizationCodeForToken(
PseudoApplication client, PseudoApplication? client,
String authCode, String? authCode,
String redirectUri, String? redirectUri,
RequestContext req, RequestContext req,
ResponseContext res) async { ResponseContext res) async {
var codeVerifier = await getPkceCodeVerifier(req); var codeVerifier = await getPkceCodeVerifier(req);
@ -266,7 +266,7 @@ class AuthCodes extends MapBase<String, String> with MapMixin<String, String> {
var inner = <String, String>{}; var inner = <String, String>{};
@override @override
String operator [](Object key) => inner[key]; String? operator [](Object? key) => inner[key as String];
@override @override
void operator []=(String key, String value) => inner[key] = value; void operator []=(String key, String value) => inner[key] = value;
@ -278,5 +278,5 @@ class AuthCodes extends MapBase<String, String> with MapMixin<String, String> {
Iterable<String> get keys => inner.keys; Iterable<String> get keys => inner.keys;
@override @override
String remove(Object key) => inner.remove(key); String? remove(Object? key) => inner.remove(key);
} }