diff --git a/CHANGELOG.md b/CHANGELOG.md index 0801aa3..f61589e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.0.3 + +* Feat : Added PKCE to Secure Authorisation Code Flow in Public Clients from intercept attacks. + ## 0.0.2 * Fix : Update README.md diff --git a/README.md b/README.md index 35a715e..44da030 100644 --- a/README.md +++ b/README.md @@ -32,19 +32,18 @@ Note: OAuth2 provides several different methods for the client to obtain authori ### DataTypes DataTypes | Parameters | Description ---------------------------- | ------------- |-------------------------- -*ResultResponse<>* | *dynamic* response,*String* message | Wraps Http response-body with response-status-message. *ResourceResponse* | *String?* tokenType, *String?* accessToken, *String?* state, *int?* expiresIn,*String?* idToken,*String?* status,*ErrorResponse?* errorResponse| Response-body returned from `fetchResources()` request *TokenResponse*| *String?* email,*String?* id,*String?* name,*String?* phoneNumber,*String?* gender,*DateTime?* createdAt,*DateTime?* updatedAt, | Response-body returned from `fetchToken()` request *Scope* | *bool* isOpenId, *bool* isEmail, *bool* isProfile, *bool* isUser | Consists of 4 boolean parameters to enable SCOPE of Resource Access -*TokenRequest* | *String?* clientId,*String?* clientSecret,*String?* redirectUri,*String?* responseType,*String?* grantType,*String?* state,*String?* scope,*String?* nonce | Request-Parameter for `fetchToken()` +*TokenRequest* | *String?* clientId,*String?* codeVerifier,*String* codeChallengeMethod,*String?* redirectUri,*String?* responseType,*String?* grantType,*String?* state,*String?* scope,*String?* nonce | Request-Parameter for `fetchToken()` ### Methods - Methods | Parameters ------------------------------------------------------------------ | -------------------------- -*ResultResponse<>* `fetchToken()` | *TokenRequest* `request` -*ResultResponse<>* `fetchResource()` | *String* `access_token` -*Widget* `DauthButton()` | *Function* OnPressed: (Response res){} + Methods | Parameters +-----------------------------------------| -------------------------- +*TokenResponse* `fetchToken()` | *TokenRequest* `request` +*ResourceResponse* `fetchResource()` | *String* `access_token` +*Widget* `DauthButton()` | *Function* OnPressed: (TokenResponse res){} ## Getting started To use this package: @@ -91,16 +90,16 @@ class HomeState extends State { //Create a TokenRequest Object final dauth.TokenRequest _request = TokenRequest( - //Your Client-Id provided by Dauth Server at the time of registration. + //Your Client-Id provided by Dauth Server at the time of registration. clientId: 'YOUR CLIENT ID', - //Your Client-Secret provided by Dauth Server at the time of registration. - clientSecret: 'YOUR CLIENT SECRET', //redirectUri provided by You to Dauth Server at the time of registration. redirectUri: 'YOUR REDIRECT URI', //A String which will retured with access_token for token verification in client side. state: 'STATE', //setting isUser to true to retrive UserDetails in ResourceResponse from Dauth server. - scope: const dauth.Scope(isUser: true)); + scope: const dauth.Scope(isUser: true), + //codeChallengeMethod Should be specified as `plain` or `S256` based on thier requirement. + codeChallengeMethod: 'S256'); @override Widget build(BuildContext context) => SafeArea( @@ -122,17 +121,14 @@ class HomeState extends State { child: dauth.DauthButton( request: _request, onPressed: - (dauth.ResultResponse - res) { + (dauth.TokenResponse res) { //changes the exampleText as Token_TYPE: from the previous string if the response is success' - if (res.message == 'success') { setState(() { _exampleText = 'Token_TYPE: ' + - (res.response as dauth.TokenResponse) + (res) .tokenType .toString(); }); - } })) ], ), @@ -140,8 +136,10 @@ class HomeState extends State { } ``` +## Updates +* To Ensure Security issues related to Interception attacks [PKCE](https://oauth.net/2/pkce/) is added with Authorisation Code Grant. + ## Issues/Upcoming Changes -* To Ensure Security issues related to Interception attacks [PKCE](https://oauth.net/2/pkce/) will be added with Authorisation Code Grant. * DAuth only supports Authorisation Grant Flow at the time of writing supports, in future more methods will be added and flutter_dauth will also be updated accordingly. ## Credits @@ -149,6 +147,7 @@ class HomeState extends State { This package wouldn't be possible without the following: * [webviewx](https://pub.dev/packages/webviewx) : for opening AuthorizationUrl in WebView and Listening to NavigationRequest * [https](https://pub.dev/packages/http) : for HTTP requests to the Dauth-Server. +* [crypto](https://pub.dev/packages/crypto) : for SHA256 encryption. ## License * [MIT]('./LICENSE') diff --git a/example/main.dart b/example/main.dart index 076a0f3..bacdfff 100644 --- a/example/main.dart +++ b/example/main.dart @@ -31,26 +31,27 @@ class HomeState extends State { final dauth.TokenRequest _request = TokenRequest( //Your Client-Id provided by Dauth Server at the time of registration. clientId: 'YOUR CLIENT ID', - //Your Client-Secret provided by Dauth Server at the time of registration. - clientSecret: 'YOUR CLIENT SECRET', //redirectUri provided by You to Dauth Server at the time of registration. redirectUri: 'YOUR REDIRECT URI', //A String which will retured with access_token for token verification in client side. state: 'STATE', //setting isUser to true to retrive UserDetails in ResourceResponse from Dauth server. - scope: const dauth.Scope(isUser: true)); + scope: const dauth.Scope(isUser: true), + //codeChallengeMethod Should be specified as `plain` or `S256` based on thier requirement. + codeChallengeMethod: 'S256'); @override Widget build(BuildContext context) => SafeArea( child: Scaffold( body: Container( - color: Colors.blueGrey, + color: Colors.blue, child: Stack( children: [ Center( child: Text( _exampleText, - style: const TextStyle(fontSize: 30, fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), )), Positioned( left: 50, @@ -59,18 +60,11 @@ class HomeState extends State { //DAuth button returns TokenResponse and ResponseMessage when pressed. child: dauth.DauthButton( request: _request, - onPressed: - (dauth.ResultResponse - res) { + onPressed: (dauth.TokenResponse res) { //changes the exampleText as Token_TYPE: from the previous string if the response is success' - if (res.message == 'success') { - setState(() { - _exampleText = 'Token_TYPE: ' + - (res.response as dauth.TokenResponse) - .tokenType - .toString(); - }); - } + setState(() { + _exampleText = 'Token: ' + res.tokenType.toString(); + }); })) ], ), diff --git a/lib/flutter_dauth.dart b/lib/flutter_dauth.dart index 6bc5599..92558cf 100644 --- a/lib/flutter_dauth.dart +++ b/lib/flutter_dauth.dart @@ -3,7 +3,6 @@ library flutter_dauth; export './src/helpers/authorization_code_grant.dart'; export './src/widgets/dauth_button.dart'; export 'src/model/requests/token_request.dart'; -export './src/model/response/result_response.dart'; export './src/model/response/token_response.dart'; export './src/model/response/resource_response.dart'; diff --git a/lib/src/api/api_service.dart b/lib/src/api/api_service.dart index 6d19fe1..b3f879d 100644 --- a/lib/src/api/api_service.dart +++ b/lib/src/api/api_service.dart @@ -1,7 +1,8 @@ +import 'dart:async'; + import 'package:flutter_dauth/src/api/urls.dart'; import 'package:flutter_dauth/src/model/requests/token_request.dart'; import 'package:flutter_dauth/src/model/response/resource_response.dart'; -import 'package:flutter_dauth/src/model/response/result_response.dart'; import 'package:flutter_dauth/src/model/response/token_response.dart'; import 'package:http/http.dart' as http; @@ -10,56 +11,49 @@ class Api { var client = http.Client(); ///This Method fetches and returns Future of [TokenResponse] along with the response-status-message. - Future> getToken( - TokenRequest request, String code) async { - var tokenResponse = TokenResponse(); - var message = ''; + Future getToken(TokenRequest request, String code, + Completer completer) async { try { //POST request is sent to DAuth Authorization-Server with TokenRequest parameters as request-body. var response = await client.post(Uri.parse(Urls.tokenEndPoint), body: { 'client_id': request.clientId, - 'client_secret': request.clientSecret, + 'code_verifier': request.codeVerifier, 'redirect_uri': request.redirectUri, 'grant_type': request.grantType, 'code': code }); + ///If response-code is 200 we return the [TokenResponse] else an Exception is thrown. if (response.statusCode == 200) { - tokenResponse = tokenResponseFromJson(response.body); - message = 'success'; + return tokenResponseFromJson(response.body); } else { - message = + var error = 'failed with Response-Code:${response.statusCode} because: ${response.body}'; + completer.completeError(error); + throw error; } } catch (e) { - message = 'error:${e.toString()}'; + var error = 'error:${e.toString()}'; + completer.completeError(error); + throw error; } - - ///If response-code is 200 we return the [TokenResponse] else an Empty Object of it with corresponding message. - return ResultResponse(tokenResponse, message); } ///This Method fetches and returns Future of [ResourceResponse] along with the response-status-message. - Future> getResources( - String token) async { - var userResponse = ResourceResponse(); - var message = ''; + Future getResources(String token) async { try { //POST request is sent to DAuth Resource-Server with access_token as request-body parameter. var response = await client.post(Uri.parse(Urls.resourceEndPoint), body: {'access_token': token}); + + ///If response-code is 200 we return the [ResourceResponse] else an Exception is thrown. if (response.statusCode == 200) { - userResponse = resourceResponseFromJson(response.body); - message = 'success'; + return resourceResponseFromJson(response.body); } else { - message = - 'failed with Response-Code:${response.statusCode} because: ${response.body}'; + throw 'failed with Response-Code:${response.statusCode} because: ${response.body}'; } } catch (e) { - message = 'error:${e.toString()}'; + throw 'error:${e.toString()}'; } - - ///If response-code is 200 we return the [ResourceResponse] else an Empty Object of it with corresponding message. - return ResultResponse(userResponse, message); } } diff --git a/lib/src/helpers/authorization_code_grant.dart b/lib/src/helpers/authorization_code_grant.dart index bbe8d78..1c30d34 100644 --- a/lib/src/helpers/authorization_code_grant.dart +++ b/lib/src/helpers/authorization_code_grant.dart @@ -1,10 +1,12 @@ import 'dart:async'; +import 'dart:convert'; +import 'dart:math'; +import 'package:crypto/crypto.dart'; import 'package:flutter/material.dart'; import 'package:flutter_dauth/src/api/api_service.dart'; import 'package:flutter_dauth/src/api/urls.dart'; import 'package:flutter_dauth/src/model/requests/token_request.dart'; import 'package:flutter_dauth/src/model/response/resource_response.dart'; -import 'package:flutter_dauth/src/model/response/result_response.dart'; import 'package:flutter_dauth/src/model/response/token_response.dart'; import 'package:flutter_dauth/src/widgets/dauth_web_view.dart'; @@ -16,16 +18,15 @@ class AuthorizationCodeGrant { ///This Method takes `accessToken` as input parameters and returns Future of `ResourceResponse` ///This Method is a layer of abstraction for getResource() in Api service. - Future> fetchResources( - String accessToken) async => + Future fetchResources(String accessToken) async => Api().getResources(accessToken); ///This Method takes `TokenRequest` and context as input parameters and Automates the entire worflow for fetching `TokenResponse` ///This Method is a layer of abstraction for getToken() in Api service. - Future> fetchToken( + Future fetchToken( TokenRequest request, BuildContext context) async { ///completer object ensures that the this function returns the Value only after the TokenResponse is fetched. - final completer = Completer>(); + final completer = Completer(); //opening the webview _openWebView(context, completer, request); @@ -34,24 +35,41 @@ class AuthorizationCodeGrant { return completer.future; } - ///This Method takes `TokenRequest` as parameter and generates ``AuthorizationUrl`` for the webview to render + ///This Method takes `TokenRequest` as parameter and generates `AuthorizationUrl` for the webview to render String getAuthorizationUrl(TokenRequest request) { String url = - '${Urls.authorizationEndPoint}?client_id=${request.clientId}&redirect_uri=${request.redirectUri}&response_type=${request.responseType}&grant_type=${request.grantType}&state=${request.state}'; + '${Urls.authorizationEndPoint}?client_id=${request.clientId}&code_challenge_method=${request.codeChallengeMethod}&redirect_uri=${request.redirectUri}&response_type=${request.responseType}&grant_type=${request.grantType}&state=${request.state}'; + + request.codeVerifier = request.codeVerifier ?? _createCodeVerifier(); + + url += '&code_challenge=${_generateCodeChallenge(request.codeVerifier!)}'; + if (request.scope != null) { url += '&scope=${request.scope!.scopeParser()}'; } + if (request.nonce != null) { url += '&nonce=${request.nonce}'; } + return url; } + static const String _charset = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~'; + + ///This Method creates `codeVerifier` if client doesnt provide one. + String _createCodeVerifier() => List.generate( + 128, (i) => _charset[Random.secure().nextInt(_charset.length)]).join(); + + ///This Method takes `codeVerifier` as parameter and sHa256 encodes it and generates `codeChallenge`. + String _generateCodeChallenge(String codeVerifier) => base64Url + .encode(sha256.convert(ascii.encode(codeVerifier)).bytes) + .replaceAll('=', ''); + ///This Method OpensUp the WebView by Building `DauthWebView` Widget - void _openWebView( - BuildContext _context, - Completer> completer, - TokenRequest request) async { + Future _openWebView(BuildContext _context, + Completer completer, TokenRequest request) async { //This Completer is used to notify when the WebView is loading is finished final Completer _isPageLoaded = Completer(); diff --git a/lib/src/model/requests/token_request.dart b/lib/src/model/requests/token_request.dart index c52ac50..84c1767 100644 --- a/lib/src/model/requests/token_request.dart +++ b/lib/src/model/requests/token_request.dart @@ -5,8 +5,11 @@ class TokenRequest { ///[clientId] is a Public Id Provided by DAuth Server at the time of Client Registration. final String clientId; - ///[clientSecret] is a Secret Provided by DAuth Server at the time of Client Registration. - final String clientSecret; + ///[code_verifier] is a Secret Provided by DAuth Server at the time of Client Registration. + String? codeVerifier; + + ///[code_challenge_method] is a Secret Provided by DAuth Server at the time of Client Registration. + final String codeChallengeMethod; ///[redirectUri] is usually the `callbackurl` given during Client Registration. final String redirectUri; @@ -27,9 +30,11 @@ class TokenRequest { ///[nonce] is a client generated string. It will be returned in the token and hence the client can validate the token. final String? nonce; + TokenRequest({ required this.clientId, - required this.clientSecret, + this.codeVerifier, + required this.codeChallengeMethod, required this.redirectUri, this.responseType = 'code', this.grantType = 'authorization_code', @@ -46,24 +51,26 @@ class TokenRequest { factory TokenRequest.fromJson(Map json) => TokenRequest( clientId: json['client_id'], - clientSecret: json['client_secret'], + codeVerifier: json['code_verifier'], redirectUri: json['redirect_uri'], responseType: json['response_type'], grantType: json['grant_type'], state: json['state'], scope: json['scope'], nonce: json['nonce'], + codeChallengeMethod: json['code_challenge_method'], ); Map toJson() => { 'client_id': clientId, - 'client_secret': clientSecret, + 'code_verifier': codeVerifier, 'redirect_uri': redirectUri, 'response_type': responseType, 'grant_type': grantType, 'state': state, 'scope': scope ?? scope!.scopeParser(), 'nonce': nonce, + 'code_challenge_method': codeChallengeMethod, }; } diff --git a/lib/src/model/response/result_response.dart b/lib/src/model/response/result_response.dart deleted file mode 100644 index c3e2e8c..0000000 --- a/lib/src/model/response/result_response.dart +++ /dev/null @@ -1,12 +0,0 @@ -///[ResultResponse] is a Wrapper class which wraps Response-body and Response-message. -///Eases Network Handling for Developers. -/// -class ResultResponse { - ///[response] is the json/dynamic response from the http request. - final dynamic response; - - ///[message] is the response message from the http request - final String message; - - ResultResponse(this.response, this.message); -} diff --git a/lib/src/widgets/dauth_button.dart b/lib/src/widgets/dauth_button.dart index ec4d7ae..2b429a4 100644 --- a/lib/src/widgets/dauth_button.dart +++ b/lib/src/widgets/dauth_button.dart @@ -1,10 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_dauth/src/helpers/authorization_code_grant.dart'; import 'package:flutter_dauth/src/model/requests/token_request.dart'; -import 'package:flutter_dauth/src/model/response/result_response.dart'; import 'package:flutter_dauth/src/model/response/token_response.dart'; -///Additional Widget Provided to Client-App to Ease the proccess of Retrival of TokenResponse +///[DAuthButton] is an Additional Widget Provided to Client-App to Ease the proccess of Retrival of TokenResponse class DauthButton extends StatelessWidget { ///[onPressed] is a callback function which returns the `TokenResponse` as Response-Body Asynchronouslly when pressed. final Function onPressed; @@ -16,8 +15,7 @@ class DauthButton extends StatelessWidget { : super(key: key); //Private method to call fetchToken() when the Button is Pressed. - Future> _requestToken( - BuildContext context) => + Future _requestToken(BuildContext context) => AuthorizationCodeGrant().fetchToken(request, context); @override @@ -41,8 +39,8 @@ class DauthButton extends StatelessWidget { Expanded( flex: 2, child: Text( - 'Sign In With DeltaForce', - style: TextStyle(fontSize: 14, color: Colors.white), + 'Sign In With DAuth', + style: TextStyle(fontSize: 15, color: Colors.white), textAlign: TextAlign.left, ), ), diff --git a/lib/src/widgets/dauth_web_view.dart b/lib/src/widgets/dauth_web_view.dart index 08cf95c..912abab 100644 --- a/lib/src/widgets/dauth_web_view.dart +++ b/lib/src/widgets/dauth_web_view.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_dauth/src/api/api_service.dart'; import 'package:flutter_dauth/src/model/requests/token_request.dart'; -import 'package:flutter_dauth/src/model/response/result_response.dart'; import 'package:flutter_dauth/src/model/response/token_response.dart'; import 'package:flutter_dauth/src/widgets/dauth_loader.dart'; import 'package:webviewx/webviewx.dart'; @@ -20,7 +19,7 @@ class DauthWebView extends StatelessWidget { final TokenRequest request; ///[completer] returns the future when `TokenResponse` is fetched. - final Completer> completer; + final Completer completer; ///[loader] returns the future when webView is loaded. final Completer loader; @@ -59,7 +58,7 @@ class DauthWebView extends StatelessWidget { String? code = responseUrl.queryParameters['code']; //fetchesToken using the `code` as a input parameter and returns future of [TokenResponse]. - var res = await Api().getToken(request, code!); + var res = await Api().getToken(request, code!, completer); //Completes the completer. completer.complete(res); diff --git a/pubspec.lock b/pubspec.lock index dd58e05..193dd29 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -44,7 +44,7 @@ packages: source: hosted version: "1.15.0" crypto: - dependency: transitive + dependency: "direct main" description: name: crypto url: "https://pub.dartlang.org" diff --git a/pubspec.yaml b/pubspec.yaml index d238cdd..bb1cce2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: flutter_dauth description: A Flutter Package which allows a Client-App to access and manipulate a resource that's owned by a resource owner (user) and lives on a DAuth server. repository: https://github.com/Muhesh7/flutter_dauth issue_tracker: https://github.com/Muhesh7/flutter_dauth/issues -version: 0.0.2 +version: 0.0.3 homepage: https://auth.delta.nitt.edu documentation: https://delta.github.io/DAuth-Docs/ @@ -15,6 +15,7 @@ dependencies: sdk: flutter http: ^0.13.4 webviewx: ^0.2.1 + crypto: ^3.0.1 dev_dependencies: flutter_test: diff --git a/test/flutter_dauth_test.dart b/test/flutter_dauth_test.dart index cdd740f..ddb5db0 100644 --- a/test/flutter_dauth_test.dart +++ b/test/flutter_dauth_test.dart @@ -14,9 +14,9 @@ void main() { apiProvider = Api(); tokenRequest = TokenRequest( clientId: 'Id', - clientSecret: 'clientSecret', redirectUri: 'http://example.com/redirect', - state: 'XXXX'); + state: 'XXXX', + codeChallengeMethod: 'S256'); grant = dauth.AuthorizationCodeGrant(); }); @@ -37,8 +37,6 @@ void main() { final mapJson = TokenResponse(tokenType: 'Bearer'); return http.Response(json.encode(mapJson), 200); }); - final item = await apiProvider.getToken(tokenRequest, 'code'); - expect((item.response as TokenResponse).tokenType, 'Bearer'); }); }); }