From f19883b61bc6466cdca40e3bd3bcc89caf9ddd94 Mon Sep 17 00:00:00 2001 From: Ti Wang Date: Thu, 12 Apr 2018 14:57:31 -0700 Subject: [PATCH 1/8] expose custom claims --- packages/auth-types/index.d.ts | 10 + packages/auth/demo/public/index.html | 6 + packages/auth/demo/public/script.js | 40 +++- packages/auth/src/authuser.js | 15 ++ packages/auth/src/exports_auth.js | 4 + packages/auth/src/idtoken.js | 22 +- packages/auth/src/idtokenresult.js | 63 ++++++ packages/auth/test/authuser_test.js | 265 ++++++++++++++++++++++- packages/auth/test/idtoken_test.js | 83 +++++++ packages/auth/test/idtokenresult_test.js | 142 ++++++++++++ packages/auth/test/testhelper.js | 30 +++ packages/firebase/index.d.ts | 10 + 12 files changed, 676 insertions(+), 14 deletions(-) create mode 100644 packages/auth/src/idtokenresult.js create mode 100644 packages/auth/test/idtokenresult_test.js diff --git a/packages/auth-types/index.d.ts b/packages/auth-types/index.d.ts index 7e085efee5b..e16b1945337 100644 --- a/packages/auth-types/index.d.ts +++ b/packages/auth-types/index.d.ts @@ -20,6 +20,7 @@ import { Observer, Unsubscribe } from '@firebase/util'; export interface User extends UserInfo { delete(): Promise; emailVerified: boolean; + getIdTokenResult(forceRefresh?: boolean): Promise; getIdToken(forceRefresh?: boolean): Promise; getToken(forceRefresh?: boolean): Promise; isAnonymous: boolean; @@ -160,6 +161,15 @@ export class GoogleAuthProvider_Instance implements AuthProvider { setCustomParameters(customOAuthParameters: Object): AuthProvider; } +export type IdTokenResult = { + token: string; + expirationTime: string; + authTime: string; + issuedAtTime: string; + signInProvider: string | null; + claims: Object; +}; + export class OAuthProvider implements AuthProvider { providerId: string; addScope(scope: string): AuthProvider; diff --git a/packages/auth/demo/public/index.html b/packages/auth/demo/public/index.html index a14b7c11fb5..cae68ac8e10 100644 --- a/packages/auth/demo/public/index.html +++ b/packages/auth/demo/public/index.html @@ -452,6 +452,12 @@ Confirm Email Verification + + diff --git a/packages/auth/demo/public/script.js b/packages/auth/demo/public/script.js index c7e64b89edf..96c994aa8a7 100644 --- a/packages/auth/demo/public/script.js +++ b/packages/auth/demo/public/script.js @@ -838,6 +838,38 @@ function getIdToken(forceRefresh) { } +/** + * Gets or refreshes the ID token result. + * @param {boolean} forceRefresh Whether to force the refresh of the token + * or not + */ +function getIdTokenResult(forceRefresh) { + if (activeUser() == null) { + alertError('No user logged in.'); + return; + } + activeUser().getIdTokenResult(forceRefresh).then(function(idTokenResult) { + alertSuccess(JSON.stringify(idTokenResult)); + }, onAuthError); +} + + +/** + * Triggers the retrieval of the ID token result. + */ +function onGetIdTokenResult() { + getIdTokenResult(false); +} + + +/** + * Triggers the refresh of the ID token result. + */ +function onRefreshTokenResult() { + getIdTokenResult(true); +} + + /** * Triggers the retrieval of the ID token. */ @@ -1271,8 +1303,10 @@ function initApp(){ auth.onIdTokenChanged(function(user) { refreshUserData(); if (user) { - user.getIdToken(false).then( - log, + user.getIdTokenResult(false).then( + function(idTokenResult) { + log(JSON.stringify(idTokenResult)); + }, function() { log('No token.'); } @@ -1382,6 +1416,8 @@ function initApp(){ $('#send-email-verification').click(onSendEmailVerification); $('#confirm-email-verification').click(onApplyActionCode); + $('#get-token-result').click(onGetIdTokenResult); + $('#refresh-token-result').click(onRefreshTokenResult); $('#get-token').click(onGetIdToken); $('#refresh-token').click(onRefreshToken); $('#get-token-worker').click(onGetCurrentUserDataFromWebWorker); diff --git a/packages/auth/src/authuser.js b/packages/auth/src/authuser.js index e4b200226df..7db576290ac 100644 --- a/packages/auth/src/authuser.js +++ b/packages/auth/src/authuser.js @@ -36,6 +36,7 @@ goog.require('fireauth.AuthEventHandler'); goog.require('fireauth.AuthEventManager'); goog.require('fireauth.AuthProvider'); goog.require('fireauth.ConfirmationResult'); +goog.require('fireauth.IdTokenResult'); goog.require('fireauth.PhoneAuthProvider'); goog.require('fireauth.ProactiveRefresh'); goog.require('fireauth.RpcHandler'); @@ -921,6 +922,20 @@ fireauth.AuthUser.prototype.reloadWithoutSaving_ = function() { }; +/** + * This operation resolves with the Firebase ID token result which contains + * the entire payload claims. + * @param {boolean=} opt_forceRefresh Whether to force refresh token exchange. + * @return {!goog.Promise} A Promise that resolves with + * the ID token result. + */ +fireauth.AuthUser.prototype.getIdTokenResult = function(opt_forceRefresh) { + return this.getIdToken(opt_forceRefresh).then(function(idToken) { + return new fireauth.IdTokenResult(idToken); + }); +}; + + /** * This operation resolves with the Firebase ID token. * @param {boolean=} opt_forceRefresh Whether to force refresh token exchange. diff --git a/packages/auth/src/exports_auth.js b/packages/auth/src/exports_auth.js index ebaa755de77..8d1aedd63e7 100644 --- a/packages/auth/src/exports_auth.js +++ b/packages/auth/src/exports_auth.js @@ -221,6 +221,10 @@ fireauth.exportlib.exportPrototypeMethods( name: 'delete', args: [] }, + getIdTokenResult: { + name: 'getIdTokenResult', + args: [fireauth.args.bool('opt_forceRefresh', true)] + }, getIdToken: { name: 'getIdToken', args: [fireauth.args.bool('opt_forceRefresh', true)] diff --git a/packages/auth/src/idtoken.js b/packages/auth/src/idtoken.js index 9fd4088c705..207a4fc3743 100644 --- a/packages/auth/src/idtoken.js +++ b/packages/auth/src/idtoken.js @@ -174,6 +174,23 @@ fireauth.IdToken.prototype.getPhoneNumber = function() { * @return {?fireauth.IdToken} The decoded token. */ fireauth.IdToken.parse = function(tokenString) { + var token = fireauth.IdToken.parseIdTokenClaims(tokenString); + if (token && token['sub'] && token['iss'] && token['aud'] && token['exp']) { + return new fireauth.IdToken( + /** @type {!fireauth.IdToken.JsonToken} */ (token)); + } + return null; +}; + +/** + * Converts the information part of JWT token to plain object format. + * @param {?string} tokenString The JWT token. + * @return {?Object} + */ +fireauth.IdToken.parseIdTokenClaims = function(tokenString) { + if (!tokenString) { + return null; + } // Token format is .. var fields = tokenString.split('.'); if (fields.length != 3) { @@ -187,10 +204,7 @@ fireauth.IdToken.parse = function(tokenString) { } try { var token = JSON.parse(goog.crypt.base64.decodeString(jsonInfo, true)); - if (token['sub'] && token['iss'] && token['aud'] && token['exp']) { - return new fireauth.IdToken( - /** @type {!fireauth.IdToken.JsonToken} */ (token)); - } + return /** @type {?Object} */ (token); } catch (e) {} return null; }; diff --git a/packages/auth/src/idtokenresult.js b/packages/auth/src/idtokenresult.js new file mode 100644 index 00000000000..797f0baa1d4 --- /dev/null +++ b/packages/auth/src/idtokenresult.js @@ -0,0 +1,63 @@ +/** + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Defines the firebase.auth.IdTokenResult class that is obtained + * from getIdTokenResult. It contains 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. + */ + +goog.provide('fireauth.IdTokenResult'); + +goog.require('fireauth.AuthError'); +goog.require('fireauth.IdToken'); +goog.require('fireauth.authenum.Error'); +goog.require('fireauth.object'); +goog.require('fireauth.util'); + + + +/** + * This is the ID token result object obtained from getIdTokenResult. It + * contains 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. + * @param {string} tokenString The JWT token. + * @constructor + */ +fireauth.IdTokenResult = function(tokenString) { + var idToken = fireauth.IdToken.parseIdTokenClaims(tokenString); + if (!idToken || !idToken['exp'] || !idToken['auth_time'] || !idToken['iat']) { + throw new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR, + 'An internal error occurred. The token obtained by Firebase appears ' + + 'to be malformed. Please retry the operation.'); + } + fireauth.object.setReadonlyProperties(this, { + 'token': tokenString, + 'expirationTime': fireauth.util.utcTimestampToDateString( + idToken['exp'] * 1000), + 'authTime': fireauth.util.utcTimestampToDateString( + idToken['auth_time'] * 1000), + 'issuedAtTime': fireauth.util.utcTimestampToDateString( + idToken['iat'] * 1000), + 'signInProvider': (idToken['firebase'] && + idToken['firebase']['sign_in_provider']) ? + idToken['firebase']['sign_in_provider'] : null, + 'claims': idToken + }); +}; diff --git a/packages/auth/test/authuser_test.js b/packages/auth/test/authuser_test.js index 11006b9f2ca..97706ec673f 100644 --- a/packages/auth/test/authuser_test.js +++ b/packages/auth/test/authuser_test.js @@ -82,18 +82,60 @@ var getAccountInfoResponseProviderData1 = null; var getAccountInfoResponseProviderData2 = null; // A sample JWT, along with its decoded contents. var idTokenGmail = { - jwt: 'HEADER.ew0KICAiaXNzIjogIkdJVGtpdCIsDQogICJleHAiOiAxMzI2NDM5' + - 'MDQ0LA0KICAic3ViIjogIjY3OSIsDQogICJhdWQiOiAiMjA0MjQxNjMxNjg2IiwNCiAgImZl' + - 'ZGVyYXRlZF9pZCI6ICJodHRwczovL3d3dy5nb29nbGUuY29tL2FjY291bnRzLzEyMzQ1Njc4' + - 'OSIsDQogICJwcm92aWRlcl9pZCI6ICJnbWFpbC5jb20iLA0KICAiZW1haWwiOiAidGVzdDEy' + - 'MzQ1NkBnbWFpbC5jb20iDQp9.SIGNATURE', + jwt: 'HEADER.eyJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vcHJvamVjdE' + + 'lkIiwiYXV0aF90aW1lIjoxNTIyNzE1MzI1LCJzdWIiOiI2NzkiLCJhdWQiOiJwcm9qZWN' + + '0SWQiLCJpYXQiOjE1MjI3NzY4MDcsImV4cCI6MTUyMjc4MDU3NSwiZmVkZXJhdGVkX2lk' + + 'IjoiaHR0cHM6Ly93d3cuZ29vZ2xlLmNvbS9hY2NvdW50cy8xMjM0NTY3ODkiLCJwcm92a' + + 'WRlcl9pZCI6ImdtYWlsLmNvbSIsImVtYWlsIjoidGVzdDEyMzQ1NkBnbWFpbC5jb20iLC' + + 'JmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7ImVtYWlsIjpbInRlc3QxMjM0NTZAZ21haWw' + + 'uY29tIl19LCJzaWduX2luX3Byb3ZpZGVyIjoicGFzc3dvcmQifX0.SIGNATURE', data: { - exp: 1326439044, + iss: 'https://securetoken.google.com/projectId', + auth_time: 1522715325, sub: '679', - aud: '204241631686', + aud: 'projectId', + iat: 1522776807, + exp: 1522780575, provider_id: 'gmail.com', email: 'test123456@gmail.com', - federated_id: 'https://www.google.com/accounts/123456789' + federated_id: 'https://www.google.com/accounts/123456789', + firebase: { + identities: { + email: [ + 'test123456@gmail.com' + ] + }, + sign_in_provider: 'password' + } + } +}; +var idTokenCustomClaims = { + jwt: 'HEADER.eyJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vcHJvamVjdE' + + 'lkIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImF1ZCI6InByb2plY3RJZCI' + + 'sImF1dGhfdGltZSI6MTUyMjcxNTMyNSwic3ViIjoibmVwMnV3TkNLNFBxanZvS2piMElu' + + 'VkpIbEdpMSIsImlhdCI6MTUyMjc3NjgwNywiZXhwIjoxNTIyNzgwNTc1LCJlbWFpbCI6I' + + 'nRlc3R1c2VyQGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJmaXJlYmFzZS' + + 'I6eyJpZGVudGl0aWVzIjp7ImVtYWlsIjpbInRlc3R1c2VyQGdtYWlsLmNvbSJdfSwic2l' + + 'nbl9pbl9wcm92aWRlciI6InBhc3N3b3JkIn19.SIGNATURE', + data: { + iss: 'https://securetoken.google.com/projectId', + name: 'John Doe', + admin: true, + aud: 'projectId', + auth_time: 1522715325, + sub: 'nep2uwNCK4PqjvoKjb0InVJHlGi1', + iat: 1522776807, + exp: 1522780575, + email: "testuser@gmail.com", + email_verified: true, + firebase: { + identities: { + email: [ + 'testuser@gmail.com' + ] + }, + sign_in_provider: 'password' + } } }; var stsTokenResponse = { @@ -1180,6 +1222,213 @@ function testExtractLinkedAccounts_withoutEmail() { } +function testUser_getIdTokenResult() { + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + asyncTestCase.waitForSignals(1); + // Test with available token. + stubs.replace( + fireauth.AuthUser.prototype, + 'getIdToken', + function(opt_forceRefresh) { + return goog.Promise.resolve(idTokenCustomClaims.jwt); + }); + user.getIdTokenResult().then(function(idTokenResult) { + fireauth.common.testHelper.assertIdTokenResult( + idTokenResult, + idTokenCustomClaims.jwt, + 1522780575, + 1522715325, + 1522776807, + 'password', + idTokenCustomClaims.data); + asyncTestCase.signal(); + }); +} + + +function testUser_getIdTokenResult_forceRefresh() { + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + asyncTestCase.waitForSignals(1); + // Test with available token. + stubs.replace( + fireauth.AuthUser.prototype, + 'getIdToken', + function(opt_forceRefresh) { + assertTrue(opt_forceRefresh); + return goog.Promise.resolve(idTokenCustomClaims.jwt); + }); + user.getIdTokenResult(true).then(function(idTokenResult) { + fireauth.common.testHelper.assertIdTokenResult( + idTokenResult, + idTokenCustomClaims.jwt, + 1522780575, + 1522715325, + 1522776807, + 'password', + idTokenCustomClaims.data); + asyncTestCase.signal(); + }); +} + + +function testUser_getIdTokenResult_error() { + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + asyncTestCase.waitForSignals(1); + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR); + stubs.replace( + fireauth.AuthUser.prototype, + 'getIdToken', + function(opt_forceRefresh) { + return goog.Promise.reject(expectedError); + }); + user.getIdTokenResult().thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testUser_getIdTokenResult_invalidToken() { + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + asyncTestCase.waitForSignals(1); + stubs.replace( + fireauth.AuthUser.prototype, + 'getIdToken', + function(opt_forceRefresh) { + return goog.Promise.resolve('gegege.invalid.ggrgheh'); + }); + user.getIdTokenResult().thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR, + 'An internal error occurred. The token obtained by Firebase '+ + 'appears to be malformed. Please retry the operation.'), + error); + asyncTestCase.signal(); + }); +} + + +function testUser_getIdTokenResult_expiredToken_reauth() { + // Test when token is expired and user is reauthenticated. + // User should be validated after, even though user invalidation event is + // triggered. + var validTokenResponse = { + 'idToken': expectedReauthenticateTokenResponse['idToken'], + 'accessToken': expectedReauthenticateTokenResponse['idToken'], + 'refreshToken': expectedReauthenticateTokenResponse['refreshToken'], + 'expiresIn': 3600 + }; + var credential = /** @type {!fireauth.AuthCredential} */ ({ + matchIdTokenWithUid: function() { + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(idToken) { + return goog.Promise.resolve(getAccountInfoResponse); + }); + return goog.Promise.resolve(expectedReauthenticateTokenResponse); + } + }); + // Expected token expired error. + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.TOKEN_EXPIRED); + // Event trackers. + var stateChangeCounter = 0; + var authChangeCounter = 0; + var userInvalidateCounter = 0; + accountInfo['uid'] = '679'; + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + // Track token changes. + goog.events.listen( + user, fireauth.UserEventType.TOKEN_CHANGED, function(event) { + authChangeCounter++; + }); + // Track user invalidation events. + goog.events.listen( + user, fireauth.UserEventType.USER_INVALIDATED, function(event) { + userInvalidateCounter++; + }); + // State change should be triggered. + user.addStateChangeListener(function(userTemp) { + stateChangeCounter++; + return goog.Promise.resolve(); + }); + asyncTestCase.waitForSignals(1); + // Stub token manager. + stubs.replace( + fireauth.StsTokenManager.prototype, + 'getToken', + function(opt_forceRefresh) { + // Resolve if new refresh token provided. + if (this.getRefreshToken() == validTokenResponse['refreshToken']) { + return goog.Promise.resolve(validTokenResponse); + } + // Reject otherwise. + return goog.Promise.reject(expectedError); + }); + // Confirm expected initial refresh token set on user. + assertEquals(tokenResponse['refreshToken'], user['refreshToken']); + // Call getIdToken, it should trigger the expected error and a state change + // event. + user.getIdTokenResult().thenCatch(function(error) { + // Refresh token nullified. + assertEquals(tokenResponse['refreshToken'], user['refreshToken']); + // Confirm expected error. + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + // No state change. + assertEquals(0, stateChangeCounter); + // No Auth change. + assertEquals(0, authChangeCounter); + // User invalidated change. + assertEquals(1, userInvalidateCounter); + // Call again, it should not trigger another state change or any other + // event. + user.getIdTokenResult().thenCatch(function(error) { + // Resolves with same error. + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + // No additional change. + assertEquals(0, stateChangeCounter); + assertEquals(0, authChangeCounter); + assertEquals(1, userInvalidateCounter); + // Assume user reauthenticated. + // This should resolve. + user.reauthenticateAndRetrieveDataWithCredential(credential) + .then(function(result) { + // Set via reauthentication. + assertEquals( + validTokenResponse['refreshToken'], user['refreshToken']); + // Auth token change triggered. + assertEquals(1, authChangeCounter); + // State change triggers, after reauthentication. + assertEquals(1, stateChangeCounter); + // Shouldn't trigger again. + assertEquals(1, userInvalidateCounter); + // This should return cached token set via reauthentication. + user.getIdTokenResult().then(function(idTokenResult) { + // Shouldn't trigger again. + assertEquals(1, authChangeCounter); + assertEquals(1, stateChangeCounter); + assertEquals(1, userInvalidateCounter); + // Refresh token should be updated along with ID token. + assertEquals( + validTokenResponse['refreshToken'], user['refreshToken']); + fireauth.common.testHelper.assertIdTokenResult( + idTokenResult, + idTokenGmail.jwt, + 1522780575, + 1522715325, + 1522776807, + 'password', + idTokenGmail.data); + asyncTestCase.signal(); + }); + }); + }); + }); +} + + function testUser_getIdToken() { user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); goog.events.listen( diff --git a/packages/auth/test/idtoken_test.js b/packages/auth/test/idtoken_test.js index 058e2a64438..e2775a1b8e6 100644 --- a/packages/auth/test/idtoken_test.js +++ b/packages/auth/test/idtoken_test.js @@ -106,6 +106,32 @@ var tokenPhone = 'HEAD.ew0KICAiaXNzIjogImh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLm' + '5lIg0KICB9DQp9.SIGNATURE'; +// "iss": "https://securetoken.google.com/projectId", +// "name": "John Doe", +// "admin": true, +// "aud": "projectId", +// "auth_time": 1522715325, +// "sub": "nep2uwNCK4PqjvoKjb0InVJHlGi1", +// "iat": 1522776807, +// "exp": 1522780575, +// "email": "testuser@gmail.com", +// "email_verified": true, +// "firebase": { +// "identities": { +// "email": [ +// "testuser@gmail.com" +// ] +// }, +// "sign_in_provider": "password" +var tokenCustomClaim = 'HEAD.eyJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5j' + + 'b20vcHJvamVjdElkIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImF1ZCI6InBy' + + 'b2plY3RJZCIsImF1dGhfdGltZSI6MTUyMjcxNTMyNSwic3ViIjoibmVwMnV3TkNLNFBxanZv' + + 'S2piMEluVkpIbEdpMSIsImlhdCI6MTUyMjc3NjgwNywiZXhwIjoxNTIyNzgwNTc1LCJlbWFp' + + 'bCI6InRlc3R1c2VyQGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJmaXJlYmFz' + + 'ZSI6eyJpZGVudGl0aWVzIjp7ImVtYWlsIjpbInRlc3R1c2VyQGdtYWlsLmNvbSJdfSwic2ln' + + 'bl9pbl9wcm92aWRlciI6InBhc3N3b3JkIn19.SIGNATURE'; + + /** * Asserts the values in the token provided. * @param {!fireauth.IdToken} token The ID token to assert. @@ -240,3 +266,60 @@ function testParse_phoneAndFirebaseProviderId() { '+11234567890'); assertEquals('https://securetoken.google.com/projectId', token.getIssuer()); } + + +function testParseIdTokenClaims_invalid() { + assertNull(fireauth.IdToken.parseIdTokenClaims('gegege.invalid.ggrgheh')); +} + + +function testParseIdTokenClaims_null() { + assertNull(fireauth.IdToken.parseIdTokenClaims(null)); +} + + +function testParseIdTokenClaims() { + var tokenJSON = fireauth.IdToken.parseIdTokenClaims( + tokenGoogleWithFederatedId); + assertObjectEquals( + { + 'iss': 'https://identitytoolkit.google.com/', + 'aud': '12345678.apps.googleusercontent.com', + 'iat': 1441246088, + 'exp': 2442455688, + 'sub': '1458474', + 'email': 'testuser@gmail.com', + 'provider_id': 'google.com', + 'verified': true, + 'display_name': 'John Doe', + 'photo_url': 'https://lh5.googleusercontent.com/1458474/photo.jpg' + }, + tokenJSON); +} + + +function testParseIdTokenClaims_customClaims() { + var tokenJSON = fireauth.IdToken.parseIdTokenClaims(tokenCustomClaim); + assertObjectEquals( + { + 'iss': 'https://securetoken.google.com/projectId', + 'name': 'John Doe', + 'admin': true, + 'aud': 'projectId', + 'auth_time': 1522715325, + 'sub': 'nep2uwNCK4PqjvoKjb0InVJHlGi1', + 'iat': 1522776807, + 'exp': 1522780575, + 'email': "testuser@gmail.com", + 'email_verified': true, + 'firebase': { + 'identities': { + 'email': [ + 'testuser@gmail.com' + ] + }, + 'sign_in_provider': 'password' + } + }, + tokenJSON); +} diff --git a/packages/auth/test/idtokenresult_test.js b/packages/auth/test/idtokenresult_test.js new file mode 100644 index 00000000000..675cd1efb7e --- /dev/null +++ b/packages/auth/test/idtokenresult_test.js @@ -0,0 +1,142 @@ +/** + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + /** + * @fileoverview Tests for idtoken.js + */ + +goog.provide('fireauth.IdTokenResultTest'); + +goog.require('fireauth.AuthError'); +goog.require('fireauth.IdTokenResult'); +goog.require('fireauth.authenum.Error'); +goog.require('fireauth.common.testHelper'); +goog.require('goog.testing.jsunit'); + +goog.setTestOnly(); + + +function testIdTokenResult() { + // "iss": "https://securetoken.google.com/projectId", + // "name": "John Doe", + // "admin": true, + // "aud": "projectId", + // "auth_time": 1522715325, + // "sub": "nep2uwNCK4PqjvoKjb0InVJHlGi1", + // "iat": 1522776807, + // "exp": 1522780575, + // "email": "testuser@gmail.com", + // "email_verified": true, + // "firebase": { + // "identities": { + // "email": [ + // "testuser@gmail.com" + // ] + // }, + // "sign_in_provider": "password" + var tokenString = 'HEADER.eyJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb' + + '20vcHJvamVjdElkIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImF1ZCI6InB' + + 'yb2plY3RJZCIsImF1dGhfdGltZSI6MTUyMjcxNTMyNSwic3ViIjoibmVwMnV3TkNLNFBxa' + + 'nZvS2piMEluVkpIbEdpMSIsImlhdCI6MTUyMjc3NjgwNywiZXhwIjoxNTIyNzgwNTc1LCJ' + + 'lbWFpbCI6InRlc3R1c2VyQGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJma' + + 'XJlYmFzZSI6eyJpZGVudGl0aWVzIjp7ImVtYWlsIjpbInRlc3R1c2VyQGdtYWlsLmNvbSJ' + + 'dfSwic2lnbl9pbl9wcm92aWRlciI6InBhc3N3b3JkIn19.SIGNATURE'; + var idTokenResult = new fireauth.IdTokenResult(tokenString); + fireauth.common.testHelper.assertIdTokenResult( + idTokenResult, + tokenString, + 1522780575, + 1522715325, + 1522776807, + 'password', + { + 'iss': 'https://securetoken.google.com/projectId', + 'name': 'John Doe', + 'admin': true, + 'aud': 'projectId', + 'auth_time': 1522715325, + 'sub': 'nep2uwNCK4PqjvoKjb0InVJHlGi1', + 'iat': 1522776807, + 'exp': 1522780575, + 'email': "testuser@gmail.com", + 'email_verified': true, + 'firebase': { + 'identities': { + 'email': [ + 'testuser@gmail.com' + ] + }, + 'sign_in_provider': 'password' + } + }); +} + + +function testIdTokenResult_invalid() { + var tokenString = 'gegege.invalid.ggrgheh'; + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR, + 'An internal error occurred. The token obtained by Firebase appears ' + + 'to be malformed. Please retry the operation.'); + var error = assertThrows(function() { + new fireauth.IdTokenResult(tokenString); + }); + fireauth.common.testHelper.assertErrorEquals(expectedError, error); +} + + +function testIdTokenResult_null() { + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR, + 'An internal error occurred. The token obtained by Firebase appears ' + + 'to be malformed. Please retry the operation.'); + var error = assertThrows(function() { + new fireauth.IdTokenResult(null); + }); + fireauth.common.testHelper.assertErrorEquals(expectedError, error); +} + + +function testIdTokenResult_missingRequiredFields() { + // "iss": "https://securetoken.google.com/projectId", + // "name": "John Doe", + // "admin": true, + // "aud": "projectId", + // "sub": "nep2uwNCK4PqjvoKjb0InVJHlGi1", + // "email": "testuser@gmail.com", + // "email_verified": true, + // "firebase": { + // "identities": { + // "email": [ + // "testuser@gmail.com" + // ] + // }, + // "sign_in_provider": "password" + var tokenString = 'HEADER.eyJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb' + + '20vcHJvamVjdElkIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImF1ZCI6InB' + + 'yb2plY3RJZCIsInN1YiI6Im5lcDJ1d05DSzRQcWp2b0tqYjBJblZKSGxHaTEiLCJlbWFpb' + + 'CI6InRlc3R1c2VyQGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJmaXJlYmF' + + 'zZSI6eyJpZGVudGl0aWVzIjp7ImVtYWlsIjpbInRlc3R1c2VyQGdtYWlsLmNvbSJdfSwic' + + '2lnbl9pbl9wcm92aWRlciI6InBhc3N3b3JkIn19.SIGNATURE'; + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR, + 'An internal error occurred. The token obtained by Firebase appears ' + + 'to be malformed. Please retry the operation.'); + var error = assertThrows(function() { + new fireauth.IdTokenResult(tokenString); + }); + fireauth.common.testHelper.assertErrorEquals(expectedError, error); +} diff --git a/packages/auth/test/testhelper.js b/packages/auth/test/testhelper.js index b8de22915b3..bf8d64c9fc8 100644 --- a/packages/auth/test/testhelper.js +++ b/packages/auth/test/testhelper.js @@ -22,6 +22,7 @@ goog.provide('fireauth.common.testHelper'); goog.require('fireauth.storage.Factory'); +goog.require('fireauth.util'); goog.require('goog.Promise'); goog.setTestOnly('fireauth.common.testHelper'); @@ -157,6 +158,35 @@ fireauth.common.testHelper.assertUserStorage = }; +/** + * @param {!fireauth.IdTokenResult} idTokenResult The ID token result to assert. + * @param {?string} token The expected token string. + * @param {?number} expirationTime The expected expiration time in seconds. + * @param {?number} authTime The expected auth time in seconds. + * @param {?number} issuedAtTime The expected issued time in seconds. + * @param {?string} signInProvider The expected sign-in provider. + * @param {!Object} claims The expected payload claims . + */ +fireauth.common.testHelper.assertIdTokenResult = function ( + idTokenResult, + token, + expirationTime, + authTime, + issuedAtTime, + signInProvider, + claims) { + assertEquals(token, idTokenResult['token']); + assertEquals(fireauth.util.utcTimestampToDateString(expirationTime * 1000), + idTokenResult['expirationTime']); + assertEquals(fireauth.util.utcTimestampToDateString(authTime * 1000), + idTokenResult['authTime']); + assertEquals(fireauth.util.utcTimestampToDateString(issuedAtTime * 1000), + idTokenResult['issuedAtTime']); + assertEquals(signInProvider, idTokenResult['signInProvider']); + assertObjectEquals(claims, idTokenResult['claims']); +}; + + /** * Installs different persistent/temporary storage using the provided mocks. * @param {!goog.testing.PropertyReplacer} stub The property replacer. diff --git a/packages/firebase/index.d.ts b/packages/firebase/index.d.ts index 7f04594d4d2..eb47599e5fa 100644 --- a/packages/firebase/index.d.ts +++ b/packages/firebase/index.d.ts @@ -36,6 +36,7 @@ declare namespace firebase { interface User extends firebase.UserInfo { delete(): Promise; emailVerified: boolean; + getIdTokenResult(forceRefresh?: boolean): Promise; getIdToken(forceRefresh?: boolean): Promise; getToken(forceRefresh?: boolean): Promise; isAnonymous: boolean; @@ -296,6 +297,15 @@ declare namespace firebase.auth { ): firebase.auth.AuthProvider; } + type IdTokenResult = { + token: string; + expirationTime: string; + authTime: string; + issuedAtTime: string; + signInProvider: string | null; + claims: Object; + }; + class PhoneAuthProvider extends PhoneAuthProvider_Instance { static PROVIDER_ID: string; static PHONE_SIGN_IN_METHOD: string; From a0fd500dbecc865f628498a26107c5a99e9fe9b2 Mon Sep 17 00:00:00 2001 From: Ti Wang Date: Fri, 13 Apr 2018 10:21:55 -0700 Subject: [PATCH 2/8] idtoken type declaration --- packages/auth-types/index.d.ts | 8 +++++--- packages/firebase/index.d.ts | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/auth-types/index.d.ts b/packages/auth-types/index.d.ts index e16b1945337..bb221213ebd 100644 --- a/packages/auth-types/index.d.ts +++ b/packages/auth-types/index.d.ts @@ -20,7 +20,7 @@ import { Observer, Unsubscribe } from '@firebase/util'; export interface User extends UserInfo { delete(): Promise; emailVerified: boolean; - getIdTokenResult(forceRefresh?: boolean): Promise; + getIdTokenResult(forceRefresh?: boolean): Promise; getIdToken(forceRefresh?: boolean): Promise; getToken(forceRefresh?: boolean): Promise; isAnonymous: boolean; @@ -161,13 +161,15 @@ export class GoogleAuthProvider_Instance implements AuthProvider { setCustomParameters(customOAuthParameters: Object): AuthProvider; } -export type IdTokenResult = { +export interface IdTokenResult = { token: string; expirationTime: string; authTime: string; issuedAtTime: string; signInProvider: string | null; - claims: Object; + claims: { + [key: string]: any; + }; }; export class OAuthProvider implements AuthProvider { diff --git a/packages/firebase/index.d.ts b/packages/firebase/index.d.ts index eb47599e5fa..cf7aefe0e10 100644 --- a/packages/firebase/index.d.ts +++ b/packages/firebase/index.d.ts @@ -36,7 +36,7 @@ declare namespace firebase { interface User extends firebase.UserInfo { delete(): Promise; emailVerified: boolean; - getIdTokenResult(forceRefresh?: boolean): Promise; + getIdTokenResult(forceRefresh?: boolean): Promise; getIdToken(forceRefresh?: boolean): Promise; getToken(forceRefresh?: boolean): Promise; isAnonymous: boolean; @@ -297,13 +297,15 @@ declare namespace firebase.auth { ): firebase.auth.AuthProvider; } - type IdTokenResult = { + interface IdTokenResult = { token: string; expirationTime: string; authTime: string; issuedAtTime: string; signInProvider: string | null; - claims: Object; + claims: { + [key: string]: any; + }; }; class PhoneAuthProvider extends PhoneAuthProvider_Instance { From 20895004049083705ed3d9534a903965fcc4a8a5 Mon Sep 17 00:00:00 2001 From: Ti Wang Date: Fri, 13 Apr 2018 10:24:21 -0700 Subject: [PATCH 3/8] idtoken type declaration --- packages/auth-types/index.d.ts | 2 +- packages/firebase/index.d.ts | 2 +- packages/firestore/src/api/user_data_converter.ts | 5 +---- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/auth-types/index.d.ts b/packages/auth-types/index.d.ts index bb221213ebd..f63386b740d 100644 --- a/packages/auth-types/index.d.ts +++ b/packages/auth-types/index.d.ts @@ -161,7 +161,7 @@ export class GoogleAuthProvider_Instance implements AuthProvider { setCustomParameters(customOAuthParameters: Object): AuthProvider; } -export interface IdTokenResult = { +export interface IdTokenResult { token: string; expirationTime: string; authTime: string; diff --git a/packages/firebase/index.d.ts b/packages/firebase/index.d.ts index 5f0c04c94de..90daabe9fad 100644 --- a/packages/firebase/index.d.ts +++ b/packages/firebase/index.d.ts @@ -297,7 +297,7 @@ declare namespace firebase.auth { ): firebase.auth.AuthProvider; } - interface IdTokenResult = { + interface IdTokenResult { token: string; expirationTime: string; authTime: string; diff --git a/packages/firestore/src/api/user_data_converter.ts b/packages/firestore/src/api/user_data_converter.ts index b55cf038d78..af4f9b46ebf 100644 --- a/packages/firestore/src/api/user_data_converter.ts +++ b/packages/firestore/src/api/user_data_converter.ts @@ -556,10 +556,7 @@ export class UserDataConverter { * * @return The parsed value */ - private parseScalarValue( - value: AnyJs, - context: ParseContext - ): FieldValue { + private parseScalarValue(value: AnyJs, context: ParseContext): FieldValue { if (value === null) { return NullValue.INSTANCE; } else if (typeof value === 'number') { From 382a0e187ebb932b1e59082dd082f3360c639637 Mon Sep 17 00:00:00 2001 From: Ti Wang Date: Fri, 13 Apr 2018 10:25:06 -0700 Subject: [PATCH 4/8] [AUTOMATED]: Prettier Code Styling --- packages/auth-types/index.d.ts | 2 +- packages/firebase/index.d.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/auth-types/index.d.ts b/packages/auth-types/index.d.ts index f63386b740d..18fa9de777a 100644 --- a/packages/auth-types/index.d.ts +++ b/packages/auth-types/index.d.ts @@ -170,7 +170,7 @@ export interface IdTokenResult { claims: { [key: string]: any; }; -}; +} export class OAuthProvider implements AuthProvider { providerId: string; diff --git a/packages/firebase/index.d.ts b/packages/firebase/index.d.ts index 90daabe9fad..3624ccb56d2 100644 --- a/packages/firebase/index.d.ts +++ b/packages/firebase/index.d.ts @@ -306,7 +306,7 @@ declare namespace firebase.auth { claims: { [key: string]: any; }; - }; + } class PhoneAuthProvider extends PhoneAuthProvider_Instance { static PROVIDER_ID: string; From 3d5764aa648d90c5a3755501fbdb706cffe9bff4 Mon Sep 17 00:00:00 2001 From: Ti Wang Date: Fri, 13 Apr 2018 10:56:08 -0700 Subject: [PATCH 5/8] style issue fix --- packages/firestore/src/api/user_data_converter.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/firestore/src/api/user_data_converter.ts b/packages/firestore/src/api/user_data_converter.ts index af4f9b46ebf..c62533b9b42 100644 --- a/packages/firestore/src/api/user_data_converter.ts +++ b/packages/firestore/src/api/user_data_converter.ts @@ -556,7 +556,10 @@ export class UserDataConverter { * * @return The parsed value */ - private parseScalarValue(value: AnyJs, context: ParseContext): FieldValue { + private parseScalarValue( + value: AnyJs, + context: ParseContext + ): FieldValue { if (value === null) { return NullValue.INSTANCE; } else if (typeof value === 'number') { From 8d8f2fed29256b123c615bee4205edbb6330bb2a Mon Sep 17 00:00:00 2001 From: Ti Wang Date: Fri, 13 Apr 2018 10:56:42 -0700 Subject: [PATCH 6/8] [AUTOMATED]: Prettier Code Styling --- packages/firestore/src/api/user_data_converter.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/firestore/src/api/user_data_converter.ts b/packages/firestore/src/api/user_data_converter.ts index c62533b9b42..af4f9b46ebf 100644 --- a/packages/firestore/src/api/user_data_converter.ts +++ b/packages/firestore/src/api/user_data_converter.ts @@ -556,10 +556,7 @@ export class UserDataConverter { * * @return The parsed value */ - private parseScalarValue( - value: AnyJs, - context: ParseContext - ): FieldValue { + private parseScalarValue(value: AnyJs, context: ParseContext): FieldValue { if (value === null) { return NullValue.INSTANCE; } else if (typeof value === 'number') { From 9ce0dc09ea2bac9bae64e6c8c957d08a1f12aacc Mon Sep 17 00:00:00 2001 From: Ti Wang Date: Fri, 13 Apr 2018 11:52:59 -0700 Subject: [PATCH 7/8] add namesapce --- packages/firebase/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/firebase/index.d.ts b/packages/firebase/index.d.ts index 3624ccb56d2..e188cf5d5f7 100644 --- a/packages/firebase/index.d.ts +++ b/packages/firebase/index.d.ts @@ -36,7 +36,7 @@ declare namespace firebase { interface User extends firebase.UserInfo { delete(): Promise; emailVerified: boolean; - getIdTokenResult(forceRefresh?: boolean): Promise; + getIdTokenResult(forceRefresh?: boolean): Promise; getIdToken(forceRefresh?: boolean): Promise; getToken(forceRefresh?: boolean): Promise; isAnonymous: boolean; From 209a62e2e9d9a6387f9b5101c4dc5b772deb204f Mon Sep 17 00:00:00 2001 From: Ti Wang Date: Fri, 13 Apr 2018 11:53:52 -0700 Subject: [PATCH 8/8] [AUTOMATED]: Prettier Code Styling --- packages/firebase/index.d.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/firebase/index.d.ts b/packages/firebase/index.d.ts index e188cf5d5f7..a152de12484 100644 --- a/packages/firebase/index.d.ts +++ b/packages/firebase/index.d.ts @@ -36,7 +36,9 @@ declare namespace firebase { interface User extends firebase.UserInfo { delete(): Promise; emailVerified: boolean; - getIdTokenResult(forceRefresh?: boolean): Promise; + getIdTokenResult( + forceRefresh?: boolean + ): Promise; getIdToken(forceRefresh?: boolean): Promise; getToken(forceRefresh?: boolean): Promise; isAnonymous: boolean;