feat(http): add oauth2 refresh token client

This commit is contained in:
Hugo Pointcheval 2022-05-23 20:37:26 +02:00
parent 8892337b93
commit de00c532c8
Signed by: hugo
GPG Key ID: A9E8E9615379254F
9 changed files with 324 additions and 70 deletions

View File

@ -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);
}

View File

@ -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(

View File

@ -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(

View File

@ -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 {

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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';
}

View File

@ -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';
}

View 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;
}