Add scope delimiter option (#17)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 32df035..d629306 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,10 @@
+# 1.1.0
+
+* Add a `delimiter` parameter to `new AuthorizationCodeGrant()`, `new
+  Credentials()`, and `resourceOwnerPasswordGrant()`. This controls the
+  delimiter between scopes, which some authorization servers require to be
+  different values than the specified `' '`.
+
 # 1.0.2
 
 * Fix all strong-mode warnings.
diff --git a/lib/src/authorization_code_grant.dart b/lib/src/authorization_code_grant.dart
index bd44c6a..af0b0c6 100644
--- a/lib/src/authorization_code_grant.dart
+++ b/lib/src/authorization_code_grant.dart
@@ -69,6 +69,9 @@
   /// Whether to use HTTP Basic authentication for authorizing the client.
   final bool _basicAuth;
 
+  /// A [String] used to separate scopes; defaults to `" "`.
+  String _delimiter;
+
   /// The HTTP client used to make HTTP requests.
   http.Client _httpClient;
 
@@ -98,13 +101,21 @@
   ///
   /// [httpClient] is used for all HTTP requests made by this grant, as well as
   /// those of the [Client] is constructs.
+  ///
+  /// 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.
   AuthorizationCodeGrant(
           this.identifier,
           this.authorizationEndpoint,
           this.tokenEndpoint,
-          {this.secret, bool basicAuth: true, http.Client httpClient})
+          {this.secret,
+          String delimiter,
+          bool basicAuth: true,
+          http.Client httpClient})
       : _basicAuth = basicAuth,
-        _httpClient = httpClient == null ? new http.Client() : httpClient;
+        _httpClient = httpClient == null ? new http.Client() : httpClient,
+        _delimiter = delimiter ?? ' ';
 
   /// Returns the URL to which the resource owner should be redirected to
   /// authorize this client.
@@ -148,7 +159,7 @@
     };
 
     if (state != null) parameters['state'] = state;
-    if (!scopes.isEmpty) parameters['scope'] = scopes.join(' ');
+    if (!scopes.isEmpty) parameters['scope'] = scopes.join(_delimiter);
 
     return addQueryParameters(this.authorizationEndpoint, parameters);
   }
@@ -261,7 +272,7 @@
         headers: headers, body: body);
 
     var credentials = handleAccessTokenResponse(
-        response, tokenEndpoint, startTime, _scopes);
+        response, tokenEndpoint, startTime, _scopes, _delimiter);
     return new Client(
         credentials,
         identifier: this.identifier,
diff --git a/lib/src/credentials.dart b/lib/src/credentials.dart
index 4531818..a0cbbc3 100644
--- a/lib/src/credentials.dart
+++ b/lib/src/credentials.dart
@@ -26,6 +26,9 @@
 /// Note that a given set of credentials can only be refreshed once, so be sure
 /// to save the refreshed credentials for future use.
 class Credentials {
+  /// A [String] used to separate scopes; defaults to `" "`.
+  String _delimiter;
+
   /// The token that is sent to the resource server to prove the authorization
   /// of a client.
   final String accessToken;
@@ -71,16 +74,22 @@
   /// [Client.credentials] after a [Client] is created by
   /// [AuthorizationCodeGrant]. Alternately, it may be loaded from a serialized
   /// form via [Credentials.fromJson].
+  ///
+  /// 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.
   Credentials(
           this.accessToken,
           {this.refreshToken,
           this.tokenEndpoint,
           Iterable<String> scopes,
-          this.expiration})
+          this.expiration,
+          String delimiter})
       : scopes = new UnmodifiableListView(
             // Explicitly type-annotate the list literal to work around
             // sdk#24202.
-            scopes == null ? <String>[] : scopes.toList());
+            scopes == null ? <String>[] : scopes.toList()),
+        _delimiter = delimiter ?? ' ';
 
   /// Loads a set of credentials from a JSON-serialized form.
   ///
@@ -190,7 +199,7 @@
       "grant_type": "refresh_token",
       "refresh_token": refreshToken
     };
-    if (!scopes.isEmpty) body["scope"] = scopes.join(' ');
+    if (!scopes.isEmpty) body["scope"] = scopes.join(_delimiter);
 
     if (basicAuth && secret != null) {
       headers["Authorization"] = basicAuthHeader(identifier, secret);
@@ -202,7 +211,7 @@
     var response = await httpClient.post(tokenEndpoint,
         headers: headers, body: body);
     var credentials = await handleAccessTokenResponse(
-        response, tokenEndpoint, startTime, scopes);
+        response, tokenEndpoint, startTime, scopes, _delimiter);
 
     // The authorization server may issue a new refresh token. If it doesn't,
     // we should re-use the one we already have.
diff --git a/lib/src/handle_access_token_response.dart b/lib/src/handle_access_token_response.dart
index e0fb6f6..70fe560 100644
--- a/lib/src/handle_access_token_response.dart
+++ b/lib/src/handle_access_token_response.dart
@@ -22,11 +22,14 @@
 ///
 /// This response format is common across several different components of the
 /// OAuth2 flow.
+/// 
+/// The scope strings will be separated by the provided [delimiter].
 Credentials handleAccessTokenResponse(
     http.Response response,
     Uri tokenEndpoint,
     DateTime startTime,
-    List<String> scopes) {
+    List<String> scopes,
+    String delimiter) {
   if (response.statusCode != 200) _handleErrorResponse(response, tokenEndpoint);
 
   validate(condition, message) =>
@@ -78,7 +81,7 @@
   }
 
   var scope = parameters['scope'] as String;
-  if (scope != null) scopes = scope.split(" ");
+  if (scope != null) scopes = scope.split(delimiter);
 
   var expiration = expiresIn == null ? null :
       startTime.add(new Duration(seconds: expiresIn) - _expirationGrace);
diff --git a/lib/src/resource_owner_password_grant.dart b/lib/src/resource_owner_password_grant.dart
index 5427ecc..9986ba7 100644
--- a/lib/src/resource_owner_password_grant.dart
+++ b/lib/src/resource_owner_password_grant.dart
@@ -28,6 +28,10 @@
 /// may not be granted access to every scope you request; you may check the
 /// [Credentials.scopes] field of [Client.credentials] to see which scopes you
 /// were granted.
+///
+/// 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.
 Future<Client> resourceOwnerPasswordGrant(
     Uri authorizationEndpoint,
     String username,
@@ -36,7 +40,9 @@
     String secret,
     Iterable<String> scopes,
     bool basicAuth: true,
-    http.Client httpClient}) async {
+    http.Client httpClient,
+    String delimiter}) async {
+  delimiter ??= ' ';
   var startTime = new DateTime.now();
 
   var body = {
@@ -56,13 +62,13 @@
     }
   }
 
-  if (scopes != null && !scopes.isEmpty) body['scope'] = scopes.join(' ');
+  if (scopes != null && !scopes.isEmpty) body['scope'] = scopes.join(delimiter);
 
   if (httpClient == null) httpClient = new http.Client();
   var response = await httpClient.post(authorizationEndpoint,
       headers: headers, body: body);
 
   var credentials = await handleAccessTokenResponse(
-      response, authorizationEndpoint, startTime, scopes);
+      response, authorizationEndpoint, startTime, scopes, delimiter);
   return new Client(credentials, identifier: identifier, secret: secret);
 }
diff --git a/pubspec.yaml b/pubspec.yaml
index a8a05c0..87d5a23 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: oauth2
-version: 1.0.2
+version: 1.1.0
 author: Dart Team <misc@dartlang.org>
 homepage: http://github.com/dart-lang/oauth2
 description: >
diff --git a/test/authorization_code_grant_test.dart b/test/authorization_code_grant_test.dart
index e03a607..db04725 100644
--- a/test/authorization_code_grant_test.dart
+++ b/test/authorization_code_grant_test.dart
@@ -46,6 +46,24 @@
               '&scope=scope+other%2Fscope'));
     });
 
+    test('separates scopes with the correct delimiter', () {
+      var grant = new oauth2.AuthorizationCodeGrant(
+          'identifier',
+          Uri.parse('https://example.com/authorization'),
+          Uri.parse('https://example.com/token'),
+          secret: 'secret',
+          httpClient: client,
+          delimiter: '_');
+      var authorizationUrl = grant.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'));
+    });
+
     test('builds the correct URL with state', () {
       var authorizationUrl = grant.getAuthorizationUrl(
           redirectUrl, state: 'state');
diff --git a/test/credentials_test.dart b/test/credentials_test.dart
index cedea6b..0d4eff0 100644
--- a/test/credentials_test.dart
+++ b/test/credentials_test.dart
@@ -95,6 +95,27 @@
     expect(credentials.refreshToken, equals('new refresh token'));
   });
 
+  test('sets proper scope string when using custom delimiter', () async {
+    var credentials = new oauth2.Credentials(
+        'access token',
+        refreshToken: 'refresh token',
+        tokenEndpoint: tokenEndpoint,
+        scopes: ['scope1', 'scope2'],
+        delimiter: ',');
+    httpClient.expectRequest((http.Request request) {
+      expect(request.bodyFields['scope'], equals('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'}));
+    });
+    await credentials.refresh(
+        identifier: 'idëntÄ«fier',
+        secret: 'sëcret',
+        httpClient: httpClient);
+  });
+
   test("can refresh without a client secret", () async {
     var credentials = new oauth2.Credentials(
         'access token',
diff --git a/test/handle_access_token_response_test.dart b/test/handle_access_token_response_test.dart
index fe2a338..50343ed 100644
--- a/test/handle_access_token_response_test.dart
+++ b/test/handle_access_token_response_test.dart
@@ -16,7 +16,7 @@
 final DateTime startTime = new DateTime.now();
 
 oauth2.Credentials handle(http.Response response) =>
-  handleAccessTokenResponse(response, tokenEndpoint, startTime, ["scope"]);
+  handleAccessTokenResponse(response, tokenEndpoint, startTime, ["scope"], ' ');
 
 void main() {
   group('an error response', () {
@@ -186,5 +186,18 @@
       var credentials = handleSuccess(scope: "scope1 scope2");
       expect(credentials.scopes, equals(["scope1", "scope2"]));
     });
+
+    test('with a custom scope delimiter sets the scopes', () {
+      var response = new http.Response(JSON.encode({
+        'access_token': 'access token',
+        'token_type': 'bearer',
+        'expires_in': null,
+        'refresh_token': null,
+        'scope': 'scope1,scope2'
+      }), 200, headers: {'content-type': 'application/json'});
+      var credentials = handleAccessTokenResponse(
+          response, tokenEndpoint, startTime, ['scope'], ',');
+      expect(credentials.scopes, equals(['scope1', 'scope2']));
+    });
   });
 }
diff --git a/test/resource_owner_password_grant_test.dart b/test/resource_owner_password_grant_test.dart
index 1d73a0f..e6d3f35 100644
--- a/test/resource_owner_password_grant_test.dart
+++ b/test/resource_owner_password_grant_test.dart
@@ -87,6 +87,21 @@
       expect(client.credentials.accessToken, equals('2YotnFZFEjr1zCsicMWpAA'));
     });
 
+    test('builds correct request using scope with custom delimiter', () async {
+      expectClient.expectRequest((request) async {
+        expect(request.bodyFields['grant_type'], equals('password'));
+        expect(request.bodyFields['username'], equals('username'));
+        expect(request.bodyFields['password'], equals('userpass'));
+        expect(request.bodyFields['scope'], equals('one,two'));
+        return new http.Response(success, 200,
+            headers: {'content-type': 'application/json'});
+      });
+
+      await oauth2.resourceOwnerPasswordGrant(
+          authEndpoint, 'username', 'userpass',
+          scopes: ['one', 'two'], httpClient: expectClient, delimiter: ',');
+    });
+
     test('merges with existing query parameters', () async {
       var authEndpoint = Uri.parse('https://example.com?query=value');