Merge pull request #47 from wesleyfantinel/master

OpenID's id_token implementation
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0181a09..87dfbad 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+# 1.4.0
+
+* OpenID's id_token treated.
+
 # 1.3.0
 
 * Added `onCredentialsRefreshed` option when creating `Client` objects.
diff --git a/lib/src/credentials.dart b/lib/src/credentials.dart
index e8a161a..6ea254d 100644
--- a/lib/src/credentials.dart
+++ b/lib/src/credentials.dart
@@ -44,6 +44,16 @@
   /// This may be `null`, indicating that the credentials can't be refreshed.
   final String refreshToken;
 
+  /// The token that is received from the authorization server to enable
+  /// End-Users to be Authenticated, contains Claims, represented as a
+  /// JSON Web Token (JWT).
+  ///
+  /// This may be `null`, indicating that the 'openid' scope was not
+  /// requested (or not supported).
+  ///
+  /// [spec]: https://openid.net/specs/openid-connect-core-1_0.html#IDToken
+  final String idToken;
+
   /// The URL of the authorization server endpoint that's used to refresh the
   /// credentials.
   ///
@@ -98,6 +108,7 @@
   /// [standard JSON response]: https://tools.ietf.org/html/rfc6749#section-5.1
   Credentials(this.accessToken,
       {this.refreshToken,
+      this.idToken,
       this.tokenEndpoint,
       Iterable<String> scopes,
       this.expiration,
@@ -135,7 +146,7 @@
         'required field "accessToken" was not a string, was '
         '${parsed["accessToken"]}');
 
-    for (var stringField in ['refreshToken', 'tokenEndpoint']) {
+    for (var stringField in ['refreshToken', 'idToken', 'tokenEndpoint']) {
       var value = parsed[stringField];
       validate(value == null || value is String,
           'field "$stringField" was not a string, was "$value"');
@@ -158,6 +169,7 @@
 
     return new Credentials(parsed['accessToken'],
         refreshToken: parsed['refreshToken'],
+        idToken: parsed['idToken'],
         tokenEndpoint: tokenEndpoint,
         scopes: (scopes as List).map((scope) => scope as String),
         expiration: expiration);
@@ -170,6 +182,7 @@
   String toJson() => jsonEncode({
         'accessToken': accessToken,
         'refreshToken': refreshToken,
+        'idToken': idToken,
         'tokenEndpoint':
             tokenEndpoint == null ? null : tokenEndpoint.toString(),
         'scopes': scopes,
@@ -236,6 +249,7 @@
     if (credentials.refreshToken != null) return credentials;
     return new Credentials(credentials.accessToken,
         refreshToken: this.refreshToken,
+        idToken: credentials.idToken,
         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 ca4896e..851475a 100644
--- a/lib/src/handle_access_token_response.dart
+++ b/lib/src/handle_access_token_response.dart
@@ -72,7 +72,7 @@
           'parameter "expires_in" was not an int, was "$expiresIn"');
     }
 
-    for (var name in ['refresh_token', 'scope']) {
+    for (var name in ['refresh_token', 'id_token', 'scope']) {
       var value = parameters[name];
       if (value != null && value is! String)
         throw new FormatException(
@@ -88,6 +88,7 @@
 
     return new Credentials(parameters['access_token'],
         refreshToken: parameters['refresh_token'],
+        idToken: parameters['id_token'],
         tokenEndpoint: tokenEndpoint,
         scopes: scopes,
         expiration: expiration);
diff --git a/pubspec.yaml b/pubspec.yaml
index 9ccb43e..9e8c56f 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: oauth2
-version: 1.3.0
+version: 1.4.0
 author: Dart Team <misc@dartlang.org>
 homepage: https://github.com/dart-lang/oauth2
 description: >-
diff --git a/test/credentials_test.dart b/test/credentials_test.dart
index 039a060..386f948 100644
--- a/test/credentials_test.dart
+++ b/test/credentials_test.dart
@@ -270,6 +270,7 @@
 
       var credentials = new oauth2.Credentials('access token',
           refreshToken: 'refresh token',
+          idToken: 'id token',
           tokenEndpoint: tokenEndpoint,
           scopes: ['scope1', 'scope2'],
           expiration: expiration);
@@ -277,6 +278,7 @@
 
       expect(reloaded.accessToken, equals(credentials.accessToken));
       expect(reloaded.refreshToken, equals(credentials.refreshToken));
+      expect(reloaded.idToken, equals(credentials.idToken));
       expect(reloaded.tokenEndpoint.toString(),
           equals(credentials.tokenEndpoint.toString()));
       expect(reloaded.scopes, equals(credentials.scopes));
@@ -306,6 +308,11 @@
           throwsFormatException);
     });
 
+    test("should throw a FormatException if idToken is not a string", () {
+      expect(() => fromMap({"accessToken": "foo", "idToken": 12}),
+          throwsFormatException);
+    });
+
     test("should throw a FormatException if tokenEndpoint is not a string", () {
       expect(() => fromMap({"accessToken": "foo", "tokenEndpoint": 12}),
           throwsFormatException);
diff --git a/test/handle_access_token_response_test.dart b/test/handle_access_token_response_test.dart
index fd4b1a7..1556164 100644
--- a/test/handle_access_token_response_test.dart
+++ b/test/handle_access_token_response_test.dart
@@ -259,4 +259,34 @@
       expect(credentials.scopes, equals(['scope1', 'scope2']));
     });
   });
+
+  group('a success response with a id_token', () {
+    oauth2.Credentials handleSuccess(
+        {String contentType = "application/json",
+        accessToken = 'access token',
+        tokenType = 'bearer',
+        expiresIn,
+        idToken = 'decode me',
+        scope}) {
+      return handle(new http.Response(
+          jsonEncode({
+            'access_token': accessToken,
+            'token_type': tokenType,
+            'expires_in': expiresIn,
+            'id_token': idToken,
+            'scope': scope
+          }),
+          200,
+          headers: {'content-type': contentType}));
+    }
+
+    test('with a non-string id token throws a FormatException', () {
+      expect(() => handleSuccess(idToken: 12), throwsFormatException);
+    });
+
+    test('with a id token sets the id token', () {
+      var credentials = handleSuccess(idToken: "decode me");
+      expect(credentials.idToken, equals("decode me"));
+    });
+  });
 }