diff --git a/packages/wyatt_http_client/example/wyatt_http_client_example.dart b/packages/wyatt_http_client/example/http_client_example.dart similarity index 75% rename from packages/wyatt_http_client/example/wyatt_http_client_example.dart rename to packages/wyatt_http_client/example/http_client_example.dart index 27d567fa..e8766148 100644 --- a/packages/wyatt_http_client/example/wyatt_http_client_example.dart +++ b/packages/wyatt_http_client/example/http_client_example.dart @@ -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 handleBasic(HttpRequest req) async { Future 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 handleUnsafe(HttpRequest req) async { ); } +Future 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 server() async { final server = await HttpServer.bind(InternetAddress.anyIPv6, 8080); var error = 0; @@ -99,6 +143,9 @@ Future 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 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 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({ + '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); } diff --git a/packages/wyatt_http_client/lib/src/authentication/basic_authentication_client.dart b/packages/wyatt_http_client/lib/src/authentication/basic_authentication_client.dart index 497b6303..79bdef1b 100644 --- a/packages/wyatt_http_client/lib/src/authentication/basic_authentication_client.dart +++ b/packages/wyatt_http_client/lib/src/authentication/basic_authentication_client.dart @@ -15,30 +15,26 @@ // along with this program. If not, see . 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 modifyHeader( diff --git a/packages/wyatt_http_client/lib/src/authentication/bearer_authentication_client.dart b/packages/wyatt_http_client/lib/src/authentication/bearer_authentication_client.dart index 12408833..cf16c730 100644 --- a/packages/wyatt_http_client/lib/src/authentication/bearer_authentication_client.dart +++ b/packages/wyatt_http_client/lib/src/authentication/bearer_authentication_client.dart @@ -14,33 +14,26 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -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 modifyHeader( diff --git a/packages/wyatt_http_client/lib/src/authentication/digest_authentication_client.dart b/packages/wyatt_http_client/lib/src/authentication/digest_authentication_client.dart index 54df955b..cc5b96ea 100644 --- a/packages/wyatt_http_client/lib/src/authentication/digest_authentication_client.dart +++ b/packages/wyatt_http_client/lib/src/authentication/digest_authentication_client.dart @@ -14,35 +14,28 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -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 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 { diff --git a/packages/wyatt_http_client/lib/src/authentication/interfaces/oauth2_client.dart b/packages/wyatt_http_client/lib/src/authentication/interfaces/oauth2_client.dart new file mode 100644 index 00000000..1da70e99 --- /dev/null +++ b/packages/wyatt_http_client/lib/src/authentication/interfaces/oauth2_client.dart @@ -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 . + +import 'package:wyatt_http_client/src/authentication/interfaces/header_authentication_client.dart'; + +typedef TokenParser = String Function(Map); + +abstract class Oauth2Client extends HeaderAuthenticationClient { + Oauth2Client(super.inner); + + Future refresh() { + return Future.value(); + } + + Future authorize( + Map body, { + Map? headers, + }) { + return Future.value(); + } +} diff --git a/packages/wyatt_http_client/lib/src/authentication/refresh_token_client.dart b/packages/wyatt_http_client/lib/src/authentication/refresh_token_client.dart new file mode 100644 index 00000000..67d6a1d0 --- /dev/null +++ b/packages/wyatt_http_client/lib/src/authentication/refresh_token_client.dart @@ -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 . + +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 modifyHeader( + Map header, [ + BaseRequest? request, + ]) { + if (accessToken != null && request != null) { + header[authenticationHeader] = '$authenticationMethod $accessToken'; + return header; + } + return header; + } + + @override + Future authorize( + Map body, { + Map? 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; + final accessToken = accessTokenParser(body); + final refreshToken = refreshTokenParser(body); + + if (accessToken.isNotEmpty) { + this.accessToken = accessToken; + } + if (refreshToken.isNotEmpty) { + this.refreshToken = refreshToken; + } + } + } + + @override + Future refresh() async { + if (refreshToken != null) { + final Map 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; + accessToken = accessTokenParser(body); + } + } + } + + @override + Future 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; + } +} diff --git a/packages/wyatt_http_client/lib/src/utils/authentication_methods.dart b/packages/wyatt_http_client/lib/src/utils/authentication_methods.dart index 5ca57054..ef73c15f 100644 --- a/packages/wyatt_http_client/lib/src/utils/authentication_methods.dart +++ b/packages/wyatt_http_client/lib/src/utils/authentication_methods.dart @@ -14,18 +14,8 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -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'; } diff --git a/packages/wyatt_http_client/lib/src/utils/header_keys.dart b/packages/wyatt_http_client/lib/src/utils/header_keys.dart index 978e8821..9fe72ce8 100644 --- a/packages/wyatt_http_client/lib/src/utils/header_keys.dart +++ b/packages/wyatt_http_client/lib/src/utils/header_keys.dart @@ -14,17 +14,8 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -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'; } diff --git a/packages/wyatt_http_client/lib/src/utils/http_status.dart b/packages/wyatt_http_client/lib/src/utils/http_status.dart new file mode 100644 index 00000000..931c3587 --- /dev/null +++ b/packages/wyatt_http_client/lib/src/utils/http_status.dart @@ -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 . + +/// 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; +}