diff --git a/.gitignore b/.gitignore index bc5c909b..f716c972 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ node_modules/ generated/ out/ dist/ +.firebase # Generated files. *.log diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29b..464e88f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1,2 @@ +* Migrated to new one-tap sign-up API. +* Fixed issue when existing email link user tries to sign in when only email/password sign-in method is provided. \ No newline at end of file diff --git a/README.md b/README.md index 1a120fb0..b1e39727 100644 --- a/README.md +++ b/README.md @@ -141,8 +141,10 @@ FirebaseUI includes the following flows: by default.) 6. [Account Chooser](https://www.accountchooser.com/learnmore.html?lang=en) for remembering emails -7. Ability to upgrade anonymous users through sign-in/sign-up -8. Sign-in as a guest +7. Integration with +[one-tap sign-up](https://developers.google.com/identity/one-tap/web/) +8. Ability to upgrade anonymous users through sign-in/sign-up. +9. Sign-in as a guest ### Configuring sign-in providers @@ -452,6 +454,7 @@ When one is enabled, your users will be prompted with email addresses and usernames they have saved from your app or other applications. FirebaseUI supports the following credential helpers: +- [one-tap sign-up](https://developers.google.com/identity/one-tap/web/) - [accountchooser.com](https://www.accountchooser.com/learnmore.html) #### accountchooser.com @@ -463,9 +466,70 @@ website and will be able to select one of their saved accounts. You can disable it by specifying the value below. This feature is always disabled for non HTTP/HTTPS environments. +#### One-tap sign-up + +[One-tap sign-up](https://developers.google.com/identity/one-tap/web/) +provides seamless authentication flows to +your users with Google's one tap sign-up and automatic sign-in APIs. +With one tap sign-up, users are prompted to create an account with a dialog +that's inline with FirebaseUI NASCAR screen. With just one tap, they get a +secure, token-based, passwordless account with your service, protected by their +Google Account. As the process is frictionless, users are much more likely to +register. +Returning users are signed in automatically, even when they switch devices or +platforms, or after their session expires. +One-tap sign-up integrates with FirebaseUI and if you request Google OAuth +scopes, you will still get back the expected Google OAuth access token even if +the user goes through the one-tap flow. However, in that case 'redirect' flow is +always used even when 'popup' is specified. +In addition, if you choose to force prompt for Google sign-in, one-tap auto +sign-in will be automatically disabled. +One-tap is an additive feature and is only supported in the latest evergreen +modern browser environments. +For more information on how to configure one-tap sign-up, refer to the +[one-tap get started guide](https://developers.google.com/identity/one-tap/web/guides/get-google-api-clientid). + +The following example shows how to configure one-tap sign-up with FirebaseUI. +Along with the corresponding one-tap `credentialHelper`, the Google OAuth +`clientId` has to be provided with the Firebase Google provider: + +```javascript +ui.start('#firebaseui-auth-container', { + signInOptions: [ + { + // Google provider must be enabled in Firebase Console to support one-tap + // sign-up. + provider: firebase.auth.GoogleAuthProvider.PROVIDER_ID, + // Required to enable ID token credentials for this provider. + // This can be obtained from the Credentials page of the Google APIs + // console. Use the same OAuth client ID used for the Google provider + // configured with GCIP or Firebase Auth. + clientId: 'xxxxxxxxxxxxxxxxx.apps.googleusercontent.com' + }, + firebase.auth.FacebookAuthProvider.PROVIDER_ID, + firebase.auth.TwitterAuthProvider.PROVIDER_ID, + firebase.auth.GithubAuthProvider.PROVIDER_ID, + firebase.auth.EmailAuthProvider.PROVIDER_ID, + ], + // Required to enable one-tap sign-up credential helper. + credentialHelper: firebaseui.auth.CredentialHelper.GOOGLE_YOLO +}); +// Auto sign-in for returning users is enabled by default except when prompt is +// not 'none' in the Google provider custom parameters. To manually disable: +ui.disableAutoSignIn(); +``` + +Auto sign-in for returning users can be disabled by calling +`ui.disableAutoSignIn()`. This may be needed if the FirebaseUI sign-in page is +being rendered after the user signs out. + +To see FirebaseUI in action with one-tap sign-up, check out the FirebaseUI +[demo app](https://fir-ui-demo-84a6c.firebaseapp.com/). + |Credential Helper |Value | |------------------|------------------------------------------------------| |accountchooser.com|`firebaseui.auth.CredentialHelper.ACCOUNT_CHOOSER_COM`| +|One-tap sign-up |`firebaseui.auth.CredentialHelper.GOOGLE_YOLO` | |None (disable) |`firebaseui.auth.CredentialHelper.NONE` | ### Available providers diff --git a/demo/public/app.js b/demo/public/app.js index 66cebf22..745a2027 100644 --- a/demo/public/app.js +++ b/demo/public/app.js @@ -42,8 +42,6 @@ function getUiConfig() { // TODO(developer): Remove the providers you don't need for your app. { provider: firebase.auth.GoogleAuthProvider.PROVIDER_ID, - // Required to enable this provider in One-Tap Sign-up. - authMethod: 'https://accounts.google.com', // Required to enable ID token credentials for this provider. clientId: CLIENT_ID }, diff --git a/demo/public/widget.html b/demo/public/widget.html index 8cf9c106..2f33c365 100644 --- a/demo/public/widget.html +++ b/demo/public/widget.html @@ -36,8 +36,6 @@ // TODO(developer): Remove the providers you don't need for your app. { provider: firebase.auth.GoogleAuthProvider.PROVIDER_ID, - // Required to enable this provider in One-Tap Sign-up. - authMethod: 'https://accounts.google.com', // Required to enable ID token credentials for this provider. clientId: CLIENT_ID }, diff --git a/javascript/externs/googleyolo.js b/javascript/externs/googleyolo.js index 73eed8cd..ada56213 100644 --- a/javascript/externs/googleyolo.js +++ b/javascript/externs/googleyolo.js @@ -1,5 +1,9 @@ /** * Smart Lock API externs. + * Note that this SDK is not dedicated to One-tap API. It seems to cover other + * Google sign-in related functionality. We may want to consider renaming + * these APIs to not be One-tap specific. + * https://developers.google.com/identity/one-tap/web/reference/js-reference * * @externs */ @@ -8,31 +12,17 @@ * @record * @struct */ -function SmartLockHintOptions() {} +function SmartLockOptions() {} -/** @type {!Array} */ -SmartLockHintOptions.prototype.supportedAuthMethods; - -/** @type {!Array>} */ -SmartLockHintOptions.prototype.supportedIdTokenProviders; - -/** @type {?string|undefined} */ -SmartLockHintOptions.prototype.context; - -/** - * @record - * @struct - */ -function SmartLockRequestOptions() {} +/** @type {string} */ +SmartLockOptions.prototype.client_id; -/** @type {!Array} */ -SmartLockRequestOptions.prototype.supportedAuthMethods; +/** @type {?boolean|undefined} */ +SmartLockOptions.prototype.auto_select; -/** @type {!Array>} */ -SmartLockRequestOptions.prototype.supportedIdTokenProviders; +/** @type {function(SmartLockCredential)} */ +SmartLockOptions.prototype.callback; -/** @type {?string|undefined} */ -SmartLockRequestOptions.prototype.context; /** * @record @@ -40,23 +30,14 @@ SmartLockRequestOptions.prototype.context; */ function SmartLockCredential() {} -/** @type {string} */ -SmartLockCredential.prototype.id; - -/** @type {string} */ -SmartLockCredential.prototype.authMethod; - /** @type {string|undefined} */ -SmartLockCredential.prototype.authDomain; +SmartLockCredential.prototype.credential; /** @type {string|undefined} */ -SmartLockCredential.prototype.displayName; +SmartLockCredential.prototype.clientId; /** @type {string|undefined} */ -SmartLockCredential.prototype.profilePicture; - -/** @type {string|undefined} */ -SmartLockCredential.prototype.idToken; +SmartLockCredential.prototype.select_by; /** * @record @@ -65,80 +46,21 @@ SmartLockCredential.prototype.idToken; function SmartLockApi() {} /** - * Requests the credential provider whether hints are available or not for - * the current user. - * - * @param {!SmartLockHintOptions} options - * Describes the types of credentials that are supported by the origin. - * @return {!Promise} - * A promise that resolves with true if at least one hint is available, - * and resolves with false if none are available. The promise will not - * reject: if an error happen, it should resolve with false. - */ -SmartLockApi.prototype.hintsAvailable = function(options) {}; - -/** - * Attempts to retrieve a sign-up hint that can be used to create a new - * user account. + * Initializes GSI sign in process. * - * @param {!SmartLockHintOptions} options + * @param {!SmartLockOptions} options * Describes the types of credentials that are supported by the origin, * and customization properties for the display of any UI pertaining to * releasing this credential. - * @return {!Promise} - * A promise for a credential hint. The promise will be rejected if the - * user cancels the hint selection process, if there are no hints available, - * or if an error happens. - */ -SmartLockApi.prototype.hint = function(options) {}; - -/** - * Attempts to retrieve a credential for the current origin. - * - * @param {!SmartLockRequestOptions} options - * Describes the types of credentials that are supported by the origin. - * @return {!Promise} - * A promise for the credential, which will be rejected if there are no - * credentials available or the user refuses to release the credential. - * Otherwise, the promise will resolve with a credential that the app - * can use. */ -SmartLockApi.prototype.retrieve = function(options) {}; +SmartLockApi.prototype.initialize = function(options) {}; /** - * Prevents the automatic release of a credential from the retrieve operation. - * This should be invoked when the user signs out, in order to prevent an - * automatic sign-in loop. This cannot be called while another operation is - * pending so should be called before retrieve. - * @return {!Promise} - * A promise for the completion of notifying the provider to disable - * automatic sign-in. + * Triggers the prompt to display. */ -SmartLockApi.prototype.disableAutoSignIn = function() {}; +SmartLockApi.prototype.prompt = function() {}; /** * Cancels the last operation triggered. - * @return {!Promise} - * A promise for the completion of the cancellation. */ -SmartLockApi.prototype.cancelLastOperation = function() {}; - -/** - * Sets a custom timeouts, in milliseconds, after which a request is - * considered failed. - * @param {number|null} timeoutMs The timeout in milliseconds. - */ -SmartLockApi.prototype.setTimeouts = function(timeoutMs) {}; - -/** - * Sets the render mode of the credentials selector, or null if the default - * should be used. Available render modes are: 'bottomSheet' and 'navPopout'. - * @param {string|null} renderMode - */ -SmartLockApi.prototype.setRenderMode = function(renderMode) {}; - -/** @type {!SmartLockApi} */ -var googleyolo; - -/** @type {function(!SmartLockApi)|undefined} */ -var onGoogleYoloLoad; +SmartLockApi.prototype.cancel = function() {}; diff --git a/javascript/utils/googleyolo.js b/javascript/utils/googleyolo.js index f071b1fc..62de3322 100644 --- a/javascript/utils/googleyolo.js +++ b/javascript/utils/googleyolo.js @@ -28,6 +28,14 @@ goog.require('goog.net.jsloader'); goog.require('goog.string.Const'); +/** @return {?SmartLockApi} The SmartLockApi handle if available. */ +function getGoogleAccountsId() { + return (goog.global[firebaseui.auth.GoogleYolo.NAMESPACE_] && + goog.global[firebaseui.auth.GoogleYolo.NAMESPACE_]['accounts'] && + goog.global[firebaseui.auth.GoogleYolo.NAMESPACE_]['accounts']['id']) || + null; +} + /** * The One-Tap sign-up API wrapper. */ @@ -42,148 +50,87 @@ firebaseui.auth.GoogleYolo = class { * @private {?SmartLockApi|undefined} The One-Tap instance reference. If no * reference is available, googleyolo will be lazy loaded dynamically. */ - this.googleyolo_ = - googleyolo || goog.global[firebaseui.auth.GoogleYolo.NAMESPACE_]; - /** - * @private {?Promise} The last cancellation promise. - */ - this.lastCancel_ = null; + this.googleyolo_ = googleyolo || getGoogleAccountsId(); /** * @private {boolean} Whether googleyolo UI has already been initialized. */ this.initialized_ = false; + /** + * @private {?function(?SmartLockCredential)} The callback to trigger when + * a credential is available or the flow is cancelled. + */ + this.callback_ = null; } /** Cancels any pending One-Tap operation if available. */ cancel() { // Call underlying googleyolo API if supported and previously initialized. - // There is also a current issue with One-Tap. It will always fail with - // noCredentialsAvailable error if cancelLastOperation is called before - // rendering. // If googleyolo is not yet loaded, there is no need to run cancel even // after loading since there is nothing to cancel. if (this.googleyolo_ && this.initialized_) { - this.lastCancel_ = - this.googleyolo_.cancelLastOperation().catch(function(error) { - // Suppress error. - }); + if (this.callback_) { + this.callback_(null); + } + this.googleyolo_.cancel(); } } /** * Shows the One-Tap UI if available and returns a promise that resolves with - * the selected googleyolo credential or null if not available. - * @param {?SmartLockRequestOptions} config The One-Tap configuration if - * available. + * the selected googleyolo credential. + * If no credential is available, the promise will never resolve. + * If flow is cancelled, promise will resolve with null. + * @param {?string} clientId The One-Tap client ID if available. * @param {boolean} autoSignInDisabled Whether auto sign-in is disabled. - * @return {!Promise} A promise that resolves when - * One-Tap sign in is dismissed or resolved. A googleyolo credential is - * returned in the process. + * @return {!goog.Promise} A promise that resolves when + * One-Tap sign in is resolved or cancel is called. A googleyolo + * credential is returned in the process. */ - show(config, autoSignInDisabled) { + show(clientId, autoSignInDisabled) { var self = this; - // Configuration available and googleyolo is available. - if (this.googleyolo_ && config) { + // Client ID available and googleyolo is available. + if (this.googleyolo_ && clientId) { // One-Tap UI renderer. var render = function() { - // UI initialized, it is OK to cancel last operation. + // UI initialized. self.initialized_ = true; - // retrieve is only called if auto sign-in is enabled. Otherwise, it - // will get skipped. - var retrieveCredential = Promise.resolve(null); - if (!autoSignInDisabled) { - retrieveCredential = - self.googleyolo_ - .retrieve( - /** @type {!SmartLockRequestOptions} */ (config)) - .catch(function(error) { - // For user cancellation or concurrent request pass down. - // Otherwise suppress and run hint. - if (error.type === - firebaseui.auth.GoogleYolo.Error.USER_CANCELED || - error.type === - firebaseui.auth.GoogleYolo.Error - .CONCURRENT_REQUEST) { - throw error; - } - // Ignore all other errors to give hint a chance to run - // next. - return null; - }); - } - // Check if a credential is already available (previously signed in - // with). - return retrieveCredential - .then(function(credential) { - if (!credential) { - // Auto sign-in not complete. - // Show account selector. - return self.googleyolo_.hint( - /** @type {!SmartLockHintOptions} */ (config)); - } - // Credential already available from the retrieve call. Pass it - // through. - return credential; - }) - .catch(function(error) { - // When user cancels the flow, reset the lastCancel promise and - // resolve with false. - if (error.type === - firebaseui.auth.GoogleYolo.Error.USER_CANCELED) { - self.lastCancel_ = Promise.resolve(); - } else if ( - error.type === - firebaseui.auth.GoogleYolo.Error.CONCURRENT_REQUEST) { - // Only one UI can be rendered at a time, cancel existing UI - // and try again. - self.cancel(); - return self.show(config, autoSignInDisabled); - } - // Return null as no credential is available. - return null; - }); + return new goog.Promise((resolve, reject) => { + self.callback_ = resolve; + self.googleyolo_.initialize({ + 'client_id': /** @type {string} */ (clientId), + 'callback': resolve, + 'auto_select': !autoSignInDisabled, + }); + self.googleyolo_.prompt(); + }); }; - // If there is a pending cancel operation, wait for it to complete. - // Otherwise, an error will be thrown. - if (this.lastCancel_) { - // render always catches the error. - return this.lastCancel_.then(render); - } else { - // No pending cancel operation. Render UI directly. - return render(); - } - } else if (config) { + return render(); + } else if (clientId) { // Try to dynamically load googleyolo dependencies. // If multiple calls of show are triggered successively, they would all // share the same loader pending promise. They would then cancel each // other successively due to concurrent requests. Only the last call will // succeed. var p = firebaseui.auth.GoogleYolo.Loader.getInstance() - .load() - .then(function() { - // Set googleyolo to prevent reloading again for future show - // calls. - self.googleyolo_ = - goog.global[firebaseui.auth.GoogleYolo.NAMESPACE_]; - // On success, retry to show. - return self.show(config, autoSignInDisabled); - }) - .thenCatch(function(error) { - // On failure, resolve with null. - return null; - }); + .load() + .then(function() { + // Set googleyolo to prevent reloading again for future show + // calls. + self.googleyolo_ = getGoogleAccountsId(); + // On success, retry to show. + return self.show(clientId, autoSignInDisabled); + }) + .thenCatch(function(error) { + // On failure, resolve with null. + return null; + }); // Cast from goog.Promise to native Promise. - return Promise.resolve(p); - } - // no-op operation, resolve with null. - if (typeof Promise !== 'undefined') { - // typecast added to bypass weird compiler issue. - return Promise.resolve(/** @type {?SmartLockCredential} */ (null)); + return goog.Promise.resolve(p); } // API not supported on older browsers. - throw new Error('One-Tap sign in not supported in the current browser!'); + return goog.Promise.resolve(null); } }; @@ -196,7 +143,7 @@ goog.addSingletonGetter(firebaseui.auth.GoogleYolo); * @const {string} * @private */ -firebaseui.auth.GoogleYolo.NAMESPACE_ = 'googleyolo'; +firebaseui.auth.GoogleYolo.NAMESPACE_ = 'google'; /** @@ -204,7 +151,7 @@ firebaseui.auth.GoogleYolo.NAMESPACE_ = 'googleyolo'; * @const {string} * @private */ -firebaseui.auth.GoogleYolo.CALLBACK_ = 'onGoogleYoloLoad'; +firebaseui.auth.GoogleYolo.CALLBACK_ = 'onGoogleLibraryLoad'; /** @@ -221,19 +168,7 @@ firebaseui.auth.GoogleYolo.LOAD_TIMEOUT_MS_ = 10000; * @private */ firebaseui.auth.GoogleYolo.GOOGLE_YOLO_SRC_ = goog.string.Const.from( - 'https://smartlock.google.com/client'); - - -/** - * The different One-Tap sign-up error types of interest. - * - * @enum {string} - */ -firebaseui.auth.GoogleYolo.Error = { - CONCURRENT_REQUEST: 'illegalConcurrentRequest', - NO_CREDENTIALS_AVAILABLE: 'noCredentialsAvailable', - USER_CANCELED: 'userCanceled' -}; + 'https://accounts.google.com/gsi/client'); /** @@ -262,12 +197,12 @@ firebaseui.auth.GoogleYolo.Loader = class { } var url = goog.html.TrustedResourceUrl.fromConstant( firebaseui.auth.GoogleYolo.GOOGLE_YOLO_SRC_); - if (!goog.global[firebaseui.auth.GoogleYolo.NAMESPACE_]) { + if (!getGoogleAccountsId()) { // Wait for DOM to be ready. this.loader_ = firebaseui.auth.util.onDomReady().then(function() { // In case it was still being loaded while DOM was not ready. // Resolve immediately. - if (goog.global[firebaseui.auth.GoogleYolo.NAMESPACE_]) { + if (getGoogleAccountsId()) { return; } return new goog.Promise(function(resolve, reject) { @@ -284,6 +219,13 @@ firebaseui.auth.GoogleYolo.Loader = class { }; // Load googleyolo dependency. goog.Promise.resolve(goog.net.jsloader.safeLoad(url)) + .then(() => { + // Callback does not always trigger. Trigger on load and + // google.accounts.id reference is available. + if (getGoogleAccountsId()) { + resolve(); + } + }) .thenCatch(function(error) { // On error, clear timer and nullify loader to allow retrial. clearTimeout(timer); diff --git a/javascript/utils/googleyolo_test.js b/javascript/utils/googleyolo_test.js index c922403b..26f58b9b 100644 --- a/javascript/utils/googleyolo_test.js +++ b/javascript/utils/googleyolo_test.js @@ -34,25 +34,13 @@ goog.setTestOnly('firebaseui.auth.GoogleYoloTest'); var mockControl; +var ignoreArgument; var clock; -// Mock googleyolo config. -var googleYoloConfig = { - 'supportedAuthMethods': [ - 'https://accounts.google.com', - 'googleyolo://id-and-password' - ], - 'supportedIdTokenProviders': [ - { - 'uri': 'https://accounts.google.com', - 'clientId': '1234567890.apps.googleusercontent.com' - } - ] -}; +var googleYoloClientId = '1234567890.apps.googleusercontent.com'; // Mock credential returned by googleyolo. var googleYoloCredential = { - 'idToken': 'ID_TOKEN', - 'id': 'user@example.com', - 'authMethod': 'https://accounts.google.com' + 'credential': 'ID_TOKEN', + 'clientId': googleYoloClientId, }; @@ -60,6 +48,7 @@ function setUp() { // Install mock clock. clock = new goog.testing.MockClock(true); mockControl = new goog.testing.MockControl(); + ignoreArgument = goog.testing.mockmatchers.ignoreArgument; } @@ -67,117 +56,56 @@ function tearDown() { goog.dispose(clock); mockControl.$verifyAll(); mockControl.$tearDown(); - delete goog.global['googleyolo']; - delete goog.global['onGoogleYoloLoad']; -} - - -/** - * Returns a googleyolo error for the type provided. - * @param {string} type The error type. - * @return {!Error} The corresponding googleyolo error. - */ -function createGoogleYoloError(type) { - var err = new Error; - err.type = type; - return err; + delete goog.global['google']; + delete goog.global['onGoogleLibraryLoad']; } /** @return {!SmartLockApi} A googleyolo mock object. */ function initializeGoogleYoloMock() { return { - 'cancelLastOperation': mockControl.createFunctionMock( - 'cancelLastOperation'), - 'retrieve': mockControl.createFunctionMock('retrieve'), - 'hint': mockControl.createFunctionMock('hint') + 'cancel': mockControl.createFunctionMock('cancel'), + 'initialize': mockControl.createFunctionMock('initialize'), + 'prompt': mockControl.createFunctionMock('prompt') }; } -function testGoogleYolo_show_autoSignInEnabled_noSavedCredentials() { - // Test when auto sign-in is enabled and no saved credentials are available. - // Ignore old browsers where googleyolo will not work. - if (typeof Promise === 'undefined') { - return; - } +function testGoogleYolo_show_autoSignInEnabled() { + // Test when auto sign-in is enabled. var mockGoogleYolo = initializeGoogleYoloMock(); - mockGoogleYolo.retrieve(googleYoloConfig).$once().$does(function() { - return Promise.reject(createGoogleYoloError('noCredentialsAvailable')); - }); - mockGoogleYolo.hint(googleYoloConfig).$once().$does(function() { - return Promise.resolve(googleYoloCredential); - }); - mockControl.$replayAll(); - - var googleYolo = new firebaseui.auth.GoogleYolo(mockGoogleYolo); - return googleYolo.show(googleYoloConfig, false) - .then(function(actualCredential) { - assertObjectEquals(googleYoloCredential, actualCredential); + mockGoogleYolo.initialize(ignoreArgument).$once() + .$does(function(config) { + assertEquals(googleYoloClientId, config.client_id); + assertTrue(config.auto_select); + config.callback(googleYoloCredential); }); -} - - -function testGoogleYolo_show_autoSignInEnabled_userCancelled_whileRetrieve() { - // Test when auto sign-in is enabled and user cancels the flow while - // retrieving the saved credential. - // Ignore old browsers where googleyolo will not work. - if (typeof Promise === 'undefined') { - return; - } - var mockGoogleYolo = initializeGoogleYoloMock(); - mockGoogleYolo.retrieve(googleYoloConfig).$once().$does(function() { - return Promise.reject(createGoogleYoloError('userCanceled')); - }); + mockGoogleYolo.prompt().$once(); mockControl.$replayAll(); var googleYolo = new firebaseui.auth.GoogleYolo(mockGoogleYolo); - return googleYolo.show(googleYoloConfig, false) + return googleYolo.show(googleYoloClientId, false) .then(function(actualCredential) { - assertNull(actualCredential); - }); -} - - -function testGoogleYolo_show_autoSignInEnabled_userCancelled_whileSelection() { - // Test when auto sign-in is enabled and user cancels the flow while - // selecting a credential from list of accounts. - // Ignore old browsers where googleyolo will not work. - if (typeof Promise === 'undefined') { - return; - } - var mockGoogleYolo = initializeGoogleYoloMock(); - mockGoogleYolo.retrieve(googleYoloConfig).$once().$does(function() { - return Promise.reject(createGoogleYoloError('noCredentialsAvailable')); - }); - mockGoogleYolo.hint(googleYoloConfig).$once().$does(function() { - return Promise.reject(createGoogleYoloError('userCanceled')); - }); - mockControl.$replayAll(); - - var googleYolo = new firebaseui.auth.GoogleYolo(mockGoogleYolo); - return googleYolo.show(googleYoloConfig, false) - .then(function(actualCredential) { - assertNull(actualCredential); + assertObjectEquals(googleYoloCredential, actualCredential); }); } -function testGoogleYolo_show_autoSignInDisabled_savedCredential() { +function testGoogleYolo_show_autoSignInDisabled() { // Test when auto sign-in is disabled and a credential is selected from // list of accounts. - // Ignore old browsers where googleyolo will not work. - if (typeof Promise === 'undefined') { - return; - } var mockGoogleYolo = initializeGoogleYoloMock(); - mockGoogleYolo.hint(googleYoloConfig).$once().$does(function() { - return Promise.resolve(googleYoloCredential); - }); + mockGoogleYolo.initialize(ignoreArgument).$once() + .$does(function(config) { + assertEquals(googleYoloClientId, config.client_id); + assertFalse(config.auto_select); + config.callback(googleYoloCredential); + }); + mockGoogleYolo.prompt().$once(); mockControl.$replayAll(); var googleYolo = new firebaseui.auth.GoogleYolo(mockGoogleYolo); - return googleYolo.show(googleYoloConfig, true) + return googleYolo.show(googleYoloClientId, true) .then(function(actualCredential) { assertObjectEquals(googleYoloCredential, actualCredential); }); @@ -186,120 +114,58 @@ function testGoogleYolo_show_autoSignInDisabled_savedCredential() { function testGoogleYolo_show_autoSignInEnabled_noAvailableCredentials() { // Test when auto sign-in is enabled and no credentials are available. - // Ignore old browsers where googleyolo will not work. - if (typeof Promise === 'undefined') { - return; - } var mockGoogleYolo = initializeGoogleYoloMock(); - mockGoogleYolo.retrieve(googleYoloConfig).$once().$does(function() { - return Promise.reject(createGoogleYoloError('noCredentialsAvailable')); - }); - mockGoogleYolo.hint(googleYoloConfig).$once().$does(function() { - return Promise.reject(createGoogleYoloError('noCredentialsAvailable')); - }); - mockControl.$replayAll(); - - var googleYolo = new firebaseui.auth.GoogleYolo(mockGoogleYolo); - return googleYolo.show(googleYoloConfig, false) - .then(function(actualCredential) { - assertNull(actualCredential); + mockGoogleYolo.initialize(ignoreArgument).$once() + .$does(function(config) { + assertEquals(googleYoloClientId, config.client_id); + assertTrue(config.auto_select); + // Simulate no credential. Promise should not resolve. }); -} - - -function testGoogleYolo_show_autoSignInEnabled_savedCredentials() { - // Test when auto sign-in is enabled and a saved credential is retrieved. - // Ignore old browsers where googleyolo will not work. - if (typeof Promise === 'undefined') { - return; - } - var mockGoogleYolo = initializeGoogleYoloMock(); - mockGoogleYolo.retrieve(googleYoloConfig).$once().$does(function() { - return Promise.resolve(googleYoloCredential); - }); + mockGoogleYolo.prompt().$once(); mockControl.$replayAll(); var googleYolo = new firebaseui.auth.GoogleYolo(mockGoogleYolo); - return googleYolo.show(googleYoloConfig, false) + googleYolo.show(googleYoloClientId, false) .then(function(actualCredential) { - assertObjectEquals(googleYoloCredential, actualCredential); - }); -} - - -function testGoogleYolo_show_autoSignInEnabled_concurrent() { - // Test when auto sign-in is enabled and a previous One-Tap UI is already - // rendered. - // Ignore old browsers where googleyolo will not work. - if (typeof Promise === 'undefined') { - return; - } - var mockGoogleYolo = initializeGoogleYoloMock(); - // The first call will fail with the concurrent request error. - mockGoogleYolo.retrieve(googleYoloConfig).$once().$does(function() { - return Promise.reject(createGoogleYoloError('illegalConcurrentRequest')); - }); - // The above error will lead to a call to cancelLastOperation. - mockGoogleYolo.cancelLastOperation().$once().$does(function() { - return Promise.resolve(); - }); - // The show routine will be called again. - mockGoogleYolo.retrieve(googleYoloConfig).$once().$does(function() { - return Promise.resolve(googleYoloCredential); - }); - mockControl.$replayAll(); - - var googleYolo = new firebaseui.auth.GoogleYolo(mockGoogleYolo); - return googleYolo.show(googleYoloConfig, false) - .then(function(actualCredential) { - assertObjectEquals(googleYoloCredential, actualCredential); + throw new Error('Should not resolve'); }); } function testGoogleYolo_cancel() { // Tests when cancel is manually called. - // Ignore old browsers where googleyolo will not work. - if (typeof Promise === 'undefined') { - return; - } // User cancelled flow. - var retrieveReject = null; - var userCancelledError = createGoogleYoloError('userCanceled'); var cancelled = false; var mockGoogleYolo = initializeGoogleYoloMock(); - mockGoogleYolo.retrieve(googleYoloConfig).$once().$does(function() { - return new Promise(function(resolve, reject) { - cancelled = true; - retrieveReject = reject; - }); - }); - mockGoogleYolo.cancelLastOperation().$once().$does(function() { - retrieveReject(userCancelledError); - return Promise.resolve(); - }); - mockGoogleYolo.retrieve(googleYoloConfig).$once().$does(function() { - // This will wait until above resolves. - assertTrue(cancelled); - return Promise.reject(createGoogleYoloError('noCredentialsAvailable')); - }); - // This corresponds to second call to show. - mockGoogleYolo.hint(googleYoloConfig).$once().$does(function() { - return Promise.resolve(googleYoloCredential); - }); + mockGoogleYolo.initialize(ignoreArgument).$once() + .$does(function(config) { + assertEquals(googleYoloClientId, config.client_id); + assertTrue(config.auto_select); + cancelled = true; + }); + mockGoogleYolo.prompt().$once(); + mockGoogleYolo.cancel().$once(); + mockGoogleYolo.initialize(ignoreArgument).$once() + .$does(function(config) { + assertEquals(googleYoloClientId, config.client_id); + assertTrue(config.auto_select); + assertTrue(cancelled); + config.callback(googleYoloCredential); + }); + mockGoogleYolo.prompt().$once(); mockControl.$replayAll(); var googleYolo = new firebaseui.auth.GoogleYolo(mockGoogleYolo); // All calls to cancel should have no effect before show is called. googleYolo.cancel(); googleYolo.cancel(); - googleYolo.show(googleYoloConfig, false) + googleYolo.show(googleYoloClientId, false) .then(function(actualCredential) { assertNull(actualCredential); }); // This will take effect and cancel the above show call. googleYolo.cancel(); - return googleYolo.show(googleYoloConfig, false) + return googleYolo.show(googleYoloClientId, false) .then(function(actualCredential) { assertObjectEquals(googleYoloCredential, actualCredential); }); @@ -308,10 +174,6 @@ function testGoogleYolo_cancel() { function testGoogleYolo_noGoogleYolo_success() { // Tests when googleyolo namespace is not available. - // Ignore old browsers where googleyolo will not work. - if (typeof Promise === 'undefined') { - return; - } var googleYoloLoader = mockControl.createStrictMock(firebaseui.auth.GoogleYolo.Loader); var getInstance = mockControl.createMethodMock( @@ -324,20 +186,29 @@ function testGoogleYolo_noGoogleYolo_success() { .$does(function() { // Simulate successful load. return goog.Promise.resolve().then(function() { - goog.global['googleyolo'] = mockGoogleYolo; + goog.global['google'] = { + 'accounts': { + 'id': mockGoogleYolo, + }, + }; }); }); - mockGoogleYolo.retrieve(googleYoloConfig).$once().$does(function() { - return Promise.resolve(googleYoloCredential); - }); + mockGoogleYolo.initialize(ignoreArgument).$once() + .$does(function(config) { + assertEquals(googleYoloClientId, config.client_id); + assertTrue(config.auto_select); + config.callback(googleYoloCredential); + }); + mockGoogleYolo.prompt().$once(); mockControl.$replayAll(); + // googleyolo namespace not available. This will dynamically load googleyolo. var googleYolo = new firebaseui.auth.GoogleYolo(null); // This should do nothing. googleYolo.cancel(); - return googleYolo.show(googleYoloConfig, false) + return googleYolo.show(googleYoloClientId, false) .then(function(actualCredential) { - assertEquals(mockGoogleYolo, goog.global['googleyolo']); + assertEquals(mockGoogleYolo, goog.global['google'].accounts.id); assertObjectEquals(googleYoloCredential, actualCredential); }); } @@ -347,10 +218,6 @@ function testGoogleYolo_noGoogleYolo_retrialAfterError() { // Tests when googleyolo namespace is not available. // This will test a flow where load initially fails and then succeeds after // retrial. - // Ignore old browsers where googleyolo will not work. - if (typeof Promise === 'undefined') { - return; - } var mockGoogleYolo = initializeGoogleYoloMock(); var googleYoloLoader = mockControl.createStrictMock(firebaseui.auth.GoogleYolo.Loader); @@ -371,24 +238,33 @@ function testGoogleYolo_noGoogleYolo_retrialAfterError() { .$does(function() { // Simulate successful load. return goog.Promise.resolve().then(function() { - goog.global['googleyolo'] = mockGoogleYolo; + goog.global['google'] = { + 'accounts': { + 'id': mockGoogleYolo, + }, + }; }); }); - mockGoogleYolo.retrieve(googleYoloConfig).$once().$does(function() { - return Promise.resolve(googleYoloCredential); - }); + mockGoogleYolo.initialize(ignoreArgument).$once() + .$does(function(config) { + assertEquals(googleYoloClientId, config.client_id); + assertTrue(config.auto_select); + config.callback(googleYoloCredential); + }); + mockGoogleYolo.prompt().$once(); mockControl.$replayAll(); + // googleyolo namespace not available. This will dynamically load googleyolo. var googleYolo = new firebaseui.auth.GoogleYolo(null); - return googleYolo.show(googleYoloConfig, false) + return googleYolo.show(googleYoloClientId, false) .then(function(actualCredential) { - assertUndefined(goog.global['googleyolo']); + assertUndefined(goog.global['google']); assertNull(actualCredential); // Try again. This should succeed. - return googleYolo.show(googleYoloConfig, false); + return googleYolo.show(googleYoloClientId, false); }) .then(function(actualCredential) { - assertEquals(mockGoogleYolo, goog.global['googleyolo']); + assertEquals(mockGoogleYolo, goog.global['google'].accounts.id); assertObjectEquals(googleYoloCredential, actualCredential); }); } @@ -396,10 +272,6 @@ function testGoogleYolo_noGoogleYolo_retrialAfterError() { function testGoogleYolo_noop_noConfig() { // Tests when googleyolo config is not available. - // Ignore old browsers where googleyolo will not work. - if (typeof Promise === 'undefined') { - return; - } // No googleyolo config available. All operations will be no-ops. var mockGoogleYolo = initializeGoogleYoloMock(); var googleYolo = new firebaseui.auth.GoogleYolo(mockGoogleYolo); @@ -410,17 +282,21 @@ function testGoogleYolo_noop_noConfig() { } -function testGoogleYoloLoader_dynamicLoading() { +function testGoogleYoloLoader_dynamicLoading_onGoogleLibraryLoad_triggered() { var expectedUrl = goog.html.TrustedResourceUrl.fromConstant( - goog.string.Const.from('https://smartlock.google.com/client')); + goog.string.Const.from('https://accounts.google.com/gsi/client')); var safeLoad = mockControl.createMethodMock(goog.net.jsloader, 'safeLoad'); // As library not available, try to load dynamically. safeLoad(expectedUrl) .$once() .$does(function(url) { return goog.Promise.resolve().then(function() { - goog.global['googleyolo'] = initializeGoogleYoloMock(); - goog.global['onGoogleYoloLoad'](goog.global['googleyolo']); + goog.global['google'] = { + 'accounts': { + 'id': initializeGoogleYoloMock(), + }, + }; + goog.global['onGoogleLibraryLoad'](); }); }); mockControl.$replayAll(); @@ -428,7 +304,36 @@ function testGoogleYoloLoader_dynamicLoading() { var googleYoloLoader = new firebaseui.auth.GoogleYolo.Loader(); return googleYoloLoader.load().then(function() { // googleyolo should be loaded. - assertTrue(typeof goog.global['googleyolo'] === 'object'); + assertTrue(typeof goog.global['google'] === 'object'); + // This should resolve with cached googleyolo without jsloader called again. + return googleYoloLoader.load(); + }); +} + + +function testGoogleYoloLoader_dynamicLoading_onGoogleLibraryLoad_notCalled() { + var expectedUrl = goog.html.TrustedResourceUrl.fromConstant( + goog.string.Const.from('https://accounts.google.com/gsi/client')); + var safeLoad = mockControl.createMethodMock(goog.net.jsloader, 'safeLoad'); + // As library not available, try to load dynamically. + safeLoad(expectedUrl) + .$once() + .$does(function(url) { + return goog.Promise.resolve().then(function() { + // Should still resolve even if onGoogleLibraryLoad is not called. + goog.global['google'] = { + 'accounts': { + 'id': initializeGoogleYoloMock(), + }, + }; + }); + }); + mockControl.$replayAll(); + + var googleYoloLoader = new firebaseui.auth.GoogleYolo.Loader(); + return googleYoloLoader.load().then(function() { + // googleyolo should be loaded. + assertTrue(typeof goog.global['google'] === 'object'); // This should resolve with cached googleyolo without jsloader called again. return googleYoloLoader.load(); }); @@ -442,7 +347,11 @@ function testGoogleYoloLoader_loadedOnDomReady() { firebaseui.auth.util, 'onDomReady'); // Simulate googleyolo already loaded on DOM ready. onDomReady().$once().$does(function() { - goog.global['googleyolo'] = initializeGoogleYoloMock(); + goog.global['google'] = { + 'accounts': { + 'id': initializeGoogleYoloMock(), + }, + }; return goog.Promise.resolve(); }); mockControl.$replayAll(); @@ -450,14 +359,18 @@ function testGoogleYoloLoader_loadedOnDomReady() { var googleYoloLoader = new firebaseui.auth.GoogleYolo.Loader(); return googleYoloLoader.load().then(function() { // googleyolo should be loaded. - assertTrue(typeof goog.global['googleyolo'] === 'object'); + assertTrue(typeof goog.global['google'] === 'object'); }); } function testGoogleYoloLoader_loadedBeforeCall() { // Simulate already loaded. - goog.global['googleyolo'] = initializeGoogleYoloMock(); + goog.global['google'] = { + 'accounts': { + 'id': initializeGoogleYoloMock(), + }, + }; mockControl.createMethodMock(goog.net.jsloader, 'safeLoad'); mockControl.createMethodMock(firebaseui.auth.util, 'onDomReady'); mockControl.$replayAll(); @@ -465,7 +378,7 @@ function testGoogleYoloLoader_loadedBeforeCall() { var googleYoloLoader = new firebaseui.auth.GoogleYolo.Loader(); return googleYoloLoader.load().then(function() { // googleyolo should be loaded. - assertTrue(typeof goog.global['googleyolo'] === 'object'); + assertTrue(typeof goog.global['google'] === 'object'); }); } @@ -473,7 +386,7 @@ function testGoogleYoloLoader_loadedBeforeCall() { function testGoogleYoloLoader_successAfterGenericError() { var expectedError = new Error; var expectedUrl = goog.html.TrustedResourceUrl.fromConstant( - goog.string.Const.from('https://smartlock.google.com/client')); + goog.string.Const.from('https://accounts.google.com/gsi/client')); var safeLoad = mockControl.createMethodMock(goog.net.jsloader, 'safeLoad'); // Simulate first load failing with expected error. safeLoad(expectedUrl) @@ -487,8 +400,12 @@ function testGoogleYoloLoader_successAfterGenericError() { .$once() .$does(function(url) { return goog.Promise.resolve().then(function() { - goog.global['googleyolo'] = initializeGoogleYoloMock(); - goog.global['onGoogleYoloLoad'](goog.global['googleyolo']); + goog.global['google'] = { + 'accounts': { + 'id': initializeGoogleYoloMock(), + }, + }; + goog.global['onGoogleLibraryLoad'](); }); }); mockControl.$replayAll(); @@ -501,7 +418,7 @@ function testGoogleYoloLoader_successAfterGenericError() { return googleYoloLoader.load() .then(function() { // googleyolo should be loaded. - assertTrue(typeof goog.global['googleyolo'] === 'object'); + assertTrue(typeof goog.global['google'] === 'object'); // Third call should succeed without jsloader running. return googleYoloLoader.load(); }); @@ -511,7 +428,7 @@ function testGoogleYoloLoader_successAfterGenericError() { function testGoogleYoloLoader_successAfterTimeout() { var expectedUrl = goog.html.TrustedResourceUrl.fromConstant( - goog.string.Const.from('https://smartlock.google.com/client')); + goog.string.Const.from('https://accounts.google.com/gsi/client')); var safeLoad = mockControl.createMethodMock(goog.net.jsloader, 'safeLoad'); // Simulate first call not resolving. safeLoad(expectedUrl) @@ -519,15 +436,19 @@ function testGoogleYoloLoader_successAfterTimeout() { .$does(function(url) { // Simulate timeout. clock.tick(10000); - return new goog.Promise.resolve(); + return goog.Promise.resolve(); }); // Second call will succeed. safeLoad(expectedUrl) .$once() .$does(function(url) { return goog.Promise.resolve().then(function() { - goog.global['googleyolo'] = initializeGoogleYoloMock(); - goog.global['onGoogleYoloLoad'](goog.global['googleyolo']); + goog.global['google'] = { + 'accounts': { + 'id': initializeGoogleYoloMock(), + }, + }; + goog.global['onGoogleLibraryLoad'](); }); }); mockControl.$replayAll(); @@ -540,7 +461,7 @@ function testGoogleYoloLoader_successAfterTimeout() { return googleYoloLoader.load() .then(function() { // googleyolo should be loaded. - assertTrue(typeof goog.global['googleyolo'] === 'object'); + assertTrue(typeof goog.global['google'] === 'object'); // Third call should succeed without jsloader running. return googleYoloLoader.load(); }); diff --git a/javascript/widgets/authui.js b/javascript/widgets/authui.js index cac68fe0..b3b35fc7 100644 --- a/javascript/widgets/authui.js +++ b/javascript/widgets/authui.js @@ -1011,7 +1011,7 @@ firebaseui.auth.AuthUI.prototype.showOneTapSignIn = function(handler) { this.checkIfDestroyed_(); try { this.googleYolo_.show( - this.getConfig().getGoogleYoloConfig(), this.isAutoSignInDisabled()) + this.getConfig().getGoogleYoloClientId(), this.isAutoSignInDisabled()) .then(function(credential) { // Run only when component is available. if (self.currentComponent_) { diff --git a/javascript/widgets/authui_test.js b/javascript/widgets/authui_test.js index cadfa19e..4d2162eb 100644 --- a/javascript/widgets/authui_test.js +++ b/javascript/widgets/authui_test.js @@ -108,11 +108,11 @@ var options = { 'apiKey': 'API_KEY', 'authDomain': 'subdomain.firebaseapp.com' }; +var googYoloClientId = '1234567890.apps.googleusercontent.com'; // Mock googleyolo ID token credential. var googleYoloIdTokenCredential = { - 'idToken': 'ID_TOKEN', - 'id': 'user@example.com', - 'authMethod': 'https://accounts.google.com' + 'credential': 'ID_TOKEN', + 'clientId': googYoloClientId, }; var mockControl; var ignoreArgument; @@ -1593,14 +1593,11 @@ function testAuthUi_oneTapSignIn_disabled() { { 'provider': 'google.com', 'customParameters': {'prompt': 'select_account'}, - 'authMethod': 'https://accounts.google.com', - 'clientId': '1234567890.apps.googleusercontent.com' + 'clientId': googYoloClientId, } ], 'credentialHelper': firebaseui.auth.widget.Config.CredentialHelper.NONE }; - // Expected googyolo config is null. - var googYoloConfig = null; asyncTestCase.waitForSignals(1); var component = new firebaseui.auth.ui.page.Callback(); component.render(container1); @@ -1609,7 +1606,7 @@ function testAuthUi_oneTapSignIn_disabled() { var getInstance = mockControl.createMethodMock( firebaseui.auth.GoogleYolo, 'getInstance'); getInstance().$returns(googleYolo); - // One-Tap should be a no-op since the googleyolo config is null. + // One-Tap should be a no-op since the googleyolo client ID is null. googleYolo.show(null, true) .$once() // Simulate no googleyolo credential returned since this is a no-op. @@ -1647,25 +1644,12 @@ function testAuthUi_oneTapSignIn_autoSignInDisabled() { { 'provider': 'google.com', 'customParameters': {'prompt': 'select_account'}, - 'authMethod': 'https://accounts.google.com', - 'clientId': '1234567890.apps.googleusercontent.com' + 'clientId': googYoloClientId, } ], 'credentialHelper': firebaseui.auth.widget.Config.CredentialHelper.GOOGLE_YOLO }; - // Expected googyolo config corresponding to the above FirebaseUI config. - var googYoloConfig = { - 'supportedAuthMethods': [ - 'https://accounts.google.com' - ], - 'supportedIdTokenProviders': [ - { - 'uri': 'https://accounts.google.com', - 'clientId': '1234567890.apps.googleusercontent.com' - } - ] - }; asyncTestCase.waitForSignals(1); var component = new firebaseui.auth.ui.page.Callback(); component.render(container1); @@ -1675,7 +1659,7 @@ function testAuthUi_oneTapSignIn_autoSignInDisabled() { firebaseui.auth.GoogleYolo, 'getInstance'); getInstance().$returns(googleYolo); // One-Tap should be shown with auto sign-in disabled. - googleYolo.show(googYoloConfig, true) + googleYolo.show(googYoloClientId, true) .$once() // Simulate googleyolo credential returned. .$returns(goog.Promise.resolve(googleYoloIdTokenCredential)); @@ -1711,25 +1695,12 @@ function testAuthUi_oneTapSignIn_noCurrentComponent() { { 'provider': 'google.com', 'customParameters': {'prompt': 'select_account'}, - 'authMethod': 'https://accounts.google.com', - 'clientId': '1234567890.apps.googleusercontent.com' + 'clientId': googYoloClientId, } ], 'credentialHelper': firebaseui.auth.widget.Config.CredentialHelper.GOOGLE_YOLO }; - // Expected googyolo config corresponding to the above FirebaseUI config. - var googYoloConfig = { - 'supportedAuthMethods': [ - 'https://accounts.google.com' - ], - 'supportedIdTokenProviders': [ - { - 'uri': 'https://accounts.google.com', - 'clientId': '1234567890.apps.googleusercontent.com' - } - ] - }; asyncTestCase.waitForSignals(1); var component = new firebaseui.auth.ui.page.Callback(); component.render(container1); @@ -1739,9 +1710,9 @@ function testAuthUi_oneTapSignIn_noCurrentComponent() { firebaseui.auth.GoogleYolo, 'getInstance'); getInstance().$returns(googleYolo); // One-Tap should be shown with auto sign-in disabled. - googleYolo.show(googYoloConfig, true) + googleYolo.show(googYoloClientId, true) .$once() - .$does(function(config, autoSignInDisabled) { + .$does(function(clientId, autoSignInDisabled) { // Simulate no current component rendered. app.setCurrentComponent(null); // Simulate googleyolo credential returned. @@ -1772,25 +1743,12 @@ function testAuthUi_oneTapSignIn_autoSignInEnabled() { 'signInOptions': [ { 'provider': 'google.com', - 'authMethod': 'https://accounts.google.com', - 'clientId': '1234567890.apps.googleusercontent.com' + 'clientId': googYoloClientId, } ], 'credentialHelper': firebaseui.auth.widget.Config.CredentialHelper.GOOGLE_YOLO }; - // Expected googyolo config corresponding to the above FirebaseUI config. - var googYoloConfig = { - 'supportedAuthMethods': [ - 'https://accounts.google.com' - ], - 'supportedIdTokenProviders': [ - { - 'uri': 'https://accounts.google.com', - 'clientId': '1234567890.apps.googleusercontent.com' - } - ] - }; asyncTestCase.waitForSignals(1); var component = new firebaseui.auth.ui.page.Callback(); component.render(container1); @@ -1800,7 +1758,7 @@ function testAuthUi_oneTapSignIn_autoSignInEnabled() { firebaseui.auth.GoogleYolo, 'getInstance'); getInstance().$returns(googleYolo); // One-Tap should be shown with auto sign-in enabled. - googleYolo.show(googYoloConfig, false) + googleYolo.show(googYoloClientId, false) .$once() .$returns(goog.Promise.resolve(googleYoloIdTokenCredential)); // Provided handler should be passed the expected parameters. diff --git a/javascript/widgets/config.js b/javascript/widgets/config.js index 1f3ac119..d996fc60 100644 --- a/javascript/widgets/config.js +++ b/javascript/widgets/config.js @@ -257,37 +257,17 @@ class Config { } /** - * @return {?SmartLockRequestOptions} The googleyolo configuration options - * if available. + * @return {?string} The googleyolo configuration client ID if available. */ - getGoogleYoloConfig() { - const supportedAuthMethods = []; - const supportedIdTokenProviders = []; - googArray.forEach(this.getSignInOptions_(), (option) => { - if (option['authMethod']) { - supportedAuthMethods.push(option['authMethod']); - if (option['clientId']) { - supportedIdTokenProviders.push({ - 'uri': option['authMethod'], - 'clientId': option['clientId'], - }); - } - } - }); - let config = null; - // Ensure configuration is not empty. At least one supportedIdTokenProviders - // or supportedAuthMethods needs to be provided. - if (this.getCredentialHelper() === - Config.CredentialHelper.GOOGLE_YOLO && - // googleyolo will enforce that clientId is present. Delegate the check - // to it. Errors will be surfaced in the console. - supportedAuthMethods.length) { - config = { - 'supportedIdTokenProviders': supportedIdTokenProviders, - 'supportedAuthMethods': supportedAuthMethods, - }; + getGoogleYoloClientId() { + const signInOptions = this.getSignInOptionsForProvider_( + firebase.auth.GoogleAuthProvider.PROVIDER_ID); + if (signInOptions && + signInOptions['clientId'] && + this.getCredentialHelper() === Config.CredentialHelper.GOOGLE_YOLO) { + return signInOptions['clientId'] || null; } - return config; + return null; } /** diff --git a/javascript/widgets/config_test.js b/javascript/widgets/config_test.js index 559bc73b..60d9b593 100644 --- a/javascript/widgets/config_test.js +++ b/javascript/widgets/config_test.js @@ -31,6 +31,7 @@ const stub = new PropertyReplacer(); let testUtil; let errorLogMessages = []; let warningLogMessages = []; +const expectedClientId = '1234567890.apps.googleusercontent.com'; testSuite({ setUp() { @@ -671,26 +672,25 @@ testSuite({ assertNull(config.getProviderIdFromAuthMethod('unknown')); }, - testGetGoogleYoloConfig_availableAndEnabled() { + testGetGoogleYoloClientId_availableAndEnabled() { config.update('signInOptions', [ { 'provider': 'google.com', 'customParameters': { 'prompt': 'none', }, - 'authMethod': 'https://accounts.google.com', - 'clientId': '1234567890.apps.googleusercontent.com', + 'clientId': expectedClientId, }, { 'provider': 'password', - 'authMethod': 'googleyolo://id-and-password', + 'clientId': 'CLIENT_ID2', }, { - 'authMethod': 'unknown', + 'clientId': 'CLIENT_ID3', }, { 'provider': 'facebook.com', - // authMethod is required. + // Only Google client ID is used. 'clientId': 'CLIENT_ID', }, ]); @@ -698,51 +698,38 @@ testSuite({ config.update( 'credentialHelper', Config.CredentialHelper.GOOGLE_YOLO); - const expectedConfig = { - 'supportedAuthMethods': [ - 'https://accounts.google.com', - 'googleyolo://id-and-password', - ], - 'supportedIdTokenProviders': [ - { - 'uri': 'https://accounts.google.com', - 'clientId': '1234567890.apps.googleusercontent.com', - }, - ], - }; - assertObjectEquals(expectedConfig, config.getGoogleYoloConfig()); + assertEquals(expectedClientId, config.getGoogleYoloClientId()); }, - testGetGoogleYoloConfig_notEnabled() { + testGetGoogleYoloClientId_notEnabled() { config.update('signInOptions', [ { 'provider': 'google.com', 'customParameters': { 'prompt': 'none', }, - 'authMethod': 'https://accounts.google.com', - 'clientId': '1234567890.apps.googleusercontent.com', + 'clientId': expectedClientId, }, { 'provider': 'password', - 'authMethod': 'googleyolo://id-and-password', + 'clientId': 'CLIENT_ID2', }, { - 'authMethod': 'unknown', + 'clientId': 'CLIENT_ID3', }, { 'provider': 'facebook.com', - // authMethod is required. + // Only Google client ID is used. 'clientId': 'CLIENT_ID', }, ]); // GOOGLE_YOLO credentialHelper not selected. config.update( 'credentialHelper', Config.CredentialHelper.NONE); - assertNull(config.getGoogleYoloConfig()); + assertNull(config.getGoogleYoloClientId()); }, - testGetGoogleYoloConfig_notAvailable() { + testGetGoogleYoloClientId_notAvailable() { config.update('signInOptions', [ { 'provider': 'google.com', @@ -761,14 +748,13 @@ testSuite({ }, 'github.com', 'password', - // authMethod with no provider. + // clientId with no provider. { - 'authMethod': 'unknown', + 'clientId': 'unknown', }, - // clientId with no authMethod. { 'provider': 'facebook.com', - // authMethod is required. + // Only Google client ID is used. 'clientId': 'CLIENT_ID', }, ]); @@ -776,7 +762,7 @@ testSuite({ config.update( 'credentialHelper', Config.CredentialHelper.GOOGLE_YOLO); - assertNull(config.getGoogleYoloConfig()); + assertNull(config.getGoogleYoloClientId()); }, testGetPhoneAuthDefaultCountry() { diff --git a/javascript/widgets/handler/common.js b/javascript/widgets/handler/common.js index b44b8cb6..5ac9d1be 100644 --- a/javascript/widgets/handler/common.js +++ b/javascript/widgets/handler/common.js @@ -1090,22 +1090,32 @@ firebaseui.auth.widget.handler.common.handleGoogleYoloCredential = app, component, providerId, opt_email); return goog.Promise.resolve(true); }; - var providerId = app.getConfig().getProviderIdFromAuthMethod( - (credential && credential.authMethod) || null); // ID token credential available and supported Firebase Auth provider also // available. - if (credential && credential.idToken && - providerId === firebase.auth.GoogleAuthProvider.PROVIDER_ID) { + if (credential && + credential.credential && + credential.clientId === app.getConfig().getGoogleYoloClientId()) { // ID token available. // Only Google has API to sign-in with an ID token. if (app.getConfig().getProviderAdditionalScopes( firebase.auth.GoogleAuthProvider.PROVIDER_ID).length) { + let email; + try { + // New one-tap API does not return the credential identitifer. + // Parse email from Google ID token. + const components = credential.credential.split('.'); + const payloadDecoded = JSON.parse(atob(components[1])); + email = payloadDecoded['email']; + } catch (e) { + // Ignore + } // Scopes available, OAuth flow with additional scopes required. - return signInWithProvider(providerId, credential.id); + return signInWithProvider( + firebase.auth.GoogleAuthProvider.PROVIDER_ID, email); } else { // Scopes not requested. Sign in with ID token directly. return signInWithCredential(firebase.auth.GoogleAuthProvider.credential( - credential.idToken)); + credential.credential)); } } else if (credential) { // Unsupported credential. diff --git a/javascript/widgets/handler/common_test.js b/javascript/widgets/handler/common_test.js index 98b017c2..4d6891b1 100644 --- a/javascript/widgets/handler/common_test.js +++ b/javascript/widgets/handler/common_test.js @@ -2627,8 +2627,7 @@ function testHandleGoogleYoloCredential_handledSuccessfully_withScopes() { 'provider': 'google.com', 'scopes': ['googl1', 'googl2'], 'customParameters': {'prompt': 'select_account'}, - 'authMethod': 'https://accounts.google.com', - 'clientId': '1234567890.apps.googleusercontent.com' + 'clientId': googYoloClientId, }, 'facebook.com', 'password', 'phone'], 'credentialHelper': firebaseui.auth.widget.Config.CredentialHelper.GOOGLE_YOLO @@ -2658,8 +2657,7 @@ function testHandleGoogleYoloCredential_handledSuccessfully_withoutScopes() { 'signInSuccessUrl': 'http://localhost/home', 'signInOptions': [{ 'provider': 'google.com', - 'authMethod': 'https://accounts.google.com', - 'clientId': '1234567890.apps.googleusercontent.com' + 'clientId': googYoloClientId, }, 'facebook.com', 'password', 'phone'], 'credentialHelper': firebaseui.auth.widget.Config.CredentialHelper.GOOGLE_YOLO @@ -2679,10 +2677,10 @@ function testHandleGoogleYoloCredential_handledSuccessfully_withoutScopes() { asyncTestCase.signal(); }); var expectedCredential = firebase.auth.GoogleAuthProvider.credential( - googleYoloIdTokenCredential.idToken); + googleYoloIdTokenCredential.credential); var cred = createMockCredential({ 'providerId': 'google.com', - 'idToken': googleYoloIdTokenCredential.idToken + 'idToken': googleYoloIdTokenCredential.credential }); testAuth.setUser({ 'email': federatedAccount.getEmail(), @@ -2723,8 +2721,7 @@ function testHandleGoogleYoloCredential_unhandled_withoutScopes() { 'signInSuccessUrl': 'http://localhost/home', 'signInOptions': [{ 'provider': 'google.com', - 'authMethod': 'https://accounts.google.com', - 'clientId': '1234567890.apps.googleusercontent.com' + 'clientId': googYoloClientId, }, 'facebook.com', 'password', 'phone'], 'credentialHelper': firebaseui.auth.widget.Config.CredentialHelper.GOOGLE_YOLO @@ -2743,7 +2740,7 @@ function testHandleGoogleYoloCredential_unhandled_withoutScopes() { asyncTestCase.signal(); }); var expectedCredential = firebase.auth.GoogleAuthProvider.credential( - googleYoloIdTokenCredential.idToken); + googleYoloIdTokenCredential.credential); // Confirm signInWithCredential called underneath with // unsuccessful response. testAuth.assertSignInWithCredential( @@ -2766,8 +2763,7 @@ function testHandleGoogleYoloCredential_cancelled_withoutScopes() { 'signInSuccessUrl': 'http://localhost/home', 'signInOptions': [{ 'provider': 'google.com', - 'authMethod': 'https://accounts.google.com', - 'clientId': '1234567890.apps.googleusercontent.com' + 'clientId': googYoloClientId, }, 'facebook.com', 'password', 'phone'], 'credentialHelper': firebaseui.auth.widget.Config.CredentialHelper.GOOGLE_YOLO @@ -2798,8 +2794,7 @@ function testHandleGoogleYoloCredential_unsupportedCredential() { 'signInSuccessUrl': 'http://localhost/home', 'signInOptions': [{ 'provider': 'google.com', - 'authMethod': 'https://accounts.google.com', - 'clientId': '1234567890.apps.googleusercontent.com' + 'clientId': googYoloClientId, }, 'facebook.com', 'password', 'phone'], 'credentialHelper': firebaseui.auth.widget.Config.CredentialHelper.GOOGLE_YOLO @@ -2832,14 +2827,13 @@ function testHandleGoogleYoloCredential_upgradeAnonymous_noScopes() { 'signInSuccessUrl': 'http://localhost/home', 'signInOptions': [{ 'provider': 'google.com', - 'authMethod': 'https://accounts.google.com', - 'clientId': '1234567890.apps.googleusercontent.com' + 'clientId': googYoloClientId, }, 'facebook.com', 'password', 'phone'], 'credentialHelper': firebaseui.auth.widget.Config.CredentialHelper.GOOGLE_YOLO }); var expectedCredential = firebase.auth.GoogleAuthProvider.credential( - googleYoloIdTokenCredential.idToken); + googleYoloIdTokenCredential.credential); var component = new firebaseui.auth.ui.page.ProviderSignIn( goog.nullFunction(), []); component.render(container); @@ -2893,14 +2887,13 @@ function testHandleGoogleYoloCredential_upgradeAnonymous_credentialInUse() { 'signInSuccessUrl': 'http://localhost/home', 'signInOptions': [{ 'provider': 'google.com', - 'authMethod': 'https://accounts.google.com', - 'clientId': '1234567890.apps.googleusercontent.com' + 'clientId': googYoloClientId, }, 'facebook.com', 'password', 'phone'], 'credentialHelper': firebaseui.auth.widget.Config.CredentialHelper.GOOGLE_YOLO }); var expectedCredential = firebase.auth.GoogleAuthProvider.credential( - googleYoloIdTokenCredential.idToken); + googleYoloIdTokenCredential.credential); // Expected linkWithCredential error. var expectedError = { 'code': 'auth/credential-already-in-use', @@ -2950,14 +2943,13 @@ function testHandleGoogleYoloCredential_upgradeAnonymous_fedEmailInUse() { 'signInSuccessUrl': 'http://localhost/home', 'signInOptions': [{ 'provider': 'google.com', - 'authMethod': 'https://accounts.google.com', - 'clientId': '1234567890.apps.googleusercontent.com' + 'clientId': googYoloClientId, }, 'facebook.com', 'password', 'phone'], 'credentialHelper': firebaseui.auth.widget.Config.CredentialHelper.GOOGLE_YOLO }); var expectedCredential = firebase.auth.GoogleAuthProvider.credential( - googleYoloIdTokenCredential.idToken); + googleYoloIdTokenCredential.credential); // Expected linkWithCredential error. var expectedError = { 'code': 'auth/email-already-in-use', @@ -2968,7 +2960,7 @@ function testHandleGoogleYoloCredential_upgradeAnonymous_fedEmailInUse() { var pendingEmailCred = new firebaseui.auth.PendingEmailCredential( federatedAccount.getEmail(), firebase.auth.GoogleAuthProvider.credential( - googleYoloIdTokenCredential.idToken, null)); + googleYoloIdTokenCredential.credential, null)); var component = new firebaseui.auth.ui.page.ProviderSignIn( goog.nullFunction(), []); component.render(container); @@ -3018,14 +3010,13 @@ function testHandleGoogleYoloCredential_upgradeAnonymous_passEmailInUse() { 'signInSuccessUrl': 'http://localhost/home', 'signInOptions': [{ 'provider': 'google.com', - 'authMethod': 'https://accounts.google.com', - 'clientId': '1234567890.apps.googleusercontent.com' + 'clientId': googYoloClientId, }, 'facebook.com', 'password', 'phone'], 'credentialHelper': firebaseui.auth.widget.Config.CredentialHelper.GOOGLE_YOLO }); var expectedCredential = firebase.auth.GoogleAuthProvider.credential( - googleYoloIdTokenCredential.idToken); + googleYoloIdTokenCredential.credential); // Expected linkWithCredential error. var expectedError = { 'code': 'auth/email-already-in-use', @@ -3036,7 +3027,7 @@ function testHandleGoogleYoloCredential_upgradeAnonymous_passEmailInUse() { var pendingEmailCred = new firebaseui.auth.PendingEmailCredential( federatedAccount.getEmail(), firebase.auth.GoogleAuthProvider.credential( - googleYoloIdTokenCredential.idToken, null)); + googleYoloIdTokenCredential.credential, null)); var component = new firebaseui.auth.ui.page.ProviderSignIn( goog.nullFunction(), []); component.render(container); @@ -3096,8 +3087,7 @@ function testHandleGoogleYoloCredential_upgradeAnonymous_withScopes() { 'provider': 'google.com', 'scopes': ['googl1', 'googl2'], 'customParameters': {'prompt': 'select_account'}, - 'authMethod': 'https://accounts.google.com', - 'clientId': '1234567890.apps.googleusercontent.com' + 'clientId': googYoloClientId, }, 'facebook.com', 'password', 'phone'], 'credentialHelper': firebaseui.auth.widget.Config.CredentialHelper.GOOGLE_YOLO diff --git a/javascript/widgets/handler/providersignin_test.js b/javascript/widgets/handler/providersignin_test.js index 23e65fbb..c42742ac 100644 --- a/javascript/widgets/handler/providersignin_test.js +++ b/javascript/widgets/handler/providersignin_test.js @@ -267,7 +267,7 @@ function testHandleProviderSignIn_oneTap_unhandled_withoutScopes() { // signInWithCredential should be called to handle the // ID token returned by googleyolo. var expectedCredential = firebase.auth.GoogleAuthProvider.credential( - googleYoloIdTokenCredential.idToken); + googleYoloIdTokenCredential.credential); // Simulate an error encountered in signInWithCredential. testAuth.assertSignInWithCredential( [expectedCredential], @@ -347,11 +347,11 @@ function testHandleProviderSignIn_oneTap_handledSuccessfully_withoutScopes() { // signInWithCredential should be called to handle the // ID token returned by googleyolo. var expectedCredential = firebase.auth.GoogleAuthProvider.credential( - googleYoloIdTokenCredential.idToken); + googleYoloIdTokenCredential.credential); // The Firebase Auth mock OAuth credential to return. var cred = firebaseui.auth.idp.getAuthCredential({ 'providerId': 'google.com', - 'idToken': googleYoloIdTokenCredential.idToken + 'idToken': googleYoloIdTokenCredential.credential }); // Mock signed in user. testAuth.setUser({ @@ -429,7 +429,7 @@ function testHandleProviderSignIn_oneTap_upgradeAnonymous_withoutScopes() { // linkWithCredential should be called to handle the // ID token returned by googleyolo. var expectedCredential = firebase.auth.GoogleAuthProvider.credential( - googleYoloIdTokenCredential.idToken); + googleYoloIdTokenCredential.credential); // Trigger onAuthStateChanged listener. externalAuth.runAuthChangeHandler(); // linkWithCredential should be called with the expected @@ -493,7 +493,7 @@ function testHandleProviderSignIn_oneTap_upgradeAnon_noScopes_credInUse() { // linkWithCredential should be called to handle the // ID token returned by googleyolo. var expectedCredential = firebase.auth.GoogleAuthProvider.credential( - googleYoloIdTokenCredential.idToken); + googleYoloIdTokenCredential.credential); // Expected linkWithCredential error. var expectedError = { 'code': 'auth/credential-already-in-use', @@ -559,7 +559,7 @@ function testHandleProviderSignIn_oneTap_upgradeAnon_noScopes_fedEmailInUse() { // linkWithCredential should be called to handle the // ID token returned by googleyolo. var expectedCredential = firebase.auth.GoogleAuthProvider.credential( - googleYoloIdTokenCredential.idToken); + googleYoloIdTokenCredential.credential); // Expected linkWithRedirect error. var expectedError = { 'code': 'auth/email-already-in-use', @@ -570,7 +570,7 @@ function testHandleProviderSignIn_oneTap_upgradeAnon_noScopes_fedEmailInUse() { var pendingEmailCred = new firebaseui.auth.PendingEmailCredential( federatedAccount.getEmail(), firebase.auth.GoogleAuthProvider.credential( - googleYoloIdTokenCredential.idToken, null)); + googleYoloIdTokenCredential.credential, null)); // Trigger onAuthStateChanged listener. externalAuth.runAuthChangeHandler(); // linkWithCredential should be called with the expected @@ -629,7 +629,7 @@ function testHandleProviderSignIn_oneTap_upgradeAnon_noScopes_passEmailInUse() { // linkWithCredential should be called to handle the // ID token returned by googleyolo. var expectedCredential = firebase.auth.GoogleAuthProvider.credential( - googleYoloIdTokenCredential.idToken); + googleYoloIdTokenCredential.credential); // Expected linkWithRedirect error. var expectedError = { 'code': 'auth/email-already-in-use', @@ -640,7 +640,7 @@ function testHandleProviderSignIn_oneTap_upgradeAnon_noScopes_passEmailInUse() { var pendingEmailCred = new firebaseui.auth.PendingEmailCredential( federatedAccount.getEmail(), firebase.auth.GoogleAuthProvider.credential( - googleYoloIdTokenCredential.idToken, null)); + googleYoloIdTokenCredential.credential, null)); // Trigger onAuthStateChanged listener. externalAuth.runAuthChangeHandler(); // linkWithCredential should be called with the expected diff --git a/javascript/widgets/handler/testhelper.js b/javascript/widgets/handler/testhelper.js index 3fc8decb..52be1756 100644 --- a/javascript/widgets/handler/testhelper.js +++ b/javascript/widgets/handler/testhelper.js @@ -97,16 +97,16 @@ var operationNotSupportedError = { 'application is running on. "location.protocol" must be http, https ' + 'or chrome-extension and web storage must be enabled.' }; +var googYoloClientId = '1234567890.apps.googleusercontent.com'; // googleyolo ID token credential. var googleYoloIdTokenCredential = { - 'idToken': 'ID_TOKEN', - 'id': federatedAccount.getEmail(), - 'authMethod': 'https://accounts.google.com' + 'credential': 'HEADER.' + + btoa(JSON.stringify({email: federatedAccount.getEmail()})) + '.SIGNATURE', + 'clientId': googYoloClientId, }; // googleyolo non ID token credential. var googleYoloOtherCredential = { - 'id': federatedAccount.getEmail(), - 'authMethod': 'https://accounts.google.com' + 'clientId': 'other', }; // Mock anonymous user. var anonymousUser = { diff --git a/package-lock.json b/package-lock.json index cc1242cb..eb1158fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9952,7 +9952,7 @@ "selenium-webdriver": "3.6.0", "source-map-support": "~0.4.0", "webdriver-js-extender": "2.1.0", - "webdriver-manager": "^12.0.6" + "webdriver-manager": "^12.1.7" }, "dependencies": { "webdriver-manager": { diff --git a/sauce_browsers.json b/sauce_browsers.json index f7f654e6..c6067212 100644 --- a/sauce_browsers.json +++ b/sauce_browsers.json @@ -12,13 +12,6 @@ "version" : "65.0", "name": "chrome-latest-mac" }, - { - "browserName" : "MicrosoftEdge", - "platform" : "Windows 10", - "timeZone": "Pacific", - "version" : "16.16299", - "name": "edge-16-windows" - }, { "browserName" : "safari", "version" : "10.0",