Merge branch 'master' into master
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3827b2b..87dfbad 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,11 @@
-# 1.2.4
+# 1.4.0
 
 * OpenID's id_token treated.
 
+# 1.3.0
+
+* Added `onCredentialsRefreshed` option when creating `Client` objects.
+
 # 1.2.3
 
 * Support the latest `package:http` release.
diff --git a/README.md b/README.md
index d3045a3..053f88b 100644
--- a/README.md
+++ b/README.md
@@ -97,7 +97,7 @@
 }
 
 main() async {
-  var client = await loadClient();
+  var client = await getClient();
 
   // Once you have a Client, you can use it just like any other HTTP client.
   var result = client.read("http://example.com/protected-resources.txt");
diff --git a/lib/src/authorization_code_grant.dart b/lib/src/authorization_code_grant.dart
index 6b28290..526ab9f 100644
--- a/lib/src/authorization_code_grant.dart
+++ b/lib/src/authorization_code_grant.dart
@@ -9,6 +9,7 @@
 
 import 'client.dart';
 import 'authorization_exception.dart';
+import 'credentials.dart';
 import 'handle_access_token_response.dart';
 import 'parameters.dart';
 import 'utils.dart';
@@ -71,6 +72,11 @@
   /// documentation.
   final Uri tokenEndpoint;
 
+  /// Callback to be invoked whenever the credentials are refreshed.
+  ///
+  /// This will be passed as-is to the constructed [Client].
+  CredentialsRefreshedCallback _onCredentialsRefreshed;
+
   /// Whether to use HTTP Basic authentication for authorizing the client.
   final bool _basicAuth;
 
@@ -107,6 +113,9 @@
   /// [httpClient] is used for all HTTP requests made by this grant, as well as
   /// those of the [Client] is constructs.
   ///
+  /// [onCredentialsRefreshed] will be called by the constructed [Client]
+  /// whenever the credentials are refreshed.
+  ///
   /// 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.
@@ -126,11 +135,13 @@
       String delimiter,
       bool basicAuth = true,
       http.Client httpClient,
+      CredentialsRefreshedCallback onCredentialsRefreshed,
       Map<String, dynamic> getParameters(MediaType contentType, String body)})
       : _basicAuth = basicAuth,
         _httpClient = httpClient == null ? new http.Client() : httpClient,
         _delimiter = delimiter ?? ' ',
-        _getParameters = getParameters ?? parseJsonParameters;
+        _getParameters = getParameters ?? parseJsonParameters,
+        _onCredentialsRefreshed = onCredentialsRefreshed;
 
   /// Returns the URL to which the resource owner should be redirected to
   /// authorize this client.
@@ -289,7 +300,8 @@
         identifier: this.identifier,
         secret: this.secret,
         basicAuth: _basicAuth,
-        httpClient: _httpClient);
+        httpClient: _httpClient,
+        onCredentialsRefreshed: _onCredentialsRefreshed);
   }
 
   /// Closes the grant and frees its resources.
diff --git a/lib/src/client.dart b/lib/src/client.dart
index 367d583..4fa64a1 100644
--- a/lib/src/client.dart
+++ b/lib/src/client.dart
@@ -11,8 +11,6 @@
 import 'credentials.dart';
 import 'expiration_exception.dart';
 
-// TODO(nweiz): Add an onCredentialsRefreshed event once we have some event
-// infrastructure.
 /// An OAuth2 client.
 ///
 /// This acts as a drop-in replacement for an [http.Client], while sending
@@ -67,6 +65,9 @@
   Credentials get credentials => _credentials;
   Credentials _credentials;
 
+  /// Callback to be invoked whenever the credentials refreshed.
+  final CredentialsRefreshedCallback _onCredentialsRefreshed;
+
   /// Whether to use HTTP Basic authentication for authorizing the client.
   final bool _basicAuth;
 
@@ -86,9 +87,11 @@
   Client(this._credentials,
       {this.identifier,
       this.secret,
+      CredentialsRefreshedCallback onCredentialsRefreshed,
       bool basicAuth = true,
       http.Client httpClient})
       : _basicAuth = basicAuth,
+        _onCredentialsRefreshed = onCredentialsRefreshed,
         _httpClient = httpClient == null ? new http.Client() : httpClient {
     if (identifier == null && secret != null) {
       throw new ArgumentError("secret may not be passed without identifier.");
@@ -156,6 +159,8 @@
         basicAuth: _basicAuth,
         httpClient: _httpClient);
 
+    if (_onCredentialsRefreshed != null) _onCredentialsRefreshed(_credentials);
+
     return this;
   }
 
diff --git a/lib/src/credentials.dart b/lib/src/credentials.dart
index ecbe0d4..6ea254d 100644
--- a/lib/src/credentials.dart
+++ b/lib/src/credentials.dart
@@ -13,6 +13,9 @@
 import 'parameters.dart';
 import 'utils.dart';
 
+/// Type of the callback when credentials are refreshed.
+typedef CredentialsRefreshedCallback = void Function(Credentials);
+
 /// Credentials that prove that a client is allowed to access a resource on the
 /// resource owner's behalf.
 ///
diff --git a/lib/src/resource_owner_password_grant.dart b/lib/src/resource_owner_password_grant.dart
index 13d8f28..a3c16f5 100644
--- a/lib/src/resource_owner_password_grant.dart
+++ b/lib/src/resource_owner_password_grant.dart
@@ -10,6 +10,7 @@
 import 'client.dart';
 import 'handle_access_token_response.dart';
 import 'utils.dart';
+import 'credentials.dart';
 
 /// Obtains credentials using a [resource owner password grant][].
 ///
@@ -49,6 +50,7 @@
     String secret,
     Iterable<String> scopes,
     bool basicAuth = true,
+    CredentialsRefreshedCallback onCredentialsRefreshed,
     http.Client httpClient,
     String delimiter,
     Map<String, dynamic> getParameters(
@@ -84,5 +86,8 @@
       response, authorizationEndpoint, startTime, scopes, delimiter,
       getParameters: getParameters);
   return new Client(credentials,
-      identifier: identifier, secret: secret, httpClient: httpClient);
+      identifier: identifier,
+      secret: secret,
+      httpClient: httpClient,
+      onCredentialsRefreshed: onCredentialsRefreshed);
 }
diff --git a/test/authorization_code_grant_test.dart b/test/authorization_code_grant_test.dart
index ec6e71b..babdb78 100644
--- a/test/authorization_code_grant_test.dart
+++ b/test/authorization_code_grant_test.dart
@@ -289,4 +289,50 @@
       })));
     });
   });
+
+  group('onCredentialsRefreshed', () {
+    test('is correctly propagated', () async {
+      var isCallbackInvoked = false;
+      var grant = new oauth2.AuthorizationCodeGrant(
+          'identifier',
+          Uri.parse('https://example.com/authorization'),
+          Uri.parse('https://example.com/token'),
+          secret: 'secret',
+          basicAuth: false,
+          httpClient: client, onCredentialsRefreshed: (credentials) {
+        isCallbackInvoked = true;
+      });
+
+      grant.getAuthorizationUrl(redirectUrl);
+      client.expectRequest((request) {
+        return new Future.value(new http.Response(
+            jsonEncode({
+              'access_token': 'access token',
+              'token_type': 'bearer',
+              "expires_in": -3600,
+              "refresh_token": "refresh token",
+            }),
+            200,
+            headers: {'content-type': 'application/json'}));
+      });
+
+      var oauth2Client = await grant.handleAuthorizationCode('auth code');
+
+      client.expectRequest((request) {
+        return new Future.value(new http.Response(
+            jsonEncode(
+                {'access_token': 'new access token', 'token_type': 'bearer'}),
+            200,
+            headers: {'content-type': 'application/json'}));
+      });
+
+      client.expectRequest((request) {
+        return new Future.value(new http.Response('good job', 200));
+      });
+
+      await oauth2Client.read(Uri.parse("http://example.com/resource"));
+
+      expect(isCallbackInvoked, equals(true));
+    });
+  });
 }
diff --git a/test/client_test.dart b/test/client_test.dart
index ec52589..1f13d58 100644
--- a/test/client_test.dart
+++ b/test/client_test.dart
@@ -64,6 +64,38 @@
       await client.read(requestUri);
       expect(client.credentials.accessToken, equals('new access token'));
     });
+
+    test("that onCredentialsRefreshed is called", () async {
+      var callbackCalled = false;
+
+      var expiration = new DateTime.now().subtract(new Duration(hours: 1));
+      var credentials = new oauth2.Credentials('access token',
+          refreshToken: 'refresh token',
+          tokenEndpoint: tokenEndpoint,
+          expiration: expiration);
+      var client = new oauth2.Client(credentials,
+          identifier: 'identifier',
+          secret: 'secret',
+          httpClient: httpClient, onCredentialsRefreshed: (credentials) {
+        callbackCalled = true;
+        expect(credentials.accessToken, equals('new access token'));
+      });
+
+      httpClient.expectRequest((request) {
+        return new Future.value(new http.Response(
+            jsonEncode(
+                {'access_token': 'new access token', 'token_type': 'bearer'}),
+            200,
+            headers: {'content-type': 'application/json'}));
+      });
+
+      httpClient.expectRequest((request) {
+        return new Future.value(new http.Response('good job', 200));
+      });
+
+      await client.read(requestUri);
+      expect(callbackCalled, equals(true));
+    });
   });
 
   group('with valid credentials', () {
diff --git a/test/resource_owner_password_grant_test.dart b/test/resource_owner_password_grant_test.dart
index 04d1537..acd76d1 100644
--- a/test/resource_owner_password_grant_test.dart
+++ b/test/resource_owner_password_grant_test.dart
@@ -3,8 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 
 @TestOn("vm")
-
 import 'dart:convert';
+import 'dart:async';
 
 import 'package:http/http.dart' as http;
 import 'package:oauth2/oauth2.dart' as oauth2;
@@ -46,6 +46,44 @@
       expect(client.credentials.accessToken, equals('2YotnFZFEjr1zCsicMWpAA'));
     });
 
+    test('passes the onCredentialsRefreshed callback to the client', () async {
+      expectClient.expectRequest((request) async {
+        return new http.Response(
+            jsonEncode({
+              "access_token": "2YotnFZFEjr1zCsicMWpAA",
+              "token_type": "bearer",
+              "expires_in": -3600,
+              "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
+            }),
+            200,
+            headers: {'content-type': 'application/json'});
+      });
+
+      var isCallbackInvoked = false;
+
+      var client = await oauth2.resourceOwnerPasswordGrant(
+          authEndpoint, 'username', 'userpass',
+          identifier: 'client', secret: 'secret', httpClient: expectClient,
+          onCredentialsRefreshed: (oauth2.Credentials credentials) {
+        isCallbackInvoked = true;
+      });
+
+      expectClient.expectRequest((request) {
+        return new Future.value(new http.Response(
+            jsonEncode(
+                {'access_token': 'new access token', 'token_type': 'bearer'}),
+            200,
+            headers: {'content-type': 'application/json'}));
+      });
+
+      expectClient.expectRequest((request) {
+        return new Future.value(new http.Response('good job', 200));
+      });
+
+      await client.read(Uri.parse("http://example.com/resource"));
+      expect(isCallbackInvoked, equals(true));
+    });
+
     test('builds correct request when using query parameters for client',
         () async {
       expectClient.expectRequest((request) async {