feat(http): add oauth2 refresh token client
This commit is contained in:
parent
8892337b93
commit
de00c532c8
@ -20,11 +20,15 @@ import 'dart:io';
|
||||
import 'package:wyatt_http_client/src/authentication/basic_authentication_client.dart';
|
||||
import 'package:wyatt_http_client/src/authentication/bearer_authentication_client.dart';
|
||||
import 'package:wyatt_http_client/src/authentication/digest_authentication_client.dart';
|
||||
import 'package:wyatt_http_client/src/authentication/refresh_token_client.dart';
|
||||
import 'package:wyatt_http_client/src/authentication/unsafe_authentication_client.dart';
|
||||
import 'package:wyatt_http_client/src/rest_client.dart';
|
||||
import 'package:wyatt_http_client/src/utils/header_keys.dart';
|
||||
import 'package:wyatt_http_client/src/utils/protocols.dart';
|
||||
|
||||
String lastToken = '';
|
||||
int token = 0;
|
||||
|
||||
void printAuth(HttpRequest req) {
|
||||
print(
|
||||
'Authorization => '
|
||||
@ -39,8 +43,7 @@ Future<void> handleBasic(HttpRequest req) async {
|
||||
Future<void> handleBasicNegotiate(HttpRequest req) async {
|
||||
if (req.headers.value('Authorization') == null) {
|
||||
req.response.statusCode = HttpStatus.unauthorized;
|
||||
req.response.headers
|
||||
.set(HeaderKeys.wwwAuthenticate.toString(), 'Basic realm="Wyatt"');
|
||||
req.response.headers.set(HeaderKeys.wwwAuthenticate, 'Basic realm="Wyatt"');
|
||||
print(req.response.headers.value('WWW-Authenticate'));
|
||||
return req.response.close();
|
||||
}
|
||||
@ -74,6 +77,47 @@ Future<void> handleUnsafe(HttpRequest req) async {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> handleOauth2RefreshToken(HttpRequest req) async {
|
||||
final action = req.uri.queryParameters['action'];
|
||||
if (action == null) {
|
||||
printAuth(req);
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case 'login':
|
||||
if (req.method == 'POST') {
|
||||
token++;
|
||||
req.response.write(
|
||||
'{"accessToken": "access-token-awesome$token", '
|
||||
'"refreshToken": "refresh-token-awesome$token"}',
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'refresh':
|
||||
printAuth(req);
|
||||
if (req.method == 'GET') {
|
||||
token++;
|
||||
req.response.write('{"accessToken": "access-token-refreshed$token"}');
|
||||
}
|
||||
break;
|
||||
case 'access-denied':
|
||||
final String receivedToken = req.headers.value('Authorization') ?? '';
|
||||
if (receivedToken != '' &&
|
||||
lastToken != '' &&
|
||||
receivedToken != lastToken) {
|
||||
lastToken = receivedToken;
|
||||
printAuth(req);
|
||||
return req.response.close();
|
||||
} else {
|
||||
lastToken = receivedToken;
|
||||
req.response.statusCode = HttpStatus.unauthorized;
|
||||
return req.response.close();
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> server() async {
|
||||
final server = await HttpServer.bind(InternetAddress.anyIPv6, 8080);
|
||||
var error = 0;
|
||||
@ -99,6 +143,9 @@ Future<void> server() async {
|
||||
case '/test/unsafe-test':
|
||||
handleUnsafe(request);
|
||||
break;
|
||||
case '/test/oauth2-test':
|
||||
handleOauth2RefreshToken(request);
|
||||
break;
|
||||
|
||||
case '/test/bearer-login':
|
||||
if (request.method == 'POST') {
|
||||
@ -121,12 +168,6 @@ Future<void> server() async {
|
||||
print('Error $error');
|
||||
request.response.statusCode = HttpStatus.unauthorized;
|
||||
break;
|
||||
case '/test/oauth2-test':
|
||||
print(
|
||||
'Authorization => '
|
||||
"${request.headers.value('Authorization') ?? 'no access token'}",
|
||||
);
|
||||
break;
|
||||
case '/test/oauth2-login':
|
||||
if (request.method == 'POST') {
|
||||
token++;
|
||||
@ -213,5 +254,23 @@ Future<void> main() async {
|
||||
);
|
||||
await unsafe.get(Uri.parse('/test/unsafe-test'));
|
||||
|
||||
// OAuth2
|
||||
final refreshToken = RefreshTokenClient(
|
||||
authorizationEndpoint: '/test/oauth2-test?action=login',
|
||||
tokenEndpoint: '/test/oauth2-test?action=refresh',
|
||||
accessTokenParser: (body) => body['accessToken']! as String,
|
||||
refreshTokenParser: (body) => body['refreshToken']! as String,
|
||||
inner: restClient,
|
||||
);
|
||||
await refreshToken.get(Uri.parse('/test/oauth2-test'));
|
||||
await refreshToken.authorize(<String, String>{
|
||||
'username': 'username',
|
||||
'password': 'password',
|
||||
});
|
||||
await refreshToken.get(Uri.parse('/test/oauth2-test'));
|
||||
await refreshToken.refresh();
|
||||
await refreshToken.get(Uri.parse('/test/oauth2-test'));
|
||||
await refreshToken.get(Uri.parse('/test/oauth2-test?action=access-denied'));
|
||||
|
||||
exit(0);
|
||||
}
|
@ -15,30 +15,26 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:wyatt_http_client/src/authentication/interfaces/header_authentication_client.dart';
|
||||
import 'package:wyatt_http_client/src/utils/authentication_methods.dart';
|
||||
import 'package:wyatt_http_client/src/utils/header_keys.dart';
|
||||
import 'package:wyatt_http_client/src/utils/http_status.dart';
|
||||
import 'package:wyatt_http_client/src/utils/utils.dart';
|
||||
|
||||
class BasicAuthenticationClient extends HeaderAuthenticationClient {
|
||||
final String username;
|
||||
final String password;
|
||||
final bool preemptive;
|
||||
|
||||
late String authenticationHeader;
|
||||
final String authenticationHeader;
|
||||
|
||||
BasicAuthenticationClient({
|
||||
required this.username,
|
||||
required this.password,
|
||||
this.preemptive = true,
|
||||
String? authenticationHeader,
|
||||
this.authenticationHeader = HeaderKeys.authorization,
|
||||
BaseClient? inner,
|
||||
}) : super(inner) {
|
||||
this.authenticationHeader =
|
||||
authenticationHeader ?? HeaderKeys.authorization.toString();
|
||||
}
|
||||
}) : super(inner);
|
||||
|
||||
@override
|
||||
Map<String, String> modifyHeader(
|
||||
|
@ -14,33 +14,26 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:http/http.dart';
|
||||
import 'package:wyatt_http_client/src/authentication/interfaces/header_authentication_client.dart';
|
||||
import 'package:wyatt_http_client/src/utils/authentication_methods.dart';
|
||||
import 'package:wyatt_http_client/src/utils/header_keys.dart';
|
||||
import 'package:wyatt_http_client/src/utils/http_status.dart';
|
||||
import 'package:wyatt_http_client/src/utils/utils.dart';
|
||||
|
||||
class BearerAuthenticationClient extends HeaderAuthenticationClient {
|
||||
final String token;
|
||||
final bool preemptive;
|
||||
|
||||
late String authenticationHeader;
|
||||
late String authenticationMethod;
|
||||
final String authenticationHeader;
|
||||
final String authenticationMethod;
|
||||
|
||||
BearerAuthenticationClient({
|
||||
required this.token,
|
||||
this.preemptive = true,
|
||||
String? authenticationHeader,
|
||||
String? authenticationMethod,
|
||||
this.authenticationHeader = HeaderKeys.authorization,
|
||||
this.authenticationMethod = AuthenticationMethods.bearer,
|
||||
BaseClient? inner,
|
||||
}) : super(inner) {
|
||||
this.authenticationHeader =
|
||||
authenticationHeader ?? HeaderKeys.authorization.toString();
|
||||
this.authenticationMethod =
|
||||
authenticationMethod ?? AuthenticationMethods.bearer.toString();
|
||||
}
|
||||
}) : super(inner);
|
||||
|
||||
@override
|
||||
Map<String, String> modifyHeader(
|
||||
|
@ -14,35 +14,28 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:http/http.dart';
|
||||
import 'package:wyatt_http_client/src/authentication/interfaces/header_authentication_client.dart';
|
||||
import 'package:wyatt_http_client/src/utils/digest_auth.dart';
|
||||
import 'package:wyatt_http_client/src/utils/header_keys.dart';
|
||||
import 'package:wyatt_http_client/src/utils/http_status.dart';
|
||||
import 'package:wyatt_http_client/src/utils/utils.dart';
|
||||
|
||||
class DigestAuthenticationClient extends HeaderAuthenticationClient {
|
||||
final String username;
|
||||
final String password;
|
||||
final DigestAuth _digestAuth;
|
||||
|
||||
late String authenticationHeader;
|
||||
late String wwwAuthenticateHeader;
|
||||
final String authenticationHeader;
|
||||
final String wwwAuthenticateHeader;
|
||||
|
||||
DigestAuthenticationClient({
|
||||
required this.username,
|
||||
required this.password,
|
||||
String? authenticationHeader,
|
||||
String? wwwAuthenticateHeader,
|
||||
this.authenticationHeader = HeaderKeys.authorization,
|
||||
this.wwwAuthenticateHeader = HeaderKeys.wwwAuthenticate,
|
||||
BaseClient? inner,
|
||||
}) : _digestAuth = DigestAuth(username, password),
|
||||
super(inner) {
|
||||
this.authenticationHeader =
|
||||
authenticationHeader ?? HeaderKeys.authorization.toString();
|
||||
this.wwwAuthenticateHeader =
|
||||
wwwAuthenticateHeader ?? HeaderKeys.wwwAuthenticate.toString();
|
||||
}
|
||||
super(inner);
|
||||
|
||||
@override
|
||||
Map<String, String> modifyHeader(
|
||||
@ -72,7 +65,7 @@ class DigestAuthenticationClient extends HeaderAuthenticationClient {
|
||||
if (response.statusCode == HttpStatus.unauthorized) {
|
||||
final newRequest = Utils.copyRequest(request);
|
||||
final authInfo =
|
||||
response.headers[HeaderKeys.wwwAuthenticate.toString().toLowerCase()];
|
||||
response.headers[HeaderKeys.wwwAuthenticate.toLowerCase()];
|
||||
_digestAuth.initFromAuthenticateHeader(authInfo);
|
||||
return super.send(newRequest);
|
||||
} else {
|
||||
|
@ -0,0 +1,34 @@
|
||||
// Copyright (C) 2022 WYATT GROUP
|
||||
// Please see the AUTHORS file for details.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import 'package:wyatt_http_client/src/authentication/interfaces/header_authentication_client.dart';
|
||||
|
||||
typedef TokenParser = String Function(Map<String, dynamic>);
|
||||
|
||||
abstract class Oauth2Client extends HeaderAuthenticationClient {
|
||||
Oauth2Client(super.inner);
|
||||
|
||||
Future<void> refresh() {
|
||||
return Future.value();
|
||||
}
|
||||
|
||||
Future<void> authorize(
|
||||
Map<String, dynamic> body, {
|
||||
Map<String, String>? headers,
|
||||
}) {
|
||||
return Future.value();
|
||||
}
|
||||
}
|
@ -0,0 +1,115 @@
|
||||
// Copyright (C) 2022 WYATT GROUP
|
||||
// Please see the AUTHORS file for details.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart';
|
||||
import 'package:wyatt_http_client/src/authentication/interfaces/oauth2_client.dart';
|
||||
import 'package:wyatt_http_client/src/utils/authentication_methods.dart';
|
||||
import 'package:wyatt_http_client/src/utils/header_keys.dart';
|
||||
import 'package:wyatt_http_client/src/utils/http_status.dart';
|
||||
import 'package:wyatt_http_client/src/utils/utils.dart';
|
||||
|
||||
class RefreshTokenClient extends Oauth2Client {
|
||||
final String authorizationEndpoint;
|
||||
final String tokenEndpoint;
|
||||
|
||||
String? accessToken;
|
||||
final TokenParser accessTokenParser;
|
||||
String? refreshToken;
|
||||
final TokenParser refreshTokenParser;
|
||||
|
||||
final String authenticationHeader;
|
||||
final String authenticationMethod;
|
||||
|
||||
RefreshTokenClient({
|
||||
required this.authorizationEndpoint,
|
||||
required this.tokenEndpoint,
|
||||
required this.accessTokenParser,
|
||||
required this.refreshTokenParser,
|
||||
this.authenticationHeader = HeaderKeys.authorization,
|
||||
this.authenticationMethod = AuthenticationMethods.bearer,
|
||||
BaseClient? inner,
|
||||
}) : super(inner);
|
||||
|
||||
@override
|
||||
Map<String, String> modifyHeader(
|
||||
Map<String, String> header, [
|
||||
BaseRequest? request,
|
||||
]) {
|
||||
if (accessToken != null && request != null) {
|
||||
header[authenticationHeader] = '$authenticationMethod $accessToken';
|
||||
return header;
|
||||
}
|
||||
return header;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> authorize(
|
||||
Map<String, dynamic> body, {
|
||||
Map<String, String>? headers,
|
||||
}) async {
|
||||
final response = await inner.post(
|
||||
Uri.parse(authorizationEndpoint),
|
||||
body: jsonEncode(body),
|
||||
headers: headers,
|
||||
);
|
||||
|
||||
if (response.statusCode == HttpStatus.ok) {
|
||||
final body = json.decode(response.body) as Map<String, dynamic>;
|
||||
final accessToken = accessTokenParser(body);
|
||||
final refreshToken = refreshTokenParser(body);
|
||||
|
||||
if (accessToken.isNotEmpty) {
|
||||
this.accessToken = accessToken;
|
||||
}
|
||||
if (refreshToken.isNotEmpty) {
|
||||
this.refreshToken = refreshToken;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> refresh() async {
|
||||
if (refreshToken != null) {
|
||||
final Map<String, String> header = {
|
||||
authenticationHeader: '$authenticationHeader $refreshToken',
|
||||
};
|
||||
|
||||
final response = await inner.get(
|
||||
Uri.parse(tokenEndpoint),
|
||||
headers: header,
|
||||
);
|
||||
|
||||
if (response.statusCode == HttpStatus.ok) {
|
||||
final body = json.decode(response.body) as Map<String, dynamic>;
|
||||
accessToken = accessTokenParser(body);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<StreamedResponse> send(BaseRequest request) async {
|
||||
final response = await super.send(request);
|
||||
|
||||
if (response.statusCode == HttpStatus.unauthorized) {
|
||||
await refresh();
|
||||
return super.send(Utils.copyRequest(request));
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
@ -14,18 +14,8 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
enum AuthenticationMethods {
|
||||
basic('Basic'),
|
||||
bearer('Bearer'),
|
||||
digest('Digest'),
|
||||
apiKey('ApiKey');
|
||||
|
||||
final String name;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return name;
|
||||
}
|
||||
|
||||
const AuthenticationMethods(this.name);
|
||||
abstract class AuthenticationMethods {
|
||||
static const String basic = 'Basic';
|
||||
static const String bearer = 'Bearer';
|
||||
static const String digest = 'Digest';
|
||||
}
|
||||
|
@ -14,17 +14,8 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
enum HeaderKeys {
|
||||
authorization('Authorization'),
|
||||
wwwAuthenticate('WWW-Authenticate'),
|
||||
contentType('Content-Type');
|
||||
|
||||
final String name;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return name;
|
||||
}
|
||||
|
||||
const HeaderKeys(this.name);
|
||||
abstract class HeaderKeys {
|
||||
static const String authorization = 'Authorization';
|
||||
static const String wwwAuthenticate = 'WWW-Authenticate';
|
||||
static const String contentType = 'Content-Type';
|
||||
}
|
||||
|
83
packages/wyatt_http_client/lib/src/utils/http_status.dart
Normal file
83
packages/wyatt_http_client/lib/src/utils/http_status.dart
Normal file
@ -0,0 +1,83 @@
|
||||
// Copyright (C) 2022 WYATT GROUP
|
||||
// Please see the AUTHORS file for details.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
/// Status codes for HTTP responses. Extracted from dart:io
|
||||
abstract class HttpStatus {
|
||||
static const int continue_ = 100;
|
||||
static const int switchingProtocols = 101;
|
||||
static const int processing = 102;
|
||||
static const int ok = 200;
|
||||
static const int created = 201;
|
||||
static const int accepted = 202;
|
||||
static const int nonAuthoritativeInformation = 203;
|
||||
static const int noContent = 204;
|
||||
static const int resetContent = 205;
|
||||
static const int partialContent = 206;
|
||||
static const int multiStatus = 207;
|
||||
static const int alreadyReported = 208;
|
||||
static const int imUsed = 226;
|
||||
static const int multipleChoices = 300;
|
||||
static const int movedPermanently = 301;
|
||||
static const int found = 302;
|
||||
static const int movedTemporarily = 302; // Common alias for found.
|
||||
static const int seeOther = 303;
|
||||
static const int notModified = 304;
|
||||
static const int useProxy = 305;
|
||||
static const int temporaryRedirect = 307;
|
||||
static const int permanentRedirect = 308;
|
||||
static const int badRequest = 400;
|
||||
static const int unauthorized = 401;
|
||||
static const int paymentRequired = 402;
|
||||
static const int forbidden = 403;
|
||||
static const int notFound = 404;
|
||||
static const int methodNotAllowed = 405;
|
||||
static const int notAcceptable = 406;
|
||||
static const int proxyAuthenticationRequired = 407;
|
||||
static const int requestTimeout = 408;
|
||||
static const int conflict = 409;
|
||||
static const int gone = 410;
|
||||
static const int lengthRequired = 411;
|
||||
static const int preconditionFailed = 412;
|
||||
static const int requestEntityTooLarge = 413;
|
||||
static const int requestUriTooLong = 414;
|
||||
static const int unsupportedMediaType = 415;
|
||||
static const int requestedRangeNotSatisfiable = 416;
|
||||
static const int expectationFailed = 417;
|
||||
static const int misdirectedRequest = 421;
|
||||
static const int unprocessableEntity = 422;
|
||||
static const int locked = 423;
|
||||
static const int failedDependency = 424;
|
||||
static const int upgradeRequired = 426;
|
||||
static const int preconditionRequired = 428;
|
||||
static const int tooManyRequests = 429;
|
||||
static const int requestHeaderFieldsTooLarge = 431;
|
||||
static const int connectionClosedWithoutResponse = 444;
|
||||
static const int unavailableForLegalReasons = 451;
|
||||
static const int clientClosedRequest = 499;
|
||||
static const int internalServerError = 500;
|
||||
static const int notImplemented = 501;
|
||||
static const int badGateway = 502;
|
||||
static const int serviceUnavailable = 503;
|
||||
static const int gatewayTimeout = 504;
|
||||
static const int httpVersionNotSupported = 505;
|
||||
static const int variantAlsoNegotiates = 506;
|
||||
static const int insufficientStorage = 507;
|
||||
static const int loopDetected = 508;
|
||||
static const int notExtended = 510;
|
||||
static const int networkAuthenticationRequired = 511;
|
||||
// Client generated status code.
|
||||
static const int networkConnectTimeoutError = 599;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user