diff --git a/packages/firebase_auth/CHANGELOG.md b/packages/firebase_auth/CHANGELOG.md index f484c288a65e..4bac95976721 100644 --- a/packages/firebase_auth/CHANGELOG.md +++ b/packages/firebase_auth/CHANGELOG.md @@ -1,3 +1,10 @@ +## 0.14.0 + +* Added new `IdTokenResult` class. +* **Breaking Change**. `getIdToken()` method now returns `IdTokenResult` instead of a token `String`. + Use the `token` property of `IdTokenResult` to retrieve the token `String`. +* Added integration testing for `getIdToken()`. + ## 0.13.1+1 * Update authentication example in README. diff --git a/packages/firebase_auth/android/src/main/java/io/flutter/plugins/firebaseauth/FirebaseAuthPlugin.java b/packages/firebase_auth/android/src/main/java/io/flutter/plugins/firebaseauth/FirebaseAuthPlugin.java index b3b782d8ac04..c135259f22d1 100755 --- a/packages/firebase_auth/android/src/main/java/io/flutter/plugins/firebaseauth/FirebaseAuthPlugin.java +++ b/packages/firebase_auth/android/src/main/java/io/flutter/plugins/firebaseauth/FirebaseAuthPlugin.java @@ -532,7 +532,7 @@ private void handleGetToken(MethodCall call, final Result result, FirebaseAuth f } Map arguments = call.arguments(); - boolean refresh = arguments.get("refresh"); + final boolean refresh = arguments.get("refresh"); currentUser .getIdToken(refresh) @@ -540,8 +540,18 @@ private void handleGetToken(MethodCall call, final Result result, FirebaseAuth f new OnCompleteListener() { public void onComplete(@NonNull Task task) { if (task.isSuccessful() && task.getResult() != null) { - String idToken = task.getResult().getToken(); - result.success(idToken); + final Map map = new HashMap<>(); + map.put("token", task.getResult().getToken()); + map.put("expirationTimestamp", task.getResult().getExpirationTimestamp()); + map.put("authTimestamp", task.getResult().getAuthTimestamp()); + map.put("issuedAtTimestamp", task.getResult().getIssuedAtTimestamp()); + map.put("claims", task.getResult().getClaims()); + + if (task.getResult().getSignInProvider() != null) { + map.put("signInProvider", task.getResult().getSignInProvider()); + } + + result.success(Collections.unmodifiableMap(map)); } else { reportException(result, task.getException()); } diff --git a/packages/firebase_auth/example/test/firebase_auth.dart b/packages/firebase_auth/example/test/firebase_auth.dart index e06b387f6802..ab969daaf2e4 100644 --- a/packages/firebase_auth/example/test/firebase_auth.dart +++ b/packages/firebase_auth/example/test/firebase_auth.dart @@ -39,6 +39,20 @@ void main() { expect(user.isAnonymous, isTrue); expect(user.metadata.creationTime.isAfter(DateTime(2018, 1, 1)), isTrue); expect(user.metadata.creationTime.isBefore(DateTime.now()), isTrue); + final IdTokenResult tokenResult = await user.getIdToken(); + expect(tokenResult.token, isNotNull); + expect(tokenResult.expirationTime.isAfter(DateTime.now()), isTrue); + expect(tokenResult.authTime, isNotNull); + expect(tokenResult.issuedAtTime, isNotNull); + // TODO(jackson): Fix behavior to be consistent across platforms + // https://github.com/firebase/firebase-ios-sdk/issues/3445 + expect( + tokenResult.signInProvider == null || + tokenResult.signInProvider == 'anonymous', + isTrue); + expect(tokenResult.claims['provider_id'], 'anonymous'); + expect(tokenResult.claims['firebase']['sign_in_provider'], 'anonymous'); + expect(tokenResult.claims['user_id'], user.uid); await auth.signOut(); final FirebaseUser user2 = (await auth.signInAnonymously()).user; expect(user2.uid, isNot(equals(user.uid))); diff --git a/packages/firebase_auth/ios/Classes/FirebaseAuthPlugin.m b/packages/firebase_auth/ios/Classes/FirebaseAuthPlugin.m index dccbd28ee8fa..682ffe18e34c 100644 --- a/packages/firebase_auth/ios/Classes/FirebaseAuthPlugin.m +++ b/packages/firebase_auth/ios/Classes/FirebaseAuthPlugin.m @@ -203,10 +203,33 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result NSDictionary *args = call.arguments; BOOL refresh = [args objectForKey:@"refresh"]; [[self getAuth:call.arguments].currentUser - getIDTokenForcingRefresh:refresh - completion:^(NSString *_Nullable token, NSError *_Nullable error) { - [self sendResult:result forObject:token error:error]; - }]; + getIDTokenResultForcingRefresh:refresh + completion:^(FIRAuthTokenResult *_Nullable tokenResult, + NSError *_Nullable error) { + NSMutableDictionary *tokenData = nil; + if (tokenResult != nil) { + long expirationTimestamp = + [tokenResult.expirationDate timeIntervalSince1970]; + long authTimestamp = [tokenResult.authDate timeIntervalSince1970]; + long issuedAtTimestamp = + [tokenResult.issuedAtDate timeIntervalSince1970]; + + tokenData = [[NSMutableDictionary alloc] initWithDictionary:@{ + @"token" : tokenResult.token, + @"expirationTimestamp" : + [NSNumber numberWithInt:expirationTimestamp], + @"authTimestamp" : [NSNumber numberWithInt:authTimestamp], + @"issuedAtTimestamp" : [NSNumber numberWithInt:issuedAtTimestamp], + @"claims" : tokenResult.claims, + }]; + + if (tokenResult.signInProvider != nil) { + tokenData[@"signInProvider"] = tokenResult.signInProvider; + } + } + + [self sendResult:result forObject:tokenData error:error]; + }]; } else if ([@"reauthenticateWithCredential" isEqualToString:call.method]) { [[self getAuth:call.arguments].currentUser reauthenticateAndRetrieveDataWithCredential:[self getCredential:call.arguments] diff --git a/packages/firebase_auth/lib/firebase_auth.dart b/packages/firebase_auth/lib/firebase_auth.dart index 0e7ba33d706c..29100487713b 100755 --- a/packages/firebase_auth/lib/firebase_auth.dart +++ b/packages/firebase_auth/lib/firebase_auth.dart @@ -22,6 +22,7 @@ part 'src/auth_exception.dart'; part 'src/auth_result.dart'; part 'src/firebase_auth.dart'; part 'src/firebase_user.dart'; +part 'src/id_token_result.dart'; part 'src/user_info.dart'; part 'src/user_metadata.dart'; part 'src/user_update_info.dart'; diff --git a/packages/firebase_auth/lib/src/firebase_user.dart b/packages/firebase_auth/lib/src/firebase_user.dart index c30a5aaf0c65..b9e4c8c244ef 100644 --- a/packages/firebase_auth/lib/src/firebase_user.dart +++ b/packages/firebase_auth/lib/src/firebase_user.dart @@ -26,19 +26,21 @@ class FirebaseUser extends UserInfo { /// Returns true if the user's email is verified. bool get isEmailVerified => _data['isEmailVerified']; - /// Obtains the id token for the current user, forcing a [refresh] if desired. + /// Obtains the id token result for the current user, forcing a [refresh] if desired. /// /// Useful when authenticating against your own backend. Use our server /// SDKs or follow the official documentation to securely verify the /// integrity and validity of this token. /// /// Completes with an error if the user is signed out. - Future getIdToken({bool refresh = false}) async { - return await FirebaseAuth.channel - .invokeMethod('getIdToken', { + Future getIdToken({bool refresh = false}) async { + final Map data = await FirebaseAuth.channel + .invokeMapMethod('getIdToken', { 'refresh': refresh, 'app': _app.name, }); + + return IdTokenResult(data, _app); } /// Associates a user account from a third-party identity provider with this diff --git a/packages/firebase_auth/lib/src/id_token_result.dart b/packages/firebase_auth/lib/src/id_token_result.dart new file mode 100644 index 000000000000..c53e9ae5bb4d --- /dev/null +++ b/packages/firebase_auth/lib/src/id_token_result.dart @@ -0,0 +1,51 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +part of firebase_auth; + +/// Represents ID token result obtained from [FirebaseUser], containing the +/// ID token JWT string and other helper properties for getting different +/// data associated with the token as well as all the decoded payload claims. +/// +/// Note that these claims are not to be trusted as they are parsed client side. +/// Only server side verification can guarantee the integrity of the token +/// claims. +class IdTokenResult { + @visibleForTesting + IdTokenResult(this._data, this._app); + + final FirebaseApp _app; + + final Map _data; + + /// The Firebase Auth ID token JWT string. + String get token => _data['token']; + + /// The time when the ID token expires. + DateTime get expirationTime => + DateTime.fromMillisecondsSinceEpoch(_data['expirationTimestamp'] * 1000); + + /// The time the user authenticated (signed in). + /// + /// Note that this is not the time the token was refreshed. + DateTime get authTime => + DateTime.fromMillisecondsSinceEpoch(_data['authTimestamp'] * 1000); + + /// The time when ID token was issued. + DateTime get issuedAtTime => + DateTime.fromMillisecondsSinceEpoch(_data['issuedAtTimestamp'] * 1000); + + /// The sign-in provider through which the ID token was obtained (anonymous, + /// custom, phone, password, etc). Note, this does not map to provider IDs. + String get signInProvider => _data['signInProvider']; + + /// The entire payload claims of the ID token including the standard reserved + /// claims as well as the custom claims. + Map get claims => _data['claims']; + + @override + String toString() { + return '$runtimeType($_data)'; + } +} diff --git a/packages/firebase_auth/pubspec.yaml b/packages/firebase_auth/pubspec.yaml index d3a4085b6b80..e4fe0c6b916b 100755 --- a/packages/firebase_auth/pubspec.yaml +++ b/packages/firebase_auth/pubspec.yaml @@ -4,7 +4,7 @@ description: Flutter plugin for Firebase Auth, enabling Android and iOS like Google, Facebook and Twitter. author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/firebase_auth -version: 0.13.1+1 +version: 0.14.0 flutter: plugin: diff --git a/packages/firebase_auth/test/firebase_auth_test.dart b/packages/firebase_auth/test/firebase_auth_test.dart index 9acff316492d..5573302c62b6 100755 --- a/packages/firebase_auth/test/firebase_auth_test.dart +++ b/packages/firebase_auth/test/firebase_auth_test.dart @@ -25,6 +25,22 @@ const String kMockPhoneNumber = '5555555555'; const String kMockVerificationId = '12345'; const String kMockSmsCode = '123456'; const String kMockLanguage = 'en'; +const String kMockIdTokenResultSignInProvider = 'password'; +const Map kMockIdTokenResultClaims = { + 'claim1': 'value1', +}; +const int kMockIdTokenResultExpirationTimestamp = 123456; +const int kMockIdTokenResultAuthTimestamp = 1234567; +const int kMockIdTokenResultIssuedAtTimestamp = 12345678; +const Map kMockIdTokenResult = { + 'token': kMockIdToken, + 'expirationTimestamp': kMockIdTokenResultExpirationTimestamp, + 'authTimestamp': kMockIdTokenResultAuthTimestamp, + 'issuedAtTimestamp': kMockIdTokenResultIssuedAtTimestamp, + 'signInProvider': kMockIdTokenResultSignInProvider, + 'claims': kMockIdTokenResultClaims, +}; + final int kMockCreationTimestamp = DateTime(2019, 1, 1).millisecondsSinceEpoch; final int kMockLastSignInTimestamp = DateTime.now().subtract(const Duration(days: 1)).millisecondsSinceEpoch; @@ -65,7 +81,7 @@ void main() { log.add(call); switch (call.method) { case "getIdToken": - return kMockIdToken; + return kMockIdTokenResult; break; case "isSignInWithEmailLink": return true; @@ -126,9 +142,28 @@ void main() { } test('getIdToken', () async { + void verifyIdTokenResult(IdTokenResult idTokenResult) { + expect(idTokenResult.token, equals(kMockIdToken)); + expect( + idTokenResult.expirationTime, + equals(DateTime.fromMillisecondsSinceEpoch( + kMockIdTokenResultExpirationTimestamp * 1000))); + expect( + idTokenResult.authTime, + equals(DateTime.fromMillisecondsSinceEpoch( + kMockIdTokenResultAuthTimestamp * 1000))); + expect( + idTokenResult.issuedAtTime, + equals(DateTime.fromMillisecondsSinceEpoch( + kMockIdTokenResultIssuedAtTimestamp * 1000))); + expect(idTokenResult.signInProvider, + equals(kMockIdTokenResultSignInProvider)); + expect(idTokenResult.claims, equals(kMockIdTokenResultClaims)); + } + final FirebaseUser user = await auth.currentUser(); - expect(await user.getIdToken(), equals(kMockIdToken)); - expect(await user.getIdToken(refresh: true), equals(kMockIdToken)); + verifyIdTokenResult(await user.getIdToken()); + verifyIdTokenResult(await user.getIdToken(refresh: true)); expect( log, [