Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] : Add PKCE for Authorization Code Flow #6

Merged
merged 2 commits into from Dec 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions 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
Expand Down
33 changes: 16 additions & 17 deletions README.md
Expand Up @@ -32,19 +32,18 @@ Note: OAuth2 provides several different methods for the client to obtain authori
### DataTypes
DataTypes | Parameters | Description
---------------------------- | ------------- |--------------------------
*ResultResponse<<T,String>>* | *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<<TokenResponse,String>>* `fetchToken()` | *TokenRequest* `request`
*ResultResponse<<ResourceResponse,String>>* `fetchResource()` | *String* `access_token`
*Widget* `DauthButton()` | *Function* OnPressed: (Response<TokenResponse,String> res){}
Methods | Parameters
-----------------------------------------| --------------------------
*TokenResponse* `fetchToken()` | *TokenRequest* `request`
*ResourceResponse* `fetchResource()` | *String* `access_token`
*Widget* `DauthButton()` | *Function* OnPressed: (TokenResponse res){}

## Getting started
To use this package:
Expand Down Expand Up @@ -91,16 +90,16 @@ class HomeState extends State<HomePage> {

//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(
Expand All @@ -122,33 +121,33 @@ class HomeState extends State<HomePage> {
child: dauth.DauthButton(
request: _request,
onPressed:
(dauth.ResultResponse<dauth.TokenResponse, String>
res) {
(dauth.TokenResponse res) {
//changes the exampleText as Token_TYPE: <YOUR_TOKEN> 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();
});
}
}))
],
),
)));
}

```
## 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

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')
26 changes: 10 additions & 16 deletions example/main.dart
Expand Up @@ -31,26 +31,27 @@ class HomeState extends State<HomePage> {
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,
Expand All @@ -59,18 +60,11 @@ class HomeState extends State<HomePage> {
//DAuth button returns TokenResponse and ResponseMessage when pressed.
child: dauth.DauthButton(
request: _request,
onPressed:
(dauth.ResultResponse<dauth.TokenResponse, String>
res) {
onPressed: (dauth.TokenResponse res) {
//changes the exampleText as Token_TYPE: <YOUR_TOKEN> 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();
});
}))
],
),
Expand Down
1 change: 0 additions & 1 deletion lib/flutter_dauth.dart
Expand Up @@ -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';

Expand Down
44 changes: 19 additions & 25 deletions 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;

Expand All @@ -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<ResultResponse<TokenResponse, String>> getToken(
TokenRequest request, String code) async {
var tokenResponse = TokenResponse();
var message = '';
Future<TokenResponse> getToken(TokenRequest request, String code,
Completer<TokenResponse> 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<ResultResponse<ResourceResponse, String>> getResources(
String token) async {
var userResponse = ResourceResponse();
var message = '';
Future<ResourceResponse> 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);
}
}
40 changes: 29 additions & 11 deletions 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';

Expand All @@ -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<ResultResponse<ResourceResponse, String>> fetchResources(
String accessToken) async =>
Future<ResourceResponse> 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<ResultResponse<TokenResponse, String>> fetchToken(
Future<TokenResponse> 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<ResultResponse<TokenResponse, String>>();
final completer = Completer<TokenResponse>();

//opening the webview
_openWebView(context, completer, request);
Expand All @@ -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<ResultResponse<TokenResponse, String>> completer,
TokenRequest request) async {
Future<void> _openWebView(BuildContext _context,
Completer<TokenResponse> completer, TokenRequest request) async {
//This Completer is used to notify when the WebView is loading is finished
final Completer<bool> _isPageLoaded = Completer<bool>();

Expand Down
17 changes: 12 additions & 5 deletions lib/src/model/requests/token_request.dart
Expand Up @@ -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;
Expand All @@ -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',
Expand All @@ -46,24 +51,26 @@ class TokenRequest {

factory TokenRequest.fromJson(Map<String, dynamic> 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<String, dynamic> 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,
};
}

Expand Down
12 changes: 0 additions & 12 deletions lib/src/model/response/result_response.dart

This file was deleted.