Add PKCE support to AuthorizationCodeGrant (#69)

* Add PKCE support

* Run dartfmt
diff --git a/lib/src/authorization_code_grant.dart b/lib/src/authorization_code_grant.dart
index 526ab9f..1e930e1 100644
--- a/lib/src/authorization_code_grant.dart
+++ b/lib/src/authorization_code_grant.dart
@@ -3,7 +3,10 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'dart:async';
+import 'dart:convert';
+import 'dart:math';
 
+import 'package:crypto/crypto.dart';
 import 'package:http/http.dart' as http;
 import 'package:http_parser/http_parser.dart';
 
@@ -100,6 +103,13 @@
   /// The current state of the grant object.
   _State _state = _State.initial;
 
+  /// Allowed characters for generating the _codeVerifier
+  static const String _charset =
+      "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
+
+  /// The generated PKCE code verifier
+  String _codeVerifier;
+
   /// Creates a new grant.
   ///
   /// If [basicAuth] is `true` (the default), the client credentials are sent to
@@ -175,13 +185,20 @@
       scopes = scopes.toList();
     }
 
+    _codeVerifier = _createCodeVerifier();
+    var codeChallenge = base64Url
+        .encode(sha256.convert(ascii.encode(_codeVerifier)).bytes)
+        .replaceAll("=", "");
+
     this._redirectEndpoint = redirect;
     this._scopes = scopes;
     this._stateString = state;
     var parameters = {
       "response_type": "code",
       "client_id": this.identifier,
-      "redirect_uri": redirect.toString()
+      "redirect_uri": redirect.toString(),
+      "code_challenge": codeChallenge,
+      "code_challenge_method": "S256"
     };
 
     if (state != null) parameters['state'] = state;
@@ -278,7 +295,8 @@
     var body = {
       "grant_type": "authorization_code",
       "code": authorizationCode,
-      "redirect_uri": this._redirectEndpoint.toString()
+      "redirect_uri": this._redirectEndpoint.toString(),
+      "code_verifier": _codeVerifier
     };
 
     if (_basicAuth && secret != null) {
@@ -304,6 +322,12 @@
         onCredentialsRefreshed: _onCredentialsRefreshed);
   }
 
+  /// Randomly generate a 128 character string to be used as the PKCE code verifier
+  static String _createCodeVerifier() {
+    return List.generate(
+        128, (i) => _charset[Random.secure().nextInt(_charset.length)]).join();
+  }
+
   /// Closes the grant and frees its resources.
   ///
   /// This will close the underlying HTTP client, which is shared by the
diff --git a/pubspec.yaml b/pubspec.yaml
index 30cd3f8..0b3d3f2 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -13,6 +13,7 @@
 dependencies:
   http: '>=0.11.0 <0.13.0'
   http_parser: '>=1.0.0 <4.0.0'
+  crypto: '^2.1.3'
 
 dev_dependencies:
   pedantic: ^1.2.0
diff --git a/test/authorization_code_grant_test.dart b/test/authorization_code_grant_test.dart
index babdb78..2fcf36b 100644
--- a/test/authorization_code_grant_test.dart
+++ b/test/authorization_code_grant_test.dart
@@ -30,10 +30,13 @@
     test('builds the correct URL', () {
       expect(
           grant.getAuthorizationUrl(redirectUrl).toString(),
-          equals('https://example.com/authorization'
-              '?response_type=code'
-              '&client_id=identifier'
-              '&redirect_uri=http%3A%2F%2Fexample.com%2Fredirect'));
+          allOf([
+            startsWith('https://example.com/authorization?response_type=code'),
+            contains('&client_id=identifier'),
+            contains('&redirect_uri=http%3A%2F%2Fexample.com%2Fredirect'),
+            matches(r'&code_challenge=[A-Za-z0-9\+\/\-\_]{43}'),
+            contains('&code_challenge_method=S256')
+          ]));
     });
 
     test('builds the correct URL with scopes', () {
@@ -41,11 +44,14 @@
           .getAuthorizationUrl(redirectUrl, scopes: ['scope', 'other/scope']);
       expect(
           authorizationUrl.toString(),
-          equals('https://example.com/authorization'
-              '?response_type=code'
-              '&client_id=identifier'
-              '&redirect_uri=http%3A%2F%2Fexample.com%2Fredirect'
-              '&scope=scope+other%2Fscope'));
+          allOf([
+            startsWith('https://example.com/authorization?response_type=code'),
+            contains('&client_id=identifier'),
+            contains('&redirect_uri=http%3A%2F%2Fexample.com%2Fredirect'),
+            matches(r'&code_challenge=[A-Za-z0-9\+\/\-\_]{43}'),
+            contains('&code_challenge_method=S256'),
+            contains('&scope=scope+other%2Fscope')
+          ]));
     });
 
     test('separates scopes with the correct delimiter', () {
@@ -60,11 +66,14 @@
           .getAuthorizationUrl(redirectUrl, scopes: ['scope', 'other/scope']);
       expect(
           authorizationUrl.toString(),
-          equals('https://example.com/authorization'
-              '?response_type=code'
-              '&client_id=identifier'
-              '&redirect_uri=http%3A%2F%2Fexample.com%2Fredirect'
-              '&scope=scope_other%2Fscope'));
+          allOf([
+            startsWith('https://example.com/authorization?response_type=code'),
+            contains('&client_id=identifier'),
+            contains('&redirect_uri=http%3A%2F%2Fexample.com%2Fredirect'),
+            matches(r'&code_challenge=[A-Za-z0-9\+\/\-\_]{43}'),
+            contains('&code_challenge_method=S256'),
+            contains('&scope=scope_other%2Fscope')
+          ]));
     });
 
     test('builds the correct URL with state', () {
@@ -72,11 +81,14 @@
           grant.getAuthorizationUrl(redirectUrl, state: 'state');
       expect(
           authorizationUrl.toString(),
-          equals('https://example.com/authorization'
-              '?response_type=code'
-              '&client_id=identifier'
-              '&redirect_uri=http%3A%2F%2Fexample.com%2Fredirect'
-              '&state=state'));
+          allOf([
+            startsWith('https://example.com/authorization?response_type=code'),
+            contains('&client_id=identifier'),
+            contains('&redirect_uri=http%3A%2F%2Fexample.com%2Fredirect'),
+            matches(r'&code_challenge=[A-Za-z0-9\+\/\-\_]{43}'),
+            contains('&code_challenge_method=S256'),
+            contains('&state=state')
+          ]));
     });
 
     test('merges with existing query parameters', () {
@@ -90,11 +102,14 @@
       var authorizationUrl = grant.getAuthorizationUrl(redirectUrl);
       expect(
           authorizationUrl.toString(),
-          equals('https://example.com/authorization'
-              '?query=value'
-              '&response_type=code'
-              '&client_id=identifier'
-              '&redirect_uri=http%3A%2F%2Fexample.com%2Fredirect'));
+          allOf([
+            startsWith('https://example.com/authorization?query=value'),
+            contains('&response_type=code'),
+            contains('&client_id=identifier'),
+            contains('&redirect_uri=http%3A%2F%2Fexample.com%2Fredirect'),
+            matches(r'&code_challenge=[A-Za-z0-9\+\/\-\_]{43}'),
+            contains('&code_challenge_method=S256'),
+          ]));
     });
 
     test("can't be called twice", () {
@@ -148,11 +163,13 @@
         expect(request.url.toString(), equals(grant.tokenEndpoint.toString()));
         expect(
             request.bodyFields,
-            equals({
-              'grant_type': 'authorization_code',
-              'code': 'auth code',
-              'redirect_uri': redirectUrl.toString()
-            }));
+            allOf([
+              containsPair('grant_type', 'authorization_code'),
+              containsPair('code', 'auth code'),
+              containsPair('redirect_uri', redirectUrl.toString()),
+              containsPair(
+                  'code_verifier', matches(r'[A-Za-z0-9\-\.\_\~]{128}'))
+            ]));
         expect(request.headers,
             containsPair("Authorization", "Basic aWRlbnRpZmllcjpzZWNyZXQ="));
 
@@ -190,11 +207,13 @@
         expect(request.url.toString(), equals(grant.tokenEndpoint.toString()));
         expect(
             request.bodyFields,
-            equals({
-              'grant_type': 'authorization_code',
-              'code': 'auth code',
-              'redirect_uri': redirectUrl.toString()
-            }));
+            allOf([
+              containsPair('grant_type', 'authorization_code'),
+              containsPair('code', 'auth code'),
+              containsPair('redirect_uri', redirectUrl.toString()),
+              containsPair(
+                  'code_verifier', matches(r'[A-Za-z0-9\-\.\_\~]{128}'))
+            ]));
         expect(request.headers,
             containsPair("Authorization", "Basic aWRlbnRpZmllcjpzZWNyZXQ="));
 
@@ -235,13 +254,15 @@
         expect(request.url.toString(), equals(grant.tokenEndpoint.toString()));
         expect(
             request.bodyFields,
-            equals({
-              'grant_type': 'authorization_code',
-              'code': 'auth code',
-              'redirect_uri': redirectUrl.toString(),
-              'client_id': 'identifier',
-              'client_secret': 'secret'
-            }));
+            allOf([
+              containsPair('grant_type', 'authorization_code'),
+              containsPair('code', 'auth code'),
+              containsPair('redirect_uri', redirectUrl.toString()),
+              containsPair(
+                  'code_verifier', matches(r'[A-Za-z0-9\-\.\_\~]{128}')),
+              containsPair('client_id', 'identifier'),
+              containsPair('client_secret', 'secret')
+            ]));
 
         return new Future.value(new http.Response(
             jsonEncode({
@@ -265,13 +286,15 @@
         expect(request.url.toString(), equals(grant.tokenEndpoint.toString()));
         expect(
             request.bodyFields,
-            equals({
-              'grant_type': 'authorization_code',
-              'code': 'auth code',
-              'redirect_uri': redirectUrl.toString(),
-              'client_id': 'identifier',
-              'client_secret': 'secret'
-            }));
+            allOf([
+              containsPair('grant_type', 'authorization_code'),
+              containsPair('code', 'auth code'),
+              containsPair('redirect_uri', redirectUrl.toString()),
+              containsPair(
+                  'code_verifier', matches(r'[A-Za-z0-9\-\.\_\~]{128}')),
+              containsPair('client_id', 'identifier'),
+              containsPair('client_secret', 'secret')
+            ]));
 
         return new Future.value(new http.Response(
             jsonEncode({