allow code verifier to be passed in constructor of AuthorizationCodeGrant (#92)

* allow code verifier to be passed in constructor

* update changelog and bump pubspec.yaml version

* Update lib/src/authorization_code_grant.dart

update docs for codeVerifier

Co-authored-by: Jonas Finnemann Jensen <jopsen@gmail.com>

Co-authored-by: Joseph Lejtman <jlejtman@clarityinnovates.com>
Co-authored-by: Jonas Finnemann Jensen <jopsen@gmail.com>
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 28cc6e4..3bb8c5a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+# 1.6.3
+
+* Added optional `codeVerifier` parameter to `AuthorizationCodeGrant` constructor.
+
 # 1.6.2-dev
 
 # 1.6.1
diff --git a/lib/src/authorization_code_grant.dart b/lib/src/authorization_code_grant.dart
index c04bc0b..963e5b2 100644
--- a/lib/src/authorization_code_grant.dart
+++ b/lib/src/authorization_code_grant.dart
@@ -107,8 +107,8 @@
   static const String _charset =
       'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
 
-  /// The generated PKCE code verifier
-  String _codeVerifier;
+  /// The PKCE code verifier. Will be generated if one is not provided in the constructor.
+  final String _codeVerifier;
 
   /// Creates a new grant.
   ///
@@ -126,6 +126,12 @@
   /// [onCredentialsRefreshed] will be called by the constructed [Client]
   /// whenever the credentials are refreshed.
   ///
+  /// [codeVerifier] String to be used as PKCE code verifier. If none is provided a
+  /// random codeVerifier will be generated.
+  /// The codeVerifier must meet requirements specified in [RFC 7636].
+  ///
+  /// [RFC 7636]: https://tools.ietf.org/html/rfc7636#section-4.1
+  ///
   /// The scope strings will be separated by the provided [delimiter]. This
   /// defaults to `" "`, the OAuth2 standard, but some APIs (such as Facebook's)
   /// use non-standard delimiters.
@@ -147,12 +153,14 @@
       http.Client httpClient,
       CredentialsRefreshedCallback onCredentialsRefreshed,
       Map<String, dynamic> Function(MediaType contentType, String body)
-          getParameters})
+          getParameters,
+      String codeVerifier})
       : _basicAuth = basicAuth,
         _httpClient = httpClient ?? http.Client(),
         _delimiter = delimiter ?? ' ',
         _getParameters = getParameters ?? parseJsonParameters,
-        _onCredentialsRefreshed = onCredentialsRefreshed;
+        _onCredentialsRefreshed = onCredentialsRefreshed,
+        _codeVerifier = codeVerifier ?? _createCodeVerifier();
 
   /// Returns the URL to which the resource owner should be redirected to
   /// authorize this client.
@@ -186,7 +194,6 @@
       scopes = scopes.toList();
     }
 
-    _codeVerifier = _createCodeVerifier();
     var codeChallenge = base64Url
         .encode(sha256.convert(ascii.encode(_codeVerifier)).bytes)
         .replaceAll('=', '');
diff --git a/pubspec.yaml b/pubspec.yaml
index 856aac8..3fad972 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: oauth2
-version: 1.6.2-dev
+version: 1.6.3
 homepage: https://github.com/dart-lang/oauth2
 description: >-
   A client library for authenticating with a remote service via OAuth2 on
diff --git a/test/authorization_code_grant_test.dart b/test/authorization_code_grant_test.dart
index 4909bf5..f74c061 100644
--- a/test/authorization_code_grant_test.dart
+++ b/test/authorization_code_grant_test.dart
@@ -54,6 +54,29 @@
           ]));
     });
 
+    test('builds the correct URL with passed in code verifier', () {
+      final codeVerifier =
+          'it1shei7LooGoh3looxaa4sieveijeib2zecauz2oo8aebae5aehee0ahPirewoh5Bo6Maexooqui3uL2si6ahweiv7shauc1shahxooveoB3aeyahsaiye0Egh3raix';
+      final expectedCodeChallenge =
+          'EjfFMv8TFPd3GuNxAn5COhlWBGpfZLimHett7ypJfJ0';
+      var grant = oauth2.AuthorizationCodeGrant(
+          'identifier',
+          Uri.parse('https://example.com/authorization'),
+          Uri.parse('https://example.com/token'),
+          secret: 'secret',
+          httpClient: client,
+          codeVerifier: codeVerifier);
+      expect(
+          grant.getAuthorizationUrl(redirectUrl).toString(),
+          allOf([
+            startsWith('https://example.com/authorization?response_type=code'),
+            contains('&client_id=identifier'),
+            contains('&redirect_uri=http%3A%2F%2Fexample.com%2Fredirect'),
+            contains('&code_challenge=$expectedCodeChallenge'),
+            contains('&code_challenge_method=S256')
+          ]));
+    });
+
     test('separates scopes with the correct delimiter', () {
       var grant = oauth2.AuthorizationCodeGrant(
           'identifier',