Code review changes

BUG=

Review URL: https://codereview.chromium.org//1304363004 .
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fc869c9..686b6ac 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,35 @@
+# 1.0.0
+
+## Breaking changes
+
+* Requests that use client authentication, such as the
+  `AuthorizationCodeGrant`'s access token request and `Credentials`' refresh
+  request, now use HTTP Basic authentication by default. This form of
+  authentication is strongly recommended by the OAuth 2.0 spec. The new
+  `basicAuth` parameter may be set to `false` to force form-based authentication
+  for servers that require it.
+
+* `new AuthorizationCodeGrant()` now takes `secret` as an optional named
+  argument rather than a required argument. This matches the OAuth 2.0 spec,
+  which says that a client secret is only required for confidential clients.
+
+* `new Client()` and `Credentials.refresh()` now take both `identifier` and
+  `secret` as optional named arguments rather than required arguments. This
+  matches the OAuth 2.0 spec, which says that the server may choose not to
+  require client authentication for some flows.
+
+* `new Credentials()` now takes named arguments rather than optional positional
+  arguments.
+
+## Non-breaking changes
+
+* The `scopes` argument to `AuthorizationCodeGrant.getAuthorizationUrl()` and
+  `new Credentials()` and the `newScopes` argument to `Credentials.refresh` now
+  take an `Iterable` rather than just a `List`.
+
+* The `scopes` argument to `AuthorizationCodeGrant.getAuthorizationUrl()` now
+  defaults to `null` rather than `const []`.
+
 # 0.9.3
 
 * Update the `http` dependency.
diff --git a/README.md b/README.md
index 23be5ff..7e390cd 100644
--- a/README.md
+++ b/README.md
@@ -64,13 +64,15 @@
   if (exists) {
     var credentials = new oauth2.Credentials.fromJson(
         await credentialsFile.readAsString());
-    return new oauth2.Client(identifier, secret, credentials);
+    return new oauth2.Client(credentials,
+        identifier: identifier, secret: secret);
   }
 
   // If we don't have OAuth2 credentials yet, we need to get the resource owner
   // to authorize us. We're assuming here that we're a command-line application.
   var grant = new oauth2.AuthorizationCodeGrant(
-      identifier, secret, authorizationEndpoint, tokenEndpoint);
+      identifier, authorizationEndpoint, tokenEndpoint,
+      secret: secret);
 
   // Redirect the resource owner to the authorization URL. This will be a URL on
   // the authorization server (authorizationEndpoint with some additional query
diff --git a/lib/src/authorization_code_grant.dart b/lib/src/authorization_code_grant.dart
index e3bb645..7a73d44 100644
--- a/lib/src/authorization_code_grant.dart
+++ b/lib/src/authorization_code_grant.dart
@@ -68,6 +68,9 @@
   /// documentation.
   final Uri tokenEndpoint;
 
+  /// Whether to use HTTP Basic authentication for authorizing the client.
+  final bool _basicAuth;
+
   /// The HTTP client used to make HTTP requests.
   http.Client _httpClient;
 
@@ -87,15 +90,23 @@
 
   /// Creates a new grant.
   ///
+  /// If [basicAuth] is `true` (the default), the client credentials are sent to
+  /// the server using using HTTP Basic authentication as defined in [RFC 2617].
+  /// Otherwise, they're included in the request body. Note that the latter form
+  /// is not recommended by the OAuth 2.0 spec, and should only be used if the
+  /// server doesn't support Basic authentication.
+  ///
+  /// [RFC 2617]: https://tools.ietf.org/html/rfc2617
+  ///
   /// [httpClient] is used for all HTTP requests made by this grant, as well as
   /// those of the [Client] is constructs.
   AuthorizationCodeGrant(
           this.identifier,
-          this.secret,
           this.authorizationEndpoint,
           this.tokenEndpoint,
-          {http.Client httpClient})
-      : _httpClient = httpClient == null ? new http.Client() : httpClient;
+          {this.secret, bool basicAuth: true, http.Client httpClient})
+      : _basicAuth = basicAuth,
+        _httpClient = httpClient == null ? new http.Client() : httpClient;
 
   /// Returns the URL to which the resource owner should be redirected to
   /// authorize this client.
@@ -116,13 +127,19 @@
   /// query parameters provided to the redirect URL.
   ///
   /// It is a [StateError] to call this more than once.
-  Uri getAuthorizationUrl(Uri redirect,
-      {List<String> scopes: const <String>[], String state}) {
+  Uri getAuthorizationUrl(Uri redirect, {Iterable<String> scopes,
+      String state}) {
     if (_state != _State.initial) {
       throw new StateError('The authorization URL has already been generated.');
     }
     _state = _State.awaitingResponse;
 
+    if (scopes == null) {
+      scopes = [];
+    } else {
+      scopes = scopes.toList();
+    }
+
     this._redirectEndpoint = redirect;
     this._scopes = scopes;
     this._stateString = state;
@@ -224,21 +241,35 @@
   /// the state beforehand.
   Future<Client> _handleAuthorizationCode(String authorizationCode) async {
     var startTime = new DateTime.now();
-    var response = await _httpClient.post(this.tokenEndpoint, body: {
+
+    var headers = {};
+
+    var body = {
       "grant_type": "authorization_code",
       "code": authorizationCode,
-      "redirect_uri": this._redirectEndpoint.toString(),
-      // TODO(nweiz): the spec recommends that HTTP basic auth be used in
-      // preference to form parameters, but Google doesn't support that. Should
-      // it be configurable?
-      "client_id": this.identifier,
-      "client_secret": this.secret
-    });
+      "redirect_uri": this._redirectEndpoint.toString()
+    };
+
+    if (_basicAuth && secret != null) {
+      headers["Authorization"] = basicAuthHeader(identifier, secret);
+    } else {
+      // The ID is required for this request any time basic auth isn't being
+      // used, even if there's no actual client authentication to be done.
+      body["client_id"] = identifier;
+      if (secret != null) body["client_secret"] = secret;
+    }
+
+    var response = await _httpClient.post(this.tokenEndpoint,
+        headers: headers, body: body);
 
     var credentials = handleAccessTokenResponse(
         response, tokenEndpoint, startTime, _scopes);
     return new Client(
-        this.identifier, this.secret, credentials, httpClient: _httpClient);
+        credentials,
+        identifier: this.identifier,
+        secret: this.secret,
+        basicAuth: _basicAuth,
+        httpClient: _httpClient);
   }
 
   /// Closes the grant and frees its resources.
diff --git a/lib/src/client.dart b/lib/src/client.dart
index 371eed3..adf53d5 100644
--- a/lib/src/client.dart
+++ b/lib/src/client.dart
@@ -7,6 +7,7 @@
 import 'dart:async';
 
 import 'package:http/http.dart' as http;
+import 'package:http_parser/http_parser.dart';
 
 import 'authorization_exception.dart';
 import 'credentials.dart';
@@ -69,6 +70,9 @@
   Credentials get credentials => _credentials;
   Credentials _credentials;
 
+  /// Whether to use HTTP Basic authentication for authorizing the client.
+  final bool _basicAuth;
+
   /// The underlying HTTP client.
   http.Client _httpClient;
 
@@ -79,12 +83,16 @@
   ///
   /// [httpClient] is the underlying client that this forwards requests to after
   /// adding authorization credentials to them.
-  Client(
-      this.identifier,
-      this.secret,
-      this._credentials,
-      {http.Client httpClient})
-    : _httpClient = httpClient == null ? new http.Client() : httpClient;
+  ///
+  /// Throws an [ArgumentError] if [secret] is passed without [identifier].
+  Client(this._credentials, {this.identifier, this.secret,
+          bool basicAuth: true, http.Client httpClient})
+      : _basicAuth = basicAuth,
+        _httpClient = httpClient == null ? new http.Client() : httpClient {
+    if (identifier == null && secret != null) {
+      throw new ArgumentError("secret may not be passed without identifier.");
+    }
+  }
 
   /// Sends an HTTP request with OAuth2 authorization credentials attached.
   ///
@@ -102,17 +110,19 @@
     if (response.statusCode != 401) return response;
     if (!response.headers.containsKey('www-authenticate')) return response;
 
-    var authenticate;
+    var challenges;
     try {
-      authenticate = new AuthenticateHeader.parse(
+      challenges = AuthenticationChallenge.parseHeader(
           response.headers['www-authenticate']);
     } on FormatException catch (_) {
       return response;
     }
 
-    if (authenticate.scheme != 'bearer') return response;
+    var challenge = challenges.firstWhere(
+        (challenge) => challenge.scheme == 'bearer', orElse: () => null);
+    if (challenge == null) return response;
 
-    var params = authenticate.parameters;
+    var params = challenge.parameters;
     if (!params.containsKey('error')) return response;
 
     throw new AuthorizationException(
@@ -137,8 +147,11 @@
     }
 
     _credentials = await credentials.refresh(
-        identifier, secret,
-        newScopes: newScopes, httpClient: _httpClient);
+        identifier: identifier,
+        secret: secret,
+        newScopes: newScopes,
+        basicAuth: _basicAuth,
+        httpClient: _httpClient);
 
     return this;
   }
diff --git a/lib/src/credentials.dart b/lib/src/credentials.dart
index 37e7114..0a12f3f 100644
--- a/lib/src/credentials.dart
+++ b/lib/src/credentials.dart
@@ -5,11 +5,13 @@
 library oauth2.credentials;
 
 import 'dart:async';
+import 'dart:collection';
 import 'dart:convert';
 
 import 'package:http/http.dart' as http;
 
 import 'handle_access_token_response.dart';
+import 'utils.dart';
 
 /// Credentials that prove that a client is allowed to access a resource on the
 /// resource owner's behalf.
@@ -72,11 +74,15 @@
   /// [AuthorizationCodeGrant]. Alternately, it may be loaded from a serialized
   /// form via [Credentials.fromJson].
   Credentials(
-      this.accessToken,
-      [this.refreshToken,
-       this.tokenEndpoint,
-       this.scopes,
-       this.expiration]);
+          this.accessToken,
+          {this.refreshToken,
+          this.tokenEndpoint,
+          Iterable<String> scopes,
+          this.expiration})
+      : scopes = new UnmodifiableListView(
+            // Explicitly type-annotate the list literal to work around
+            // sdk#24202.
+            scopes == null ? <String>[] : scopes.toList());
 
   /// Loads a set of credentials from a JSON-serialized form.
   ///
@@ -126,10 +132,10 @@
 
     return new Credentials(
         parsed['accessToken'],
-        parsed['refreshToken'],
-        tokenEndpoint,
-        scopes,
-        expiration);
+        refreshToken: parsed['refreshToken'],
+        tokenEndpoint: tokenEndpoint,
+        scopes: scopes,
+        expiration: expiration);
   }
 
   /// Serializes a set of credentials to JSON.
@@ -152,19 +158,25 @@
   /// You may request different scopes than the default by passing in
   /// [newScopes]. These must be a subset of [scopes].
   ///
-  /// This will throw a [StateError] if these credentials can't be refreshed, an
+  /// This throws an [ArgumentError] if [secret] is passed without [identifier],
+  /// a [StateError] if these credentials can't be refreshed, an
   /// [AuthorizationException] if refreshing the credentials fails, or a
   /// [FormatError] if the authorization server returns invalid responses.
   Future<Credentials> refresh(
-      String identifier,
+      {String identifier,
       String secret,
-      {List<String> newScopes,
-       http.Client httpClient}) async {
+      Iterable<String> newScopes,
+      bool basicAuth: true,
+      http.Client httpClient}) async {
     var scopes = this.scopes;
-    if (newScopes != null) scopes = newScopes;
-    if (scopes == null) scopes = <String>[];
+    if (newScopes != null) scopes = newScopes.toList();
+    if (scopes == null) scopes = [];
     if (httpClient == null) httpClient = new http.Client();
 
+    if (identifier == null && secret != null) {
+      throw new ArgumentError("secret may not be passed without identifier.");
+    }
+
     var startTime = new DateTime.now();
     if (refreshToken == null) {
       throw new StateError("Can't refresh credentials without a refresh "
@@ -174,29 +186,34 @@
           "endpoint.");
     }
 
-    var fields = {
-      "grant_type": "refresh_token",
-      "refresh_token": refreshToken,
-      // TODO(nweiz): the spec recommends that HTTP basic auth be used in
-      // preference to form parameters, but Google doesn't support that.
-      // Should it be configurable?
-      "client_id": identifier,
-      "client_secret": secret
-    };
-    if (!scopes.isEmpty) fields["scope"] = scopes.join(' ');
+    var headers = {};
 
-    var response = await httpClient.post(tokenEndpoint, body: fields);
+    var body = {
+      "grant_type": "refresh_token",
+      "refresh_token": refreshToken
+    };
+    if (!scopes.isEmpty) body["scope"] = scopes.join(' ');
+
+    if (basicAuth && secret != null) {
+      headers["Authorization"] = basicAuthHeader(identifier, secret);
+    } else {
+      if (identifier != null) body["client_id"] = identifier;
+      if (secret != null) body["client_secret"] = secret;
+    }
+
+    var response = await httpClient.post(tokenEndpoint,
+        headers: headers, body: body);
     var credentials = await handleAccessTokenResponse(
-          response, tokenEndpoint, startTime, scopes);
+        response, tokenEndpoint, startTime, scopes);
 
     // The authorization server may issue a new refresh token. If it doesn't,
     // we should re-use the one we already have.
     if (credentials.refreshToken != null) return credentials;
     return new Credentials(
         credentials.accessToken,
-        this.refreshToken,
-        credentials.tokenEndpoint,
-        credentials.scopes,
-        credentials.expiration);
+        refreshToken: this.refreshToken,
+        tokenEndpoint: credentials.tokenEndpoint,
+        scopes: credentials.scopes,
+        expiration: credentials.expiration);
   }
 }
diff --git a/lib/src/handle_access_token_response.dart b/lib/src/handle_access_token_response.dart
index 0065f0c..ef9b198 100644
--- a/lib/src/handle_access_token_response.dart
+++ b/lib/src/handle_access_token_response.dart
@@ -81,10 +81,10 @@
 
   return new Credentials(
       parameters['access_token'],
-      parameters['refresh_token'],
-      tokenEndpoint,
-      scopes,
-      expiration);
+      refreshToken: parameters['refresh_token'],
+      tokenEndpoint: tokenEndpoint,
+      scopes: scopes,
+      expiration: expiration);
 }
 
 /// Throws the appropriate exception for an error response from the
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
index 734c58e..bf260eb 100644
--- a/lib/src/utils.dart
+++ b/lib/src/utils.dart
@@ -4,65 +4,16 @@
 
 library oauth2.utils;
 
+import 'dart:convert';
+
+import 'package:crypto/crypto.dart';
+
 /// Adds additional query parameters to [url], overwriting the original
 /// parameters if a name conflict occurs.
 Uri addQueryParameters(Uri url, Map<String, String> parameters) => url.replace(
     queryParameters: new Map.from(url.queryParameters)..addAll(parameters));
 
-/// Like [String.split], but only splits on the first occurrence of the pattern.
-///
-/// This will always return a list of two elements or fewer.
-List<String> split1(String toSplit, String pattern) {
-  if (toSplit.isEmpty) return [];
-
-  var index = toSplit.indexOf(pattern);
-  if (index == -1) return [toSplit];
-  return [toSplit.substring(0, index),
-      toSplit.substring(index + pattern.length)];
-}
-
-/// A WWW-Authenticate header value, parsed as per [RFC 2617][].
-///
-/// [RFC 2617]: http://tools.ietf.org/html/rfc2617
-class AuthenticateHeader {
-  final String scheme;
-  final Map<String, String> parameters;
-
-  AuthenticateHeader(this.scheme, this.parameters);
-
-  /// Parses a header string. Throws a [FormatException] if the header is
-  /// invalid.
-  factory AuthenticateHeader.parse(String header) {
-    var split = split1(header, ' ');
-    if (split.length == 0) {
-      throw new FormatException('Invalid WWW-Authenticate header: "$header"');
-    } else if (split.length == 1 || split[1].trim().isEmpty) {
-      return new AuthenticateHeader(split[0].toLowerCase(), {});
-    }
-    var scheme = split[0].toLowerCase();
-    var paramString = split[1];
-
-    // From http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html.
-    var tokenChar = r'[^\0-\x1F()<>@,;:\\"/\[\]?={} \t\x7F]';
-    var quotedStringChar = r'(?:[^\0-\x1F\x7F"]|\\.)';
-    var regexp = new RegExp('^ *($tokenChar+)="($quotedStringChar*)" *(, *)?');
-
-    var parameters = {};
-    var match;
-    do {
-      match = regexp.firstMatch(paramString);
-      if (match == null) {
-        throw new FormatException('Invalid WWW-Authenticate header: "$header"');
-      }
-
-      paramString = paramString.substring(match.end);
-      parameters[match.group(1).toLowerCase()] = match.group(2);
-    } while (match.group(3) != null);
-
-    if (!paramString.trim().isEmpty) {
-      throw new FormatException('Invalid WWW-Authenticate header: "$header"');
-    }
-
-    return new AuthenticateHeader(scheme, parameters);
-  }
+String basicAuthHeader(String identifier, String secret) {
+  var userPass = Uri.encodeFull(identifier) + ":" + Uri.encodeFull(secret);
+  return "Basic " + CryptoUtils.bytesToBase64(ASCII.encode(userPass));
 }
diff --git a/pubspec.yaml b/pubspec.yaml
index e40265a..f52b8be 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: oauth2
-version: 0.9.4-dev
+version: 1.0.0-dev
 author: Dart Team <misc@dartlang.org>
 homepage: http://github.com/dart-lang/oauth2
 description: >
@@ -10,6 +10,6 @@
   sdk: '>=1.9.0 <2.0.0'
 dependencies:
   http: '>=0.11.0 <0.12.0'
-  http_parser: '>=0.0.0 <0.1.0'
+  http_parser: '^1.0.0'
 dev_dependencies:
   test: '>=0.12.0 <0.13.0'
diff --git a/test/authorization_code_grant_test.dart b/test/authorization_code_grant_test.dart
index deaad5c..e03a607 100644
--- a/test/authorization_code_grant_test.dart
+++ b/test/authorization_code_grant_test.dart
@@ -20,9 +20,9 @@
     client = new ExpectClient();
     grant = new oauth2.AuthorizationCodeGrant(
         'identifier',
-        'secret',
         Uri.parse('https://example.com/authorization'),
         Uri.parse('https://example.com/token'),
+        secret: 'secret',
         httpClient: client);
   });
 
@@ -60,9 +60,9 @@
     test('merges with existing query parameters', () {
       grant = new oauth2.AuthorizationCodeGrant(
           'identifier',
-          'secret',
           Uri.parse('https://example.com/authorization?query=value'),
           Uri.parse('https://example.com/token'),
+          secret: 'secret',
           httpClient: client);
 
       var authorizationUrl = grant.getAuthorizationUrl(redirectUrl);
@@ -127,10 +127,11 @@
         expect(request.bodyFields, equals({
           'grant_type': 'authorization_code',
           'code': 'auth code',
-          'redirect_uri': redirectUrl.toString(),
-          'client_id': 'identifier',
-          'client_secret': 'secret'
+          'redirect_uri': redirectUrl.toString()
         }));
+        expect(request.headers, containsPair(
+            "Authorization",
+            "Basic aWRlbnRpZmllcjpzZWNyZXQ="));
 
         return new Future.value(new http.Response(JSON.encode({
           'access_token': 'access token',
@@ -163,6 +164,71 @@
         expect(request.bodyFields, equals({
           'grant_type': 'authorization_code',
           'code': 'auth code',
+          'redirect_uri': redirectUrl.toString()
+        }));
+        expect(request.headers, containsPair(
+            "Authorization",
+            "Basic aWRlbnRpZmllcjpzZWNyZXQ="));
+
+        return new Future.value(new http.Response(JSON.encode({
+          'access_token': 'access token',
+          'token_type': 'bearer',
+        }), 200, headers: {'content-type': 'application/json'}));
+      });
+
+      expect(grant.handleAuthorizationCode('auth code'),
+          completion(predicate((client) {
+            expect(client.credentials.accessToken, equals('access token'));
+            return true;
+          })));
+    });
+  });
+
+  group("with basicAuth: false", () {
+    setUp(() {
+      client = new ExpectClient();
+      grant = new oauth2.AuthorizationCodeGrant(
+          'identifier',
+          Uri.parse('https://example.com/authorization'),
+          Uri.parse('https://example.com/token'),
+          secret: 'secret',
+          basicAuth: false,
+          httpClient: client);
+    });
+
+    test('.handleAuthorizationResponse sends an authorization code request',
+        () {
+      grant.getAuthorizationUrl(redirectUrl);
+      client.expectRequest((request) {
+        expect(request.method, equals('POST'));
+        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'
+        }));
+
+        return new Future.value(new http.Response(JSON.encode({
+          'access_token': 'access token',
+          'token_type': 'bearer',
+        }), 200, headers: {'content-type': 'application/json'}));
+      });
+
+      expect(grant.handleAuthorizationResponse({'code': 'auth code'})
+            .then((client) => client.credentials.accessToken),
+          completion(equals('access token')));
+    });
+
+    test('.handleAuthorizationCode sends an authorization code request', () {
+      grant.getAuthorizationUrl(redirectUrl);
+      client.expectRequest((request) {
+        expect(request.method, equals('POST'));
+        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'
diff --git a/test/client_test.dart b/test/client_test.dart
index 969787a..7dc5afd 100644
--- a/test/client_test.dart
+++ b/test/client_test.dart
@@ -23,8 +23,10 @@
     test("that can't be refreshed throws an ExpirationException on send", () {
       var expiration = new DateTime.now().subtract(new Duration(hours: 1));
       var credentials = new oauth2.Credentials(
-          'access token', null, null, [], expiration);
-      var client = new oauth2.Client('identifier', 'secret', credentials,
+          'access token', expiration: expiration);
+      var client = new oauth2.Client(credentials,
+          identifier: 'identifier',
+          secret: 'secret', 
           httpClient: httpClient);
 
       expect(client.get(requestUri),
@@ -35,8 +37,13 @@
         "request", () async {
       var expiration = new DateTime.now().subtract(new Duration(hours: 1));
       var credentials = new oauth2.Credentials(
-          'access token', 'refresh token', tokenEndpoint, [], expiration);
-      var client = new oauth2.Client('identifier', 'secret', credentials,
+          'access token',
+          refreshToken: 'refresh token',
+          tokenEndpoint: tokenEndpoint,
+          expiration: expiration);
+      var client = new oauth2.Client(credentials,
+          identifier: 'identifier',
+          secret: 'secret',
           httpClient: httpClient);
 
       httpClient.expectRequest((request) {
@@ -65,7 +72,9 @@
   group('with valid credentials', () {
     test("sends a request with bearer authorization", () {
       var credentials = new oauth2.Credentials('access token');
-      var client = new oauth2.Client('identifier', 'secret', credentials,
+      var client = new oauth2.Client(credentials,
+          identifier: 'identifier',
+          secret: 'secret',
           httpClient: httpClient);
 
       httpClient.expectRequest((request) {
@@ -81,8 +90,12 @@
 
     test("can manually refresh the credentials", () async {
       var credentials = new oauth2.Credentials(
-          'access token', 'refresh token', tokenEndpoint);
-      var client = new oauth2.Client('identifier', 'secret', credentials,
+          'access token',
+          refreshToken: 'refresh token',
+          tokenEndpoint: tokenEndpoint);
+      var client = new oauth2.Client(credentials,
+          identifier: 'identifier',
+          secret: 'secret',
           httpClient: httpClient);
 
       httpClient.expectRequest((request) {
@@ -100,7 +113,9 @@
 
     test("without a refresh token can't manually refresh the credentials", () {
       var credentials = new oauth2.Credentials('access token');
-      var client = new oauth2.Client('identifier', 'secret', credentials,
+      var client = new oauth2.Client(credentials,
+          identifier: 'identifier',
+          secret: 'secret',
           httpClient: httpClient);
 
       expect(client.refreshCredentials(), throwsA(isStateError));
@@ -110,7 +125,9 @@
   group('with invalid credentials', () {
     test('throws an AuthorizationException for a 401 response', () {
       var credentials = new oauth2.Credentials('access token');
-      var client = new oauth2.Client('identifier', 'secret', credentials,
+      var client = new oauth2.Client(credentials,
+          identifier: 'identifier',
+          secret: 'secret',
           httpClient: httpClient);
 
       httpClient.expectRequest((request) {
@@ -130,7 +147,9 @@
 
     test('passes through a 401 response without www-authenticate', () async {
       var credentials = new oauth2.Credentials('access token');
-      var client = new oauth2.Client('identifier', 'secret', credentials,
+      var client = new oauth2.Client(credentials,
+          identifier: 'identifier',
+          secret: 'secret',
           httpClient: httpClient);
 
       httpClient.expectRequest((request) {
@@ -148,7 +167,9 @@
     test('passes through a 401 response with invalid www-authenticate',
         () async {
       var credentials = new oauth2.Credentials('access token');
-      var client = new oauth2.Client('identifier', 'secret', credentials,
+      var client = new oauth2.Client(credentials,
+          identifier: 'identifier',
+          secret: 'secret',
           httpClient: httpClient);
 
       httpClient.expectRequest((request) {
@@ -157,8 +178,8 @@
         expect(request.headers['authorization'],
             equals('Bearer access token'));
 
-        var authenticate = 'Bearer error="invalid_token", error_description='
-          '"Something is terribly wrong.", ';
+        var authenticate = 'Bearer error="invalid_token" error_description='
+          '"Something is terribly wrong."';
         return new Future.value(new http.Response('bad job', 401,
                 headers: {'www-authenticate': authenticate}));
       });
@@ -169,7 +190,9 @@
     test('passes through a 401 response with non-bearer www-authenticate',
         () async {
       var credentials = new oauth2.Credentials('access token');
-      var client = new oauth2.Client('identifier', 'secret', credentials,
+      var client = new oauth2.Client(credentials,
+          identifier: 'identifier',
+          secret: 'secret',
           httpClient: httpClient);
 
       httpClient.expectRequest((request) {
@@ -188,7 +211,9 @@
     test('passes through a 401 response with non-OAuth2 www-authenticate',
         () async {
       var credentials = new oauth2.Credentials('access token');
-      var client = new oauth2.Client('identifier', 'secret', credentials,
+      var client = new oauth2.Client(credentials,
+          identifier: 'identifier',
+          secret: 'secret',
           httpClient: httpClient);
 
       httpClient.expectRequest((request) {
diff --git a/test/credentials_test.dart b/test/credentials_test.dart
index d72614f..f4d8886 100644
--- a/test/credentials_test.dart
+++ b/test/credentials_test.dart
@@ -25,37 +25,82 @@
   test('is not expired if the expiration is in the future', () {
     var expiration = new DateTime.now().add(new Duration(hours: 1));
     var credentials = new oauth2.Credentials(
-        'access token', null, null, null, expiration);
+        'access token', expiration: expiration);
     expect(credentials.isExpired, isFalse);
   });
 
   test('is expired if the expiration is in the past', () {
     var expiration = new DateTime.now().subtract(new Duration(hours: 1));
     var credentials = new oauth2.Credentials(
-        'access token', null, null, null, expiration);
+        'access token', expiration: expiration);
     expect(credentials.isExpired, isTrue);
   });
 
   test("can't refresh without a refresh token", () {
     var credentials = new oauth2.Credentials(
-        'access token', null, tokenEndpoint);
+        'access token', tokenEndpoint: tokenEndpoint);
     expect(credentials.canRefresh, false);
 
-    expect(credentials.refresh('identifier', 'secret', httpClient: httpClient),
+    expect(credentials.refresh(
+            identifier: 'identifier',
+            secret: 'secret',
+            httpClient: httpClient),
         throwsStateError);
   });
 
   test("can't refresh without a token endpoint", () {
-    var credentials = new oauth2.Credentials('access token', 'refresh token');
+    var credentials = new oauth2.Credentials(
+        'access token', refreshToken: 'refresh token');
     expect(credentials.canRefresh, false);
 
-    expect(credentials.refresh('identifier', 'secret', httpClient: httpClient),
+    expect(credentials.refresh(
+            identifier: 'identifier',
+            secret: 'secret',
+            httpClient: httpClient),
         throwsStateError);
   });
 
   test("can refresh with a refresh token and a token endpoint", () async {
     var credentials = new oauth2.Credentials(
-        'access token', 'refresh token', tokenEndpoint, ['scope1', 'scope2']);
+        'access token',
+        refreshToken: 'refresh token',
+        tokenEndpoint: tokenEndpoint,
+        scopes: ['scope1', 'scope2']);
+    expect(credentials.canRefresh, true);
+
+    httpClient.expectRequest((request) {
+      expect(request.method, equals('POST'));
+      expect(request.url.toString(), equals(tokenEndpoint.toString()));
+      expect(request.bodyFields, equals({
+        "grant_type": "refresh_token",
+        "refresh_token": "refresh token",
+        "scope": "scope1 scope2"
+      }));
+      expect(request.headers, containsPair(
+          "Authorization",
+          "Basic aWQlQzMlQUJudCVDNCVBQmZpZXI6cyVDMyVBQmNyZXQ="));
+
+      return new Future.value(new http.Response(JSON.encode({
+        'access_token': 'new access token',
+        'token_type': 'bearer',
+        'refresh_token': 'new refresh token'
+      }), 200, headers: {'content-type': 'application/json'}));
+    });
+
+    credentials = await credentials.refresh(
+        identifier: 'idëntīfier',
+        secret: 'sëcret',
+        httpClient: httpClient);
+    expect(credentials.accessToken, equals('new access token'));
+    expect(credentials.refreshToken, equals('new refresh token'));
+  });
+
+  test("can refresh without a client secret", () async {
+    var credentials = new oauth2.Credentials(
+        'access token',
+        refreshToken: 'refresh token',
+        tokenEndpoint: tokenEndpoint,
+        scopes: ['scope1', 'scope2']);
     expect(credentials.canRefresh, true);
 
     httpClient.expectRequest((request) {
@@ -65,8 +110,7 @@
         "grant_type": "refresh_token",
         "refresh_token": "refresh token",
         "scope": "scope1 scope2",
-        "client_id": "identifier",
-        "client_secret": "secret"
+        "client_id": "identifier"
       }));
 
       return new Future.value(new http.Response(JSON.encode({
@@ -77,15 +121,19 @@
     });
 
 
-    credentials = await credentials.refresh('identifier', 'secret',
+    credentials = await credentials.refresh(
+        identifier: 'identifier',
         httpClient: httpClient);
     expect(credentials.accessToken, equals('new access token'));
     expect(credentials.refreshToken, equals('new refresh token'));
   });
 
-  test("uses the old refresh token if a new one isn't provided", () async {
+  test("can refresh without client authentication", () async {
     var credentials = new oauth2.Credentials(
-        'access token', 'refresh token', tokenEndpoint);
+        'access token',
+        refreshToken: 'refresh token',
+        tokenEndpoint: tokenEndpoint,
+        scopes: ['scope1', 'scope2']);
     expect(credentials.canRefresh, true);
 
     httpClient.expectRequest((request) {
@@ -94,23 +142,90 @@
       expect(request.bodyFields, equals({
         "grant_type": "refresh_token",
         "refresh_token": "refresh token",
-        "client_id": "identifier",
-        "client_secret": "secret"
+        "scope": "scope1 scope2"
       }));
 
       return new Future.value(new http.Response(JSON.encode({
         'access_token': 'new access token',
+        'token_type': 'bearer',
+        'refresh_token': 'new refresh token'
+      }), 200, headers: {'content-type': 'application/json'}));
+    });
+
+
+    credentials = await credentials.refresh(httpClient: httpClient);
+    expect(credentials.accessToken, equals('new access token'));
+    expect(credentials.refreshToken, equals('new refresh token'));
+  });
+
+  test("uses the old refresh token if a new one isn't provided", () async {
+    var credentials = new oauth2.Credentials(
+        'access token',
+        refreshToken: 'refresh token',
+        tokenEndpoint: tokenEndpoint);
+    expect(credentials.canRefresh, true);
+
+    httpClient.expectRequest((request) {
+      expect(request.method, equals('POST'));
+      expect(request.url.toString(), equals(tokenEndpoint.toString()));
+      expect(request.bodyFields, equals({
+        "grant_type": "refresh_token",
+        "refresh_token": "refresh token"
+      }));
+      expect(request.headers, containsPair(
+          "Authorization",
+          "Basic aWQlQzMlQUJudCVDNCVBQmZpZXI6cyVDMyVBQmNyZXQ="));
+
+      return new Future.value(new http.Response(JSON.encode({
+        'access_token': 'new access token',
         'token_type': 'bearer'
       }), 200, headers: {'content-type': 'application/json'}));
     });
 
 
-    credentials = await credentials.refresh('identifier', 'secret',
+    credentials = await credentials.refresh(
+        identifier: 'idëntīfier',
+        secret: 'sëcret',
         httpClient: httpClient);
     expect(credentials.accessToken, equals('new access token'));
     expect(credentials.refreshToken, equals('refresh token'));
   });
 
+  test("uses form-field authentication if basicAuth is false", () async {
+    var credentials = new oauth2.Credentials(
+        'access token',
+        refreshToken: 'refresh token',
+        tokenEndpoint: tokenEndpoint,
+        scopes: ['scope1', 'scope2']);
+    expect(credentials.canRefresh, true);
+
+    httpClient.expectRequest((request) {
+      expect(request.method, equals('POST'));
+      expect(request.url.toString(), equals(tokenEndpoint.toString()));
+      expect(request.bodyFields, equals({
+        "grant_type": "refresh_token",
+        "refresh_token": "refresh token",
+        "scope": "scope1 scope2",
+        "client_id": "idëntīfier",
+        "client_secret": "sëcret"
+      }));
+
+      return new Future.value(new http.Response(JSON.encode({
+        'access_token': 'new access token',
+        'token_type': 'bearer',
+        'refresh_token': 'new refresh token'
+      }), 200, headers: {'content-type': 'application/json'}));
+    });
+
+    credentials = await credentials.refresh(
+        identifier: 'idëntīfier',
+        secret: 'sëcret',
+        basicAuth: false,
+        httpClient: httpClient);
+    expect(credentials.accessToken, equals('new access token'));
+    expect(credentials.refreshToken, equals('new refresh token'));
+  });
+
   group("fromJson", () {
     oauth2.Credentials fromMap(Map map) =>
       new oauth2.Credentials.fromJson(JSON.encode(map));
@@ -118,8 +233,11 @@
     test("should load the same credentials from toJson", () {
       var expiration = new DateTime.now().subtract(new Duration(hours: 1));
       var credentials = new oauth2.Credentials(
-          'access token', 'refresh token', tokenEndpoint, ['scope1', 'scope2'],
-          expiration);
+          'access token',
+          refreshToken: 'refresh token',
+          tokenEndpoint: tokenEndpoint,
+          scopes: ['scope1', 'scope2'],
+          expiration: expiration);
       var reloaded = new oauth2.Credentials.fromJson(credentials.toJson());
 
       expect(reloaded.accessToken, equals(credentials.accessToken));
diff --git a/test/utils_test.dart b/test/utils_test.dart
deleted file mode 100644
index 54c2da5..0000000
--- a/test/utils_test.dart
+++ /dev/null
@@ -1,88 +0,0 @@
-// Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-import 'package:oauth2/src/utils.dart';
-import 'package:test/test.dart';
-
-void main() {
-  group('AuthenticateHeader', () {
-    test("parses a scheme", () {
-      var header = new AuthenticateHeader.parse('bearer');
-      expect(header.scheme, equals('bearer'));
-      expect(header.parameters, equals({}));
-    });
-
-    test("lower-cases the scheme", () {
-      var header = new AuthenticateHeader.parse('BeaRer');
-      expect(header.scheme, equals('bearer'));
-      expect(header.parameters, equals({}));
-    });
-
-    test("parses a scheme with trailing whitespace", () {
-      var header = new AuthenticateHeader.parse('bearer   ');
-      expect(header.scheme, equals('bearer'));
-      expect(header.parameters, equals({}));
-    });
-
-    test("parses a scheme with one param", () {
-      var header = new AuthenticateHeader.parse('bearer  foo="bar"');
-      expect(header.scheme, equals('bearer'));
-      expect(header.parameters, equals({'foo': 'bar'}));
-    });
-
-    test("parses a scheme with several params", () {
-      var header = new AuthenticateHeader.parse(
-          'bearer foo="bar", bar="baz"  ,baz="qux"');
-      expect(header.scheme, equals('bearer'));
-      expect(header.parameters, equals({
-        'foo': 'bar',
-        'bar': 'baz',
-        'baz': 'qux'
-      }));
-    });
-
-    test("lower-cases parameter names but not values", () {
-      var header = new AuthenticateHeader.parse('bearer FoO="bAr"');
-      expect(header.scheme, equals('bearer'));
-      expect(header.parameters, equals({'foo': 'bAr'}));
-    });
-
-    test("allows empty values", () {
-      var header = new AuthenticateHeader.parse('bearer foo=""');
-      expect(header.scheme, equals('bearer'));
-      expect(header.parameters, equals({'foo': ''}));
-    });
-
-    test("won't parse an empty string", () {
-      expect(() => new AuthenticateHeader.parse(''),
-          throwsFormatException);
-    });
-
-    test("won't parse a token without a value", () {
-      expect(() => new AuthenticateHeader.parse('bearer foo'),
-          throwsFormatException);
-
-      expect(() => new AuthenticateHeader.parse('bearer foo='),
-          throwsFormatException);
-    });
-
-    test("won't parse a token without a value", () {
-      expect(() => new AuthenticateHeader.parse('bearer foo'),
-          throwsFormatException);
-
-      expect(() => new AuthenticateHeader.parse('bearer foo='),
-          throwsFormatException);
-    });
-
-    test("won't parse a trailing comma", () {
-      expect(() => new AuthenticateHeader.parse('bearer foo="bar",'),
-          throwsFormatException);
-    });
-
-    test("won't parse a multiple params without a comma", () {
-      expect(() => new AuthenticateHeader.parse('bearer foo="bar" bar="baz"'),
-          throwsFormatException);
-    });
-  });
-}