diff --git a/packages/auth-types/index.d.ts b/packages/auth-types/index.d.ts index cda41850b8a..7e085efee5b 100644 --- a/packages/auth-types/index.d.ts +++ b/packages/auth-types/index.d.ts @@ -96,6 +96,7 @@ export interface ApplicationVerifier { export interface AuthCredential { providerId: string; + signInMethod: string; } export interface AuthProvider { @@ -109,7 +110,10 @@ export interface ConfirmationResult { export class EmailAuthProvider extends EmailAuthProvider_Instance { static PROVIDER_ID: string; + static EMAIL_PASSWORD_SIGN_IN_METHOD: string; + static EMAIL_LINK_SIGN_IN_METHOD: string; static credential(email: string, password: string): AuthCredential; + static credentialWithLink(email: string, emailLink: string): AuthCredential; } export class EmailAuthProvider_Instance implements AuthProvider { providerId: string; @@ -122,6 +126,7 @@ export interface Error { export class FacebookAuthProvider extends FacebookAuthProvider_Instance { static PROVIDER_ID: string; + static FACEBOOK_SIGN_IN_METHOD: string; static credential(token: string): AuthCredential; } export class FacebookAuthProvider_Instance implements AuthProvider { @@ -132,6 +137,7 @@ export class FacebookAuthProvider_Instance implements AuthProvider { export class GithubAuthProvider extends GithubAuthProvider_Instance { static PROVIDER_ID: string; + static GITHUB_SIGN_IN_METHOD: string; static credential(token: string): AuthCredential; } export class GithubAuthProvider_Instance implements AuthProvider { @@ -142,6 +148,7 @@ export class GithubAuthProvider_Instance implements AuthProvider { export class GoogleAuthProvider extends GoogleAuthProvider_Instance { static PROVIDER_ID: string; + static GOOGLE_SIGN_IN_METHOD: string; static credential( idToken?: string | null, accessToken?: string | null @@ -162,6 +169,7 @@ export class OAuthProvider implements AuthProvider { export class PhoneAuthProvider extends PhoneAuthProvider_Instance { static PROVIDER_ID: string; + static PHONE_SIGN_IN_METHOD: string; static credential( verificationId: string, verificationCode: string @@ -191,6 +199,7 @@ export class RecaptchaVerifier_Instance implements ApplicationVerifier { export class TwitterAuthProvider extends TwitterAuthProvider_Instance { static PROVIDER_ID: string; + static TWITTER_SIGN_IN_METHOD: string; static credential(token: string, secret: string): AuthCredential; } export class TwitterAuthProvider_Instance implements AuthProvider { @@ -238,6 +247,8 @@ export class FirebaseAuth { ): Promise; currentUser: User | null; fetchProvidersForEmail(email: string): Promise; + fetchSignInMethodsForEmail(email: string): Promise; + isSignInWithEmailLink(emailLink: string): boolean; getRedirectResult(): Promise; languageCode: string | null; onAuthStateChanged( @@ -250,6 +261,10 @@ export class FirebaseAuth { error?: (a: Error) => any, completed?: Unsubscribe ): Unsubscribe; + sendSignInLinkToEmail( + email: string, + actionCodeSettings: ActionCodeSettings + ): Promise; sendPasswordResetEmail( email: string, actionCodeSettings?: ActionCodeSettings | null @@ -266,6 +281,7 @@ export class FirebaseAuth { email: string, password: string ): Promise; + signInWithEmailLink(email: string, emailLink?: string): Promise; signInWithPhoneNumber( phoneNumber: string, applicationVerifier: ApplicationVerifier diff --git a/packages/auth/demo/public/index.html b/packages/auth/demo/public/index.html index d9f4199ce9c..2818ba17de3 100644 --- a/packages/auth/demo/public/index.html +++ b/packages/auth/demo/public/index.html @@ -254,6 +254,29 @@ + +
Sign In with Email Link
+
+ + + +
+
+ + +
+
Password Reset
@@ -280,6 +303,20 @@ Confirm Password Change
+ +
Fetch Sign In Methods/Providers
+
+ + + +
@@ -359,6 +396,21 @@ +
+ + + + + +
+
diff --git a/packages/auth/demo/public/script.js b/packages/auth/demo/public/script.js index 55c7bea41e0..c62c2408a92 100644 --- a/packages/auth/demo/public/script.js +++ b/packages/auth/demo/public/script.js @@ -312,6 +312,49 @@ function onSignInWithEmailAndPassword() { } +/** + * Signs in a user with an email link. + */ +function onSignInWithEmailLink() { + var email = $('#sign-in-with-email-link-email').val(); + var link = $('#sign-in-with-email-link-link').val() || undefined; + if (auth.isSignInWithEmailLink(link)) { + auth.signInWithEmailLink(email, link).then(onAuthSuccess, onAuthError); + } else { + alertError('Sign in link is invalid'); + } +} + +/** + * Links a user with an email link. + */ +function onLinkWithEmailLink() { + var email = $('#link-with-email-link-email').val(); + var link = $('#link-with-email-link-link').val() || undefined; + var credential = firebase.auth.EmailAuthProvider + .credentialWithLink(email, link); + activeUser().linkAndRetrieveDataWithCredential(credential) + .then(onAuthUserCredentialSuccess, onAuthError); +} + + +/** + * Re-authenticate a user with email link credential. + */ +function onReauthenticateWithEmailLink() { + var email = $('#link-with-email-link-email').val(); + var link = $('#link-with-email-link-link').val() || undefined; + var credential = firebase.auth.EmailAuthProvider + .credentialWithLink(email, link); + activeUser().reauthenticateAndRetrieveDataWithCredential(credential) + .then(function(result) { + logAdditionalUserInfo(result); + refreshUserData(); + alertSuccess('User reauthenticated!'); + }, onAuthError); +} + + /** * Signs in with a custom token. * @param {DOMEvent} event HTML DOM event returned by the listener. @@ -581,6 +624,52 @@ function onUpdateProfile() { } +/** + * Sends sign in with email link to the user. + */ +function onSendSignInLinkToEmail() { + var email = $('#sign-in-with-email-link-email').val(); + auth.sendSignInLinkToEmail(email, getActionCodeSettings()).then(function() { + alertSuccess('Email sent!'); + }, onAuthError); +} + +/** + * Sends sign in with email link to the user and pass in current url. + */ +function onSendSignInLinkToEmailCurrentUrl() { + var email = $('#sign-in-with-email-link-email').val(); + var actionCodeSettings = { + 'url': window.location.href, + 'handleCodeInApp': true + }; + + auth.sendSignInLinkToEmail(email, actionCodeSettings).then(function() { + if ('localStorage' in window && window['localStorage'] !== null) { + window.localStorage.setItem( + 'emailForSignIn', + // Save the email and the timestamp. + JSON.stringify({ + email: email, + timestamp: new Date().getTime() + })); + } + alertSuccess('Email sent!'); + }, onAuthError); +} + + +/** + * Sends email link to link the user. + */ +function onSendLinkEmailLink() { + var email = $('#link-with-email-link-email').val(); + auth.sendSignInLinkToEmail(email, getActionCodeSettings()).then(function() { + alertSuccess('Email sent!'); + }, onAuthError); +} + + /** * Sends password reset email to the user. */ @@ -615,6 +704,41 @@ function onConfirmPasswordReset() { } +/** + * Gets the list of IDPs that can be used to log in for the given email address. + */ +function onFetchProvidersForEmail() { + var email = $('#fetch-providers-email').val(); + auth.fetchProvidersForEmail(email).then(function(providers) { + log('Providers for ' + email + ' :'); + log(providers); + if (providers.length == 0) { + alertSuccess('Providers for ' + email + ': N/A'); + } else { + alertSuccess('Providers for ' + email +': ' + providers.join(', ')); + } + }, onAuthError); +} + + +/** + * Gets the list of possible sign in methods for the given email address. + */ +function onFetchSignInMethodsForEmail() { + var email = $('#fetch-providers-email').val(); + auth.fetchSignInMethodsForEmail(email).then(function(signInMethods) { + log('Sign in methods for ' + email + ' :'); + log(signInMethods); + if (signInMethods.length == 0) { + alertSuccess('Sign In Methods for ' + email + ': N/A'); + } else { + alertSuccess( + 'Sign In Methods for ' + email +': ' + signInMethods.join(', ')); + } + }, onAuthError); +} + + /** * Fetches and logs the user's providers data. */ @@ -966,6 +1090,28 @@ function getParameterByName(name) { * the input field for the confirm email verification process. */ function populateActionCodes() { + var emailForSignIn = null; + var signInTime = 0; + if ('localStorage' in window && window['localStorage'] !== null) { + try { + // Try to parse as JSON first using new storage format. + var emailForSignInData = + JSON.parse(window.localStorage.getItem('emailForSignIn')); + emailForSignIn = emailForSignInData['email'] || null; + signInTime = emailForSignInData['timestamp'] || 0; + } catch (e) { + // JSON parsing failed. This means the email is stored in the old string + // format. + emailForSignIn = window.localStorage.getItem('emailForSignIn'); + } + if (emailForSignIn) { + // Clear old codes. Old format codes should be cleared immediately. + if (new Date().getTime() - signInTime >= 1 * 24 * 3600 * 1000) { + // Remove email from storage. + window.localStorage.removeItem('emailForSignIn'); + } + } + } var actionCode = getParameterByName('oobCode'); if (actionCode != null) { var mode = getParameterByName('mode'); @@ -973,6 +1119,14 @@ function populateActionCodes() { $('#email-verification-code').val(actionCode); } else if (mode == 'resetPassword') { $('#password-reset-code').val(actionCode); + } else if (mode == 'signIn') { + if (emailForSignIn) { + $('#sign-in-with-email-link-email').val(emailForSignIn); + $('#sign-in-with-email-link-link').val(window.location.href); + onSignInWithEmailLink(); + // Remove email from storage as the code is only usable once. + window.localStorage.removeItem('emailForSignIn'); + } } else { $('#email-verification-code').val(actionCode); $('#password-reset-code').val(actionCode); @@ -1153,11 +1307,19 @@ function initApp(){ e.preventDefault(); } }); + $('#sign-in-with-email-link').click(onSignInWithEmailLink); + $('#link-with-email-link').click(onLinkWithEmailLink); + $('#reauth-with-email-link').click(onReauthenticateWithEmailLink); $('#change-email').click(onChangeEmail); $('#change-password').click(onChangePassword); $('#update-profile').click(onUpdateProfile); + $('#send-sign-in-link-to-email').click(onSendSignInLinkToEmail); + $('#send-sign-in-link-to-email-current-url') + .click(onSendSignInLinkToEmailCurrentUrl); + $('#send-link-email-link').click(onSendLinkEmailLink); + $('#send-password-reset-email').click(onSendPasswordResetEmail); $('#verify-password-reset-code').click(onVerifyPasswordResetCode); $('#confirm-password-reset').click(onConfirmPasswordReset); @@ -1202,6 +1364,9 @@ function initApp(){ $('#set-language-code').click(onSetLanguageCode); $('#use-device-language').click(onUseDeviceLanguage); + + $('#fetch-providers-for-email').click(onFetchProvidersForEmail); + $('#fetch-sign-in-methods-for-email').click(onFetchSignInMethodsForEmail); } $(initApp); diff --git a/packages/auth/src/actioncodeinfo.js b/packages/auth/src/actioncodeinfo.js index 58529cd6f6c..34a5a66a529 100644 --- a/packages/auth/src/actioncodeinfo.js +++ b/packages/auth/src/actioncodeinfo.js @@ -41,12 +41,15 @@ fireauth.ActionCodeInfo = function(response) { var newEmail = response[fireauth.ActionCodeInfo.ServerFieldName.NEW_EMAIL]; var operation = response[fireauth.ActionCodeInfo.ServerFieldName.REQUEST_TYPE]; - if (!email || !operation) { + // Email could be empty only if the request type is EMAIL_SIGNIN. + if (!operation || + (operation != fireauth.ActionCodeInfo.RequestType.EMAIL_SIGNIN && + !email)) { // This is internal only. throw new Error('Invalid provider user info!'); } data[fireauth.ActionCodeInfo.DataField.FROM_EMAIL] = newEmail || null; - data[fireauth.ActionCodeInfo.DataField.EMAIL] = email; + data[fireauth.ActionCodeInfo.DataField.EMAIL] = email || null; fireauth.object.setReadonlyProperty( this, fireauth.ActionCodeInfo.PropertyName.OPERATION, @@ -58,6 +61,18 @@ fireauth.ActionCodeInfo = function(response) { }; +/** + * Firebase Auth Action Code Info requestType possible values. + * @enum {string} + */ +fireauth.ActionCodeInfo.RequestType = { + PASSWORD_RESET: 'PASSWORD_RESET', + RECOVER_EMAIL: 'RECOVER_EMAIL', + EMAIL_SIGNIN: 'EMAIL_SIGNIN', + VERIFY_EMAIL: 'VERIFY_EMAIL' +}; + + /** * The checkActionCode endpoint server response field names. * @enum {string} diff --git a/packages/auth/src/actioncodesettings.js b/packages/auth/src/actioncodesettings.js index 7099273ba16..f9af91b80ff 100644 --- a/packages/auth/src/actioncodesettings.js +++ b/packages/auth/src/actioncodesettings.js @@ -142,13 +142,6 @@ fireauth.ActionCodeSettings.prototype.initialize_ = function(settingsObj) { } /** @private {boolean} Whether the code can be handled in app. */ this.canHandleCodeInApp_ = !!canHandleCodeInApp; - // canHandleCodeInApp can't be true when no mobile application is provided. - if (this.canHandleCodeInApp_ && !this.ibi_ && !this.apn_) { - throw new fireauth.AuthError( - fireauth.authenum.Error.ARGUMENT_ERROR, - fireauth.ActionCodeSettings.RawField.HANDLE_CODE_IN_APP + - ' property can\'t be true when no mobile application is provided.'); - } }; @@ -227,3 +220,12 @@ fireauth.ActionCodeSettings.prototype.buildRequest = function() { } return request; }; + + +/** + * Returns the canHandleCodeInApp setting of ActionCodeSettings. + * @return {boolean} Whether the code can be handled in app. + */ +fireauth.ActionCodeSettings.prototype.canHandleCodeInApp = function() { + return this.canHandleCodeInApp_; +}; diff --git a/packages/auth/src/actioncodeurl.js b/packages/auth/src/actioncodeurl.js new file mode 100644 index 00000000000..10bca61d481 --- /dev/null +++ b/packages/auth/src/actioncodeurl.js @@ -0,0 +1,92 @@ +/** + * Copyright 2017 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 firebase.auth.ActionCodeUrl class which is the utility + * to parse action code URLs. + */ + +goog.provide('fireauth.ActionCodeUrl'); + +goog.require('goog.Uri'); + + +/** + * The utility class to help parse action code URLs used for out of band email + * flows such as password reset, email verification, email link sign in, etc. + * @param {string} actionCodeUrl The action code URL. + * @constructor + */ +fireauth.ActionCodeUrl = function(actionCodeUrl) { + /** @private {!goog.Uri} The action code URL components.*/ + this.uri_ = goog.Uri.parse(actionCodeUrl); +}; + + +/** + * Enums for fields in URL query string. + * @enum {string} + */ +fireauth.ActionCodeUrl.QueryField = { + API_KEY: 'apiKey', + CODE: 'oobCode', + MODE: 'mode' +}; + + +/** + * Enums for action code modes. + * @enum {string} + */ +fireauth.ActionCodeUrl.Mode = { + RESET_PASSWORD: 'resetPassword', + REVOKE_EMAIL: 'recoverEmail', + SIGN_IN: 'signIn', + VERIFY_EMAIL: 'verifyEmail' +}; + + +/** + * Returns the API key parameter of action code URL. + * @return {?string} The first API key value in action code URL or + * undefined if apiKey does not appear in the URL. + */ +fireauth.ActionCodeUrl.prototype.getApiKey = function() { + return this.uri_.getParameterValue( + fireauth.ActionCodeUrl.QueryField.API_KEY) || null; +}; + + +/** + * Returns the action code parameter of action code URL. + * @return {?string} The first oobCode value in action code URL or + * undefined if oobCode does not appear in the URL. + */ +fireauth.ActionCodeUrl.prototype.getCode = function() { + return this.uri_.getParameterValue( + fireauth.ActionCodeUrl.QueryField.CODE) || null; +}; + + +/** + * Returns the mode parameter of action code URL. + * @return {?string} The first mode value in action code URL or + * undefined if mode does not appear in the URL. + */ +fireauth.ActionCodeUrl.prototype.getMode = function() { + return this.uri_.getParameterValue( + fireauth.ActionCodeUrl.QueryField.MODE) || null; +}; diff --git a/packages/auth/src/auth.js b/packages/auth/src/auth.js index 7585b5b757d..75cdeff3d15 100644 --- a/packages/auth/src/auth.js +++ b/packages/auth/src/auth.js @@ -31,6 +31,7 @@ goog.require('fireauth.AuthEventManager'); goog.require('fireauth.AuthProvider'); goog.require('fireauth.AuthUser'); goog.require('fireauth.ConfirmationResult'); +goog.require('fireauth.EmailAuthProvider'); goog.require('fireauth.RpcHandler'); goog.require('fireauth.UserEventType'); goog.require('fireauth.authenum.Error'); @@ -1823,6 +1824,60 @@ fireauth.Auth.prototype.fetchProvidersForEmail = function(email) { }; +/** + * Gets the list of possible sign in methods for the given email address. + * @param {string} email The email address. + * @return {!goog.Promise>} + */ +fireauth.Auth.prototype.fetchSignInMethodsForEmail = function(email) { + return /** @type {!goog.Promise>} */ ( + this.registerPendingPromise_( + this.getRpcHandler().fetchSignInMethodsForIdentifier(email))); +}; + + +/** + * @param {string} emailLink The email link. + * @return {boolean} Whether the link is a sign in with email link. + */ +fireauth.Auth.prototype.isSignInWithEmailLink = function(emailLink) { + return !!fireauth.EmailAuthProvider + .getActionCodeFromSignInEmailLink(emailLink); +}; + + +/** + * Sends the sign-in with email link for the email account provided. + * @param {string} email The email account to sign in with. + * @param {!Object} actionCodeSettings The action code settings object. + * @return {!goog.Promise} + */ +fireauth.Auth.prototype.sendSignInLinkToEmail = function( + email, actionCodeSettings) { + var self = this; + return this.registerPendingPromise_( + // Wrap in promise as ActionCodeSettings constructor could throw a + // synchronous error if invalid arguments are specified. + goog.Promise.resolve() + .then(function() { + var actionCodeSettingsBuilder = + new fireauth.ActionCodeSettings(actionCodeSettings); + if (!actionCodeSettingsBuilder.canHandleCodeInApp()) { + throw new fireauth.AuthError( + fireauth.authenum.Error.ARGUMENT_ERROR, + fireauth.ActionCodeSettings.RawField.HANDLE_CODE_IN_APP + + ' must be true when sending sign in link to email'); + } + return actionCodeSettingsBuilder.buildRequest(); + }).then(function(additionalRequestData) { + return self.getRpcHandler().sendSignInLinkToEmail( + email, additionalRequestData); + }).then(function(email) { + // Do not return the email. + })); +}; + + /** * Verifies an email action code for password reset and returns a promise * that resolves with the associated email if verified. @@ -1930,3 +1985,23 @@ fireauth.Auth.prototype.signInWithPhoneNumber = // This will wait for redirectStateIsReady to resolve first. goog.bind(this.signInAndRetrieveDataWithCredential, this)))); }; + + +/** + * Signs in a Firebase User with the provided email and the passwordless + * sign-in email link. + * @param {string} email The email account to sign in with. + * @param {?string=} opt_link The optional link which contains the OTP needed + * to complete the sign in with email link. If not specified, the current + * URL is used instead. + * @return {!goog.Promise} + */ +fireauth.Auth.prototype.signInWithEmailLink = function(email, opt_link) { + var self = this; + return this.registerPendingPromise_( + goog.Promise.resolve().then(function() { + var credential = fireauth.EmailAuthProvider.credentialWithLink( + email, opt_link || fireauth.util.getCurrentUrl()); + return self.signInAndRetrieveDataWithCredential(credential); + })); +}; diff --git a/packages/auth/src/authcredential.js b/packages/auth/src/authcredential.js index 38fb56cc3d3..c5cffb50fa6 100644 --- a/packages/auth/src/authcredential.js +++ b/packages/auth/src/authcredential.js @@ -33,6 +33,7 @@ goog.provide('fireauth.PhoneAuthCredential'); goog.provide('fireauth.PhoneAuthProvider'); goog.provide('fireauth.TwitterAuthProvider'); +goog.require('fireauth.ActionCodeUrl'); goog.require('fireauth.AuthError'); goog.require('fireauth.IdToken'); goog.require('fireauth.authenum.Error'); @@ -159,10 +160,11 @@ fireauth.OAuthResponse; * @param {!fireauth.idp.ProviderId} providerId The provider ID. * @param {!fireauth.OAuthResponse} oauthResponse The OAuth * response object containing token information. + * @param {!fireauth.idp.SignInMethod} signInMethod The sign in method. * @constructor * @implements {fireauth.AuthCredential} */ -fireauth.OAuthCredential = function(providerId, oauthResponse) { +fireauth.OAuthCredential = function(providerId, oauthResponse, signInMethod) { if (oauthResponse['idToken'] || oauthResponse['accessToken']) { // OAuth 2 and either ID token or access token. if (oauthResponse['idToken']) { @@ -186,6 +188,7 @@ fireauth.OAuthCredential = function(providerId, oauthResponse) { } fireauth.object.setReadonlyProperty(this, 'providerId', providerId); + fireauth.object.setReadonlyProperty(this, 'signInMethod', signInMethod); }; @@ -272,7 +275,8 @@ fireauth.OAuthCredential.prototype.makeVerifyAssertionRequest_ = function() { */ fireauth.OAuthCredential.prototype.toPlainObject = function() { var obj = { - 'providerId': this['providerId'] + 'providerId': this['providerId'], + 'signInMethod': this['signInMethod'] }; if (this['idToken']) { obj['oauthIdToken'] = this['idToken']; @@ -422,7 +426,10 @@ fireauth.OAuthProvider.prototype.credential = function(opt_idToken, 'idToken': opt_idToken || null, 'accessToken': opt_accessToken || null }; - return new fireauth.OAuthCredential(this['providerId'], oauthResponse); + // For OAuthCredential, sign in method is same as providerId. + return new fireauth.OAuthCredential(this['providerId'], + oauthResponse, + this['providerId']); }; @@ -441,6 +448,9 @@ goog.inherits(fireauth.FacebookAuthProvider, fireauth.OAuthProvider); fireauth.object.setReadonlyProperty(fireauth.FacebookAuthProvider, 'PROVIDER_ID', fireauth.idp.ProviderId.FACEBOOK); +fireauth.object.setReadonlyProperty(fireauth.FacebookAuthProvider, + 'FACEBOOK_SIGN_IN_METHOD', fireauth.idp.SignInMethod.FACEBOOK); + /** * Initializes a Facebook AuthCredential. @@ -478,6 +488,9 @@ goog.inherits(fireauth.GithubAuthProvider, fireauth.OAuthProvider); fireauth.object.setReadonlyProperty(fireauth.GithubAuthProvider, 'PROVIDER_ID', fireauth.idp.ProviderId.GITHUB); +fireauth.object.setReadonlyProperty(fireauth.GithubAuthProvider, + 'GITHUB_SIGN_IN_METHOD', fireauth.idp.SignInMethod.GITHUB); + /** * Initializes a GitHub AuthCredential. @@ -519,6 +532,9 @@ goog.inherits(fireauth.GoogleAuthProvider, fireauth.OAuthProvider); fireauth.object.setReadonlyProperty(fireauth.GoogleAuthProvider, 'PROVIDER_ID', fireauth.idp.ProviderId.GOOGLE); +fireauth.object.setReadonlyProperty(fireauth.GoogleAuthProvider, + 'GOOGLE_SIGN_IN_METHOD', fireauth.idp.SignInMethod.GOOGLE); + /** * Initializes a Google AuthCredential. @@ -558,6 +574,9 @@ goog.inherits(fireauth.TwitterAuthProvider, fireauth.FederatedProvider); fireauth.object.setReadonlyProperty(fireauth.TwitterAuthProvider, 'PROVIDER_ID', fireauth.idp.ProviderId.TWITTER); +fireauth.object.setReadonlyProperty(fireauth.TwitterAuthProvider, + 'TWITTER_SIGN_IN_METHOD', fireauth.idp.SignInMethod.TWITTER); + /** * Initializes a Twitter AuthCredential. @@ -583,7 +602,8 @@ fireauth.TwitterAuthProvider.credential = function(tokenOrObject, secret) { } return new fireauth.OAuthCredential(fireauth.idp.ProviderId.TWITTER, - /** @type {!fireauth.OAuthResponse} */ (tokenObject)); + /** @type {!fireauth.OAuthResponse} */ (tokenObject), + fireauth.idp.SignInMethod.TWITTER); }; @@ -591,14 +611,21 @@ fireauth.TwitterAuthProvider.credential = function(tokenOrObject, secret) { * The email and password credential class. * @param {string} email The credential email. * @param {string} password The credential password. + * @param {string=} opt_signInMethod The credential sign in method can be either + * 'password' or 'emailLink' * @constructor * @implements {fireauth.AuthCredential} */ -fireauth.EmailAuthCredential = function(email, password) { +fireauth.EmailAuthCredential = function(email, password, opt_signInMethod) { this.email_ = email; this.password_ = password; fireauth.object.setReadonlyProperty(this, 'providerId', fireauth.idp.ProviderId.PASSWORD); + var signInMethod = opt_signInMethod === + fireauth.EmailAuthProvider['EMAIL_LINK_SIGN_IN_METHOD'] ? + fireauth.EmailAuthProvider['EMAIL_LINK_SIGN_IN_METHOD'] : + fireauth.EmailAuthProvider['EMAIL_PASSWORD_SIGN_IN_METHOD']; + fireauth.object.setReadonlyProperty(this, 'signInMethod', signInMethod); }; @@ -613,6 +640,10 @@ fireauth.EmailAuthCredential = function(email, password) { */ fireauth.EmailAuthCredential.prototype.getIdTokenProvider = function(rpcHandler) { + if (this['signInMethod'] == + fireauth.EmailAuthProvider['EMAIL_LINK_SIGN_IN_METHOD']) { + return rpcHandler.emailLinkSignIn(this.email_, this.password_); + } return rpcHandler.verifyPassword(this.email_, this.password_); }; @@ -628,6 +659,11 @@ fireauth.EmailAuthCredential.prototype.getIdTokenProvider = */ fireauth.EmailAuthCredential.prototype.linkToIdToken = function(rpcHandler, idToken) { + if (this['signInMethod'] == + fireauth.EmailAuthProvider['EMAIL_LINK_SIGN_IN_METHOD']) { + return rpcHandler.emailLinkSignInForLinking( + idToken, this.email_, this.password_); + } return rpcHandler.updateEmailAndPassword( idToken, this.email_, this.password_); }; @@ -658,7 +694,8 @@ fireauth.EmailAuthCredential.prototype.matchIdTokenWithUid = fireauth.EmailAuthCredential.prototype.toPlainObject = function() { return { 'email': this.email_, - 'password': this.password_ + 'password': this.password_, + 'signInMethod': this['signInMethod'] }; }; @@ -690,11 +727,53 @@ fireauth.EmailAuthProvider.credential = function(email, password) { }; +/** + * @param {string} email The credential email. + * @param {string} emailLink The credential email link. + * @return {!fireauth.EmailAuthCredential} The Auth credential object. + */ +fireauth.EmailAuthProvider.credentialWithLink = function(email, emailLink) { + var code = fireauth.EmailAuthProvider + .getActionCodeFromSignInEmailLink(emailLink); + if (!code) { + throw new fireauth.AuthError( + fireauth.authenum.Error.ARGUMENT_ERROR, 'Invalid email link!'); + } + return new fireauth.EmailAuthCredential(email, code, + fireauth.EmailAuthProvider['EMAIL_LINK_SIGN_IN_METHOD']); +}; + + +/** + * @param {string} emailLink The sign in email link to be validated. + * @return {?string} Action code if the email link is valid, otherwise null. + */ +fireauth.EmailAuthProvider.getActionCodeFromSignInEmailLink = + function(emailLink) { + var actionCodeUrl = new fireauth.ActionCodeUrl(emailLink); + var code = actionCodeUrl.getCode(); + if (actionCodeUrl.getMode() === fireauth.ActionCodeUrl.Mode.SIGN_IN && code) { + return code; + } + return null; +}; + + // Set read only PROVIDER_ID property. fireauth.object.setReadonlyProperties(fireauth.EmailAuthProvider, { 'PROVIDER_ID': fireauth.idp.ProviderId.PASSWORD }); +// Set read only EMAIL_LINK_SIGN_IN_METHOD property. +fireauth.object.setReadonlyProperties(fireauth.EmailAuthProvider, { + 'EMAIL_LINK_SIGN_IN_METHOD': fireauth.idp.SignInMethod.EMAIL_LINK +}); + +// Set read only EMAIL_PASSWORD_SIGN_IN_METHOD property. +fireauth.object.setReadonlyProperties(fireauth.EmailAuthProvider, { + 'EMAIL_PASSWORD_SIGN_IN_METHOD': fireauth.idp.SignInMethod.EMAIL_PASSWORD +}); + /** * A credential for phone number sign-in. @@ -721,6 +800,9 @@ fireauth.PhoneAuthCredential = function(params) { fireauth.object.setReadonlyProperty(this, 'providerId', fireauth.idp.ProviderId.PHONE); + + fireauth.object.setReadonlyProperty( + this, 'signInMethod', fireauth.idp.SignInMethod.PHONE); }; @@ -957,6 +1039,12 @@ fireauth.object.setReadonlyProperties(fireauth.PhoneAuthProvider, { }); +// Set read only PHONE_SIGN_IN_METHOD property. +fireauth.object.setReadonlyProperties(fireauth.PhoneAuthProvider, { + 'PHONE_SIGN_IN_METHOD': fireauth.idp.SignInMethod.PHONE +}); + + /** * Constructs an Auth credential from a backend response. * @param {?Object} response The backend response to build a credential from. diff --git a/packages/auth/src/exports_auth.js b/packages/auth/src/exports_auth.js index 8f93162e404..496cb50fe1a 100644 --- a/packages/auth/src/exports_auth.js +++ b/packages/auth/src/exports_auth.js @@ -66,10 +66,18 @@ fireauth.exportlib.exportPrototypeMethods( name: 'fetchProvidersForEmail', args: [fireauth.args.string('email')] }, + fetchSignInMethodsForEmail: { + name: 'fetchSignInMethodsForEmail', + args: [fireauth.args.string('email')] + }, getRedirectResult: { name: 'getRedirectResult', args: [] }, + isSignInWithEmailLink: { + name: 'isSignInWithEmailLink', + args: [fireauth.args.string('emailLink')] + }, onAuthStateChanged: { name: 'onAuthStateChanged', args: [ @@ -103,6 +111,13 @@ fireauth.exportlib.exportPrototypeMethods( true) ] }, + sendSignInLinkToEmail: { + name: 'sendSignInLinkToEmail', + args: [ + fireauth.args.string('email'), + fireauth.args.object('actionCodeSettings') + ] + }, setPersistence: { name: 'setPersistence', args: [fireauth.args.string('persistence')] @@ -135,6 +150,12 @@ fireauth.exportlib.exportPrototypeMethods( name: 'signInWithEmailAndPassword', args: [fireauth.args.string('email'), fireauth.args.string('password')] }, + signInWithEmailLink: { + name: 'signInWithEmailLink', + args: [ + fireauth.args.string('email'), fireauth.args.string('emailLink', true) + ] + }, signInAndRetrieveDataWithEmailAndPassword: { name: 'signInAndRetrieveDataWithEmailAndPassword', args: [fireauth.args.string('email'), fireauth.args.string('password')] @@ -341,6 +362,12 @@ fireauth.exportlib.exportFunction( fireauth.args.or(fireauth.args.string(), fireauth.args.object(), 'token') ]); +fireauth.exportlib.exportFunction( + fireauth.EmailAuthProvider, 'credentialWithLink', + fireauth.EmailAuthProvider.credentialWithLink, [ + fireauth.args.string('email'), + fireauth.args.string('emailLink') + ]); fireauth.exportlib.exportPrototypeMethods( fireauth.GithubAuthProvider.prototype, { diff --git a/packages/auth/src/idp.js b/packages/auth/src/idp.js index 1391b9a95ee..5df9af2d1de 100644 --- a/packages/auth/src/idp.js +++ b/packages/auth/src/idp.js @@ -22,6 +22,7 @@ goog.provide('fireauth.idp'); goog.provide('fireauth.idp.IdpSettings'); goog.provide('fireauth.idp.ProviderId'); goog.provide('fireauth.idp.Settings'); +goog.provide('fireauth.idp.SignInMethod'); /** @@ -43,6 +44,21 @@ fireauth.idp.ProviderId = { }; +/** + * Enums for supported sign in methods. + * @enum {string} + */ +fireauth.idp.SignInMethod = { + EMAIL_LINK: 'emailLink', + EMAIL_PASSWORD: 'password', + FACEBOOK: 'facebook.com', + GITHUB: 'github.com', + GOOGLE: 'google.com', + PHONE: 'phone', + TWITTER: 'twitter.com' +}; + + /** * The settings of an identity provider. The fields are: *
    diff --git a/packages/auth/src/rpchandler.js b/packages/auth/src/rpchandler.js index a127553d08b..31e7c9213b5 100644 --- a/packages/auth/src/rpchandler.js +++ b/packages/auth/src/rpchandler.js @@ -231,6 +231,7 @@ fireauth.RpcHandler.AuthServerField = { REFRESH_TOKEN: 'refreshToken', SESSION_ID: 'sessionId', SESSION_INFO: 'sessionInfo', + SIGNIN_METHODS: 'signinMethods', TEMPORARY_PROOF: 'temporaryProof' }; @@ -240,6 +241,7 @@ fireauth.RpcHandler.AuthServerField = { * @enum {string} */ fireauth.RpcHandler.GetOobCodeRequestType = { + EMAIL_SIGNIN: 'EMAIL_SIGNIN', NEW_EMAIL_ACCEPT: 'NEW_EMAIL_ACCEPT', PASSWORD_RESET: 'PASSWORD_RESET', VERIFY_EMAIL: 'VERIFY_EMAIL' @@ -883,6 +885,31 @@ fireauth.RpcHandler.prototype.fetchProvidersForIdentifier = }; +/** + * Returns the list of sign in methods for the given identifier. + * @param {string} identifier The identifier, such as an email address. + * @return {!goog.Promise>} + */ +fireauth.RpcHandler.prototype.fetchSignInMethodsForIdentifier = function( + identifier) { + // createAuthUri returns an error if continue URI is not http or https. + // For environments like Cordova, Chrome extensions, native frameworks, file + // systems, etc, use http://localhost as continue URL. + var continueUri = fireauth.util.isHttpOrHttps() ? + fireauth.util.getCurrentUrl() : + 'http://localhost'; + var request = { + 'identifier': identifier, + 'continueUri': continueUri + }; + return this.invokeRpc_(fireauth.RpcHandler.ApiMethod.CREATE_AUTH_URI, request) + .then(function(response) { + return response[fireauth.RpcHandler.AuthServerField.SIGNIN_METHODS] || + []; + }); +}; + + /** * Gets the list of authorized domains for the specified project. * @return {!goog.Promise>} @@ -1056,6 +1083,44 @@ fireauth.RpcHandler.prototype.verifyPassword = function(email, password) { }; +/** + * Verifies an email link OTP for sign-in and returns a Promise that resolves + * with the ID token. + * @param {string} email The email address. + * @param {string} oobCode The email action OTP. + * @return {!goog.Promise} + */ +fireauth.RpcHandler.prototype.emailLinkSignIn = function(email, oobCode) { + var request = { + 'email': email, + 'oobCode': oobCode + }; + return this.invokeRpc_( + fireauth.RpcHandler.ApiMethod.EMAIL_LINK_SIGNIN, request); +}; + + +/** + * Verifies an email link OTP for linking and returns a Promise that resolves + * with the ID token. + * @param {string} idToken The ID token. + * @param {string} email The email address. + * @param {string} oobCode The email action OTP. + * @return {!goog.Promise} + */ +fireauth.RpcHandler.prototype.emailLinkSignInForLinking = + function(idToken, email, oobCode) { + var request = { + 'idToken': idToken, + 'email': email, + 'oobCode': oobCode + }; + return this.invokeRpc_( + fireauth.RpcHandler.ApiMethod.EMAIL_LINK_SIGNIN_FOR_LINKING, + request); +}; + + /** * Validates a response that should contain an ID token. * @param {?Object} response The server response data. @@ -1304,8 +1369,22 @@ fireauth.RpcHandler.validateOobCodeRequest_ = function(request) { /** - * Validates a request for an email action code for password reset. - * @param {!Object} request The getOobCode request data for password reset. + * Validates a request for an email action for passwordless email sign-in. + * @param {!Object} request The getOobCode request data for email sign-in. + * @private + */ +fireauth.RpcHandler.validateEmailSignInCodeRequest_ = function(request) { + if (request['requestType'] != + fireauth.RpcHandler.GetOobCodeRequestType.EMAIL_SIGNIN) { + throw new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + } + fireauth.RpcHandler.validateRequestHasEmail_(request); +}; + + +/** + * Validates a request for an email action for email verification. + * @param {!Object} request The getOobCode request data for email verification. * @private */ fireauth.RpcHandler.validateEmailVerificationCodeRequest_ = function(request) { @@ -1335,6 +1414,26 @@ fireauth.RpcHandler.prototype.sendPasswordResetEmail = }; +/** + * Requests getOobCode endpoint for passwordless email sign-in, returns promise + * that resolves with user's email. + * @param {string} email The email account to sign in with. + * @param {!Object} additionalRequestData Additional data to add to the request. + * @return {!goog.Promise} + */ +fireauth.RpcHandler.prototype.sendSignInLinkToEmail = function( + email, additionalRequestData) { + var request = { + 'requestType': fireauth.RpcHandler.GetOobCodeRequestType.EMAIL_SIGNIN, + 'email': email + }; + // Extend the original request with the additional data. + goog.object.extend(request, additionalRequestData); + return this.invokeRpc_( + fireauth.RpcHandler.ApiMethod.GET_EMAIL_SIGNIN_CODE, request); +}; + + /** * Requests getOobCode endpoint for email verification, returns promise that * resolves with user's email. @@ -1800,6 +1899,20 @@ fireauth.RpcHandler.ApiMethod = { requestRequiredFields: ['idToken', 'deleteProvider'], requestValidator: fireauth.RpcHandler.validateDeleteLinkedAccountsRequest_ }, + EMAIL_LINK_SIGNIN: { + endpoint: 'emailLinkSignin', + requestRequiredFields: ['email', 'oobCode'], + requestValidator: fireauth.RpcHandler.validateRequestHasEmail_, + responseValidator: fireauth.RpcHandler.validateIdTokenResponse_, + returnSecureToken: true + }, + EMAIL_LINK_SIGNIN_FOR_LINKING: { + endpoint: 'emailLinkSignin', + requestRequiredFields: ['idToken', 'email', 'oobCode'], + requestValidator: fireauth.RpcHandler.validateRequestHasEmail_, + responseValidator: fireauth.RpcHandler.validateIdTokenResponse_, + returnSecureToken: true + }, GET_ACCOUNT_INFO: { endpoint: 'getAccountInfo' }, @@ -1808,6 +1921,12 @@ fireauth.RpcHandler.ApiMethod = { requestRequiredFields: ['continueUri', 'providerId'], responseValidator: fireauth.RpcHandler.validateGetAuthResponse_ }, + GET_EMAIL_SIGNIN_CODE: { + endpoint: 'getOobConfirmationCode', + requestRequiredFields: ['requestType'], + requestValidator: fireauth.RpcHandler.validateEmailSignInCodeRequest_, + responseField: fireauth.RpcHandler.AuthServerField.EMAIL + }, GET_EMAIL_VERIFICATION_CODE: { endpoint: 'getOobConfirmationCode', requestRequiredFields: ['idToken', 'requestType'], diff --git a/packages/auth/test/actioncodeinfo_test.js b/packages/auth/test/actioncodeinfo_test.js index 6ecb1c90fa5..b79a3489f38 100644 --- a/packages/auth/test/actioncodeinfo_test.js +++ b/packages/auth/test/actioncodeinfo_test.js @@ -43,6 +43,10 @@ var recoverEmailServerResponse = { 'newEmail': 'newUser@example.com', 'requestType': 'RECOVER_EMAIL' }; +var signInWithEmailLinkServerResponse = { + 'kind': 'identitytoolkit#ResetPasswordResponse', + 'requestType': 'EMAIL_SIGNIN' +}; function testActionCodeInfo_invalid_missingOperation() { @@ -139,3 +143,24 @@ function testActionCodeInfo_recoverEmail() { expectedData, actionCodeInfo['data']); } + + +function testActionCodeInfo_signInWithEmailLink() { + var expectedData = {email: null, fromEmail: null}; + var actionCodeInfo = + new fireauth.ActionCodeInfo(signInWithEmailLinkServerResponse); + + // Check operation. + assertEquals('EMAIL_SIGNIN', actionCodeInfo['operation']); + // Property should be read-only. + actionCodeInfo['operation'] = 'BLA'; + assertEquals('EMAIL_SIGNIN', actionCodeInfo['operation']); + + // Check data. + assertObjectEquals(expectedData, actionCodeInfo['data']); + // Property should be read-only. + actionCodeInfo['data']['email'] = 'other@example.com'; + assertObjectEquals(expectedData, actionCodeInfo['data']); + actionCodeInfo['data'] = 'BLA'; + assertObjectEquals(expectedData, actionCodeInfo['data']); +} diff --git a/packages/auth/test/actioncodesettings_test.js b/packages/auth/test/actioncodesettings_test.js index 45af297aa7d..fd22a100798 100644 --- a/packages/auth/test/actioncodesettings_test.js +++ b/packages/auth/test/actioncodesettings_test.js @@ -147,14 +147,21 @@ function testActionCodeSettings_error_continueUrl() { } +function testActionCodeSettings_success_urlOnly_canHandleCodeInApp() { + var settings = { + 'url': 'https://www.example.com/?state=abc', + 'handleCodeInApp': true + }; + var expectedRequest = { + 'continueUrl': 'https://www.example.com/?state=abc', + 'canHandleCodeInApp': true + }; + var actionCodeSettings = new fireauth.ActionCodeSettings(settings); + assertObjectEquals(expectedRequest, actionCodeSettings.buildRequest()); +} + + function testActionCodeSettings_error_canHandleCodeInApp() { - // Can handle code in app but no app specified. - assertActionCodeSettingsErrorThrown( - { - 'url': 'https://www.example.com/?state=abc', - 'handleCodeInApp': true - }, - 'auth/argument-error'); // Non-boolean can handle code in app. assertActionCodeSettingsErrorThrown( { diff --git a/packages/auth/test/actioncodeurl_test.js b/packages/auth/test/actioncodeurl_test.js new file mode 100644 index 00000000000..164e43744d9 --- /dev/null +++ b/packages/auth/test/actioncodeurl_test.js @@ -0,0 +1,66 @@ +/** + * Copyright 2017 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 actioncodeurl.js. + */ + +goog.provide('fireauth.ActionCodeUrlTest'); + +goog.require('fireauth.ActionCodeUrl'); +goog.require('goog.testing.jsunit'); + +goog.setTestOnly(); + + +function testActionCodeUrl_success() { + var url = 'https://www.example.com/finishSignIn?' + + 'oobCode=CODE&mode=signIn&apiKey=API_KEY&state=bla'; + var actionCodeUrl = new fireauth.ActionCodeUrl(url); + assertEquals('signIn', actionCodeUrl.getMode()); + assertEquals('CODE', actionCodeUrl.getCode()); + assertEquals('API_KEY', actionCodeUrl.getApiKey()); +} + + +function testActionCodeUrl_success_portNumberInUrl() { + var url = 'https://www.example.com:8080/finishSignIn?' + + 'oobCode=CODE&mode=signIn&apiKey=API_KEY&state=bla'; + var actionCodeUrl = new fireauth.ActionCodeUrl(url); + assertEquals('signIn', actionCodeUrl.getMode()); + assertEquals('CODE', actionCodeUrl.getCode()); + assertEquals('API_KEY', actionCodeUrl.getApiKey()); +} + + +function testActionCodeUrl_success_hashParameters() { + var url = 'https://www.example.com/finishSignIn?' + + 'oobCode=CODE1&mode=signIn&apiKey=API_KEY1&state=bla' + + '#oobCode=CODE2&mode=signIn&apiKey=API_KEY2&state=bla'; + var actionCodeUrl = new fireauth.ActionCodeUrl(url); + assertEquals('signIn', actionCodeUrl.getMode()); + assertEquals('CODE1', actionCodeUrl.getCode()); + assertEquals('API_KEY1', actionCodeUrl.getApiKey()); +} + + +function testActionCodeUrl_emptyParameter() { + var url = 'https://www.example.com/finishSignIn'; + var actionCodeUrl = new fireauth.ActionCodeUrl(url); + assertNull(actionCodeUrl.getMode()); + assertNull(actionCodeUrl.getCode()); + assertNull(actionCodeUrl.getApiKey()); +} diff --git a/packages/auth/test/auth_test.js b/packages/auth/test/auth_test.js index 516d09ea30a..9c3b4eac0a8 100644 --- a/packages/auth/test/auth_test.js +++ b/packages/auth/test/auth_test.js @@ -1349,6 +1349,73 @@ function testFetchProvidersForEmail() { } +function testFetchSignInMethodsForEmail() { + var email = 'foo@bar.com'; + var expectedSignInMethods = ['password', 'google.com']; + + asyncTestCase.waitForSignals(1); + + // Simulate successful RpcHandler fetchSignInMethodsForIdentifier. + stubs.replace( + fireauth.RpcHandler.prototype, 'fetchSignInMethodsForIdentifier', + function(data) { + assertObjectEquals(email, data); + return goog.Promise.resolve(expectedSignInMethods); + }); + + app1 = firebase.initializeApp(config1, appId1); + auth1 = app1.auth(); + auth1.fetchSignInMethodsForEmail(email).then(function(signInMethods) { + assertArrayEquals(expectedSignInMethods, signInMethods); + asyncTestCase.signal(); + }); + assertAuthTokenListenerCalledOnce(auth1); +} + + +function testFetchSignInMethodsForEmail_error() { + var email = 'foo@bar.com'; + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR); + + asyncTestCase.waitForSignals(1); + + stubs.replace( + fireauth.RpcHandler.prototype, 'fetchSignInMethodsForIdentifier', + function(data) { + assertObjectEquals(email, data); + return goog.Promise.reject(expectedError); + }); + + app1 = firebase.initializeApp(config1, appId1); + auth1 = app1.auth(); + auth1.fetchSignInMethodsForEmail(email) + .then(function(signInMethods) { + fail('fetchSignInMethodsForEmail should not resolve!'); + }).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); + assertAuthTokenListenerCalledOnce(auth1); +} + + +function testIsSignInWithEmailLink() { + var emailLink1 = 'https://www.example.com/action?mode=signIn&oobCode=oobCode'; + var emailLink2 = 'https://www.example.com/action?mode=verifyEmail&' + + 'oobCode=oobCode'; + var emailLink3 = 'https://www.example.com/action?mode=signIn'; + app1 = firebase.initializeApp(config1, appId1); + auth1 = app1.auth(); + var isSignInLink1 = auth1.isSignInWithEmailLink(emailLink1); + assertEquals(true, isSignInLink1); + var isSignInLink2 = auth1.isSignInWithEmailLink(emailLink2); + assertEquals(false, isSignInLink2); + var isSignInLink3 = auth1.isSignInWithEmailLink(emailLink3); + assertEquals(false, isSignInLink3); +} + + function testAuth_pendingPromises() { asyncTestCase.waitForSignals(1); // Simulate available token. @@ -1433,6 +1500,116 @@ function testAuth_delete() { } +/** + * Tests sendSignInLinkToEmail successful operation with action code settings. + */ +function testSendSignInLinkToEmail_success() { + var expectedEmail = 'user@example.com'; + // Simulate successful RpcHandler sendSignInLinkToEmail. + stubs.replace( + fireauth.RpcHandler.prototype, + 'sendSignInLinkToEmail', + function(email, actualActionCodeSettings) { + assertObjectEquals( + new fireauth.ActionCodeSettings(actionCodeSettings).buildRequest(), + actualActionCodeSettings); + assertEquals(expectedEmail, email); + return goog.Promise.resolve(expectedEmail); + }); + app1 = firebase.initializeApp(config1, appId1); + auth1 = app1.auth(); + assertAuthTokenListenerCalledOnce(auth1); + auth1.sendSignInLinkToEmail(expectedEmail, actionCodeSettings) + .then(function() { + asyncTestCase.signal(); + }); + asyncTestCase.waitForSignals(1); +} + + +/** + * Tests sendSignInLinkToEmail failing operation due to backend error. + */ +function testSendSignInLinkToEmail_error() { + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR); + var expectedEmail = 'user@example.com'; + // Simulate unsuccessful RpcHandler sendSignInLinkToEmail. + stubs.replace( + fireauth.RpcHandler.prototype, + 'sendSignInLinkToEmail', + function(email, actualActionCodeSettings) { + assertObjectEquals( + new fireauth.ActionCodeSettings(actionCodeSettings).buildRequest(), + actualActionCodeSettings); + return goog.Promise.reject(expectedError); + }); + asyncTestCase.waitForSignals(1); + app1 = firebase.initializeApp(config1, appId1); + auth1 = app1.auth(); + assertAuthTokenListenerCalledOnce(auth1); + auth1.sendSignInLinkToEmail(expectedEmail, actionCodeSettings) + .then(function() { + fail('sendSignInLinkToEmail should not resolve!'); + }).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests sendSignInLinkToEmail empty continue URL in action code settings. + */ +function testSendSignInLinkToEmail_emptyContinueUrl_error() { + var settings = { + 'url': '', + 'handleCodeInApp': true + }; + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.INVALID_CONTINUE_URI); + + var expectedEmail = 'user@example.com'; + asyncTestCase.waitForSignals(1); + app1 = firebase.initializeApp(config1, appId1); + auth1 = app1.auth(); + assertAuthTokenListenerCalledOnce(auth1); + auth1.sendSignInLinkToEmail(expectedEmail, settings) + .then(function() { + fail('sendSignInLinkToEmail should not resolve!'); + }).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests sendSignInLinkToEmail invalid handleCodeInApp settings. + */ +function testSendSignInLinkToEmail_handleCodeInApp_error() { + var settings = { + 'url': 'https://www.example.com/?state=abc', + 'handleCodeInApp': false + }; + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.ARGUMENT_ERROR, + 'handleCodeInApp must be true when sending sign in link to email'); + var expectedEmail = 'user@example.com'; + asyncTestCase.waitForSignals(1); + app1 = firebase.initializeApp(config1, appId1); + auth1 = app1.auth(); + assertAuthTokenListenerCalledOnce(auth1); + auth1.sendSignInLinkToEmail(expectedEmail, settings) + .then(function() { + fail('sendSignInLinkToEmail should not resolve!'); + }).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + /** * Tests sendPasswordResetEmail successful operation with no action code * settings. @@ -3733,6 +3910,135 @@ function testAuth_signInAndRetrieveDataWithCustomToken_error() { } +function testAuth_signInWithEmailLink_success() { + // Tests successful signInWithEmailLink. + fireauth.AuthEventManager.ENABLED = true; + // Expected email and link. + var expectedEmail = 'user@example.com'; + var expectedLink = 'https://www.example.com?mode=signIn&oobCode=code'; + var expectedOobCode = 'code'; + var expectedIdToken = 'HEAD.ew0KICAiaXNzIjogImh0dHBzOi8vc2VjdXJldG9rZW4uZ2' + + '9vZ2xlLmNvbS8xMjM0NTY3OCIsDQogICJwaWN0dXJlIjogImh0dHBzOi8vcGx1cy5nb29' + + 'nbGUuY29tL2FiY2RlZmdoaWprbG1ub3BxcnN0dSIsDQogICJhdWQiOiAiMTIzNDU2Nzgi' + + 'LA0KICAiYXV0aF90aW1lIjogMTUxMDM1NzYyMiwNCiAgInVzZXJfaWQiOiAiYWJjZGVmZ' + + '2hpamtsbW5vcHFyc3R1IiwNCiAgInN1YiI6ICJhYmNkZWZnaGlqa2xtbm9wcXJzdHUiLA' + + '0KICAiaWF0IjogMTUxMDM1NzYyMiwNCiAgImV4cCI6IDE1MTAzNjEyMjIsDQogICJlbWF' + + 'pbCI6ICJ1c2VyQGV4YW1wbGUuY29tIiwNCiAgImVtYWlsX3ZlcmlmaWVkIjogdHJ1ZSwN' + + 'CiAgImZpcmViYXNlIjogew0KICAgICJpZGVudGl0aWVzIjogew0KICAgICAgImVtYWlsI' + + 'jogWw0KICAgICAgICAidXNlckBleGFtcGxlLmNvbSINCiAgICAgIF0NCiAgICB9LA0KIC' + + 'AgICJzaWduX2luX3Byb3ZpZGVyIjogInBhc3N3b3JkIg0KICB9DQp9.SIGNATURE'; + expectedTokenResponse['idToken'] = expectedIdToken; + + // Stub OAuth sign in handler. + fakeOAuthSignInHandler(); + // signInWithIdTokenResponse should initialize a user using the expected + // token response generated by RPC response. + stubs.replace( + fireauth.Auth.prototype, + 'signInWithIdTokenResponse', + function(tokenResponse) { + // Token response should match rpcHandler response. + assertObjectEquals(expectedTokenResponse, tokenResponse); + // Simulate user sign in completed and returned. + auth1.setCurrentUser_(user1); + asyncTestCase.signal(); + return goog.Promise.resolve(); + }); + // emailLinkSignIn should be called with expected parameters and resolved + // with expected token response. + stubs.replace( + fireauth.RpcHandler.prototype, + 'emailLinkSignIn', + function(email, oobCode) { + assertEquals(expectedEmail, email); + assertEquals(expectedOobCode, oobCode); + asyncTestCase.signal(); + return goog.Promise.resolve(expectedTokenResponse); + }); + asyncTestCase.waitForSignals(3); + // Initialize expected user. + var user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + var expectedResult = { + 'user': user1, + 'credential': null, + 'additionalUserInfo': {'providerId': 'password', 'isNewUser': false}, + 'operationType': fireauth.constants.OperationType.SIGN_IN + }; + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Sign in with email and password. + auth1.signInWithEmailLink(expectedEmail, expectedLink) + .then(function(result) { + assertObjectEquals(expectedResult, result); + asyncTestCase.signal(); + }); +} + + +function testAuth_signInWithEmailLink_error() { + // Tests successful signInWithEmailLink. + fireauth.AuthEventManager.ENABLED = true; + // Expected email and link. + var expectedEmail = 'user@example.com'; + var expectedLink = 'https://www.example.com?mode=signIn&oobCode=code'; + var expectedOobCode = 'code'; + // Expected RPC error. + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + // Stub OAuth sign in handler. + fakeOAuthSignInHandler(); + // signInWithIdTokenResponse should initialize a user using the expected + // token response generated by RPC response. + stubs.replace( + fireauth.Auth.prototype, + 'signInWithIdTokenResponse', + function(tokenResponse) { + fail('signInWithIdTokenResponse should not be called!'); + }); + // emailLinkSignIn should be called with expected parameters and resolved + // with expected error. + stubs.replace( + fireauth.RpcHandler.prototype, + 'emailLinkSignIn', + function(email, oobCode) { + assertEquals(expectedEmail, email); + assertEquals(expectedOobCode, oobCode); + asyncTestCase.signal(); + return goog.Promise.reject(expectedError); + }); + asyncTestCase.waitForSignals(2); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Sign in with email and password should throw expected error. + auth1.signInWithEmailLink(expectedEmail, expectedLink) + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testAuth_signInWithEmailLink_invalidLink_error() { + // Tests signInWithEmailLink when an invalid link is provided. + fireauth.AuthEventManager.ENABLED = true; + // Expected email and link. + var expectedEmail = 'user@example.com'; + var expectedLink = 'https://www.example.com?mode=signIn'; + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.ARGUMENT_ERROR, 'Invalid email link!'); + asyncTestCase.waitForSignals(1); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Sign in with email and password should throw expected error. + auth1.signInWithEmailLink(expectedEmail, expectedLink) + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + function testAuth_signInWithEmailAndPassword_success() { // Tests successful signInWithEmailAndPassword. fireauth.AuthEventManager.ENABLED = true; diff --git a/packages/auth/test/authcredential_test.js b/packages/auth/test/authcredential_test.js index abdc18de6b4..2a1d53a8707 100644 --- a/packages/auth/test/authcredential_test.js +++ b/packages/auth/test/authcredential_test.js @@ -39,6 +39,7 @@ goog.require('fireauth.authenum.Error'); goog.require('fireauth.common.testHelper'); goog.require('fireauth.deprecation'); goog.require('fireauth.idp.ProviderId'); +goog.require('fireauth.idp.SignInMethod'); goog.require('fireauth.util'); goog.require('goog.Promise'); goog.require('goog.testing.MockControl'); @@ -83,6 +84,16 @@ function setUp() { goog.testing.recordFunction(function(request) { return goog.Promise.resolve(responseForIdToken); })); + stubs.replace( + fireauth.RpcHandler.prototype, 'emailLinkSignIn', + goog.testing.recordFunction(function(request) { + return goog.Promise.resolve(responseForIdToken); + })); + stubs.replace( + fireauth.RpcHandler.prototype, 'emailLinkSignInForLinking', + goog.testing.recordFunction(function(request) { + return goog.Promise.resolve(responseForIdToken); + })); stubs.replace( fireauth.RpcHandler.prototype, 'verifyAssertionForLinking', @@ -157,9 +168,9 @@ function assertRpcHandlerVerifyAssertion(request) { /** - * Assert that the correct request is sent to RPC handler verifyPassword. - * @param {!string} email The email in verifyPassword request. - * @param {!string} password The password in verifyPassword request. + * Asserts that the correct request is sent to RPC handler verifyPassword. + * @param {string} email The email in verifyPassword request. + * @param {string} password The password in verifyPassword request. */ function assertRpcHandlerVerifyPassword(email, password) { assertEquals( @@ -178,6 +189,46 @@ function assertRpcHandlerVerifyPassword(email, password) { } +/** + * Asserts that the correct request is sent to RPC handler emailLinkSignIn. + * @param {string} email The email in emailLinkSignIn request. + * @param {string} oobCode The oobCode in emailLinkSignIn request. + */ +function assertRpcHandlerEmailLinkSignIn(email, oobCode) { + assertEquals(1, fireauth.RpcHandler.prototype.emailLinkSignIn.getCallCount()); + assertObjectEquals( + email, + fireauth.RpcHandler.prototype.emailLinkSignIn.getLastCall().getArgument( + 0)); + assertObjectEquals( + oobCode, + fireauth.RpcHandler.prototype.emailLinkSignIn.getLastCall().getArgument( + 1)); +} + + +/** + * Asserts that the correct request is sent to RPC handler + * emailLinkSignInForLinking. + * @param {string} idToken The idToken in emailLinkSignInForLinking request. + * @param {string} email The email in emailLinkSignInForLinking request. + * @param {string} oobCode The oobCode in emailLinkSignInForLinking request. + */ +function assertRpcHandlerEmailLinkSignInForLinking(idToken, email, oobCode) { + assertEquals(1, fireauth.RpcHandler.prototype.emailLinkSignInForLinking + .getCallCount()); + assertObjectEquals( + idToken, fireauth.RpcHandler.prototype.emailLinkSignInForLinking + .getLastCall().getArgument(0)); + assertObjectEquals( + email, fireauth.RpcHandler.prototype.emailLinkSignInForLinking + .getLastCall().getArgument(1)); + assertObjectEquals( + oobCode, fireauth.RpcHandler.prototype.emailLinkSignInForLinking + .getLastCall().getArgument(2)); +} + + /** * Assert that the correct request is sent to RPC handler * verifyAssertionForLinking. @@ -306,12 +357,14 @@ function testOAuthCredential() { assertEquals('exampleIdToken', authCredential['idToken']); assertEquals('exampleAccessToken', authCredential['accessToken']); assertEquals('example.com', authCredential['providerId']); + assertEquals('example.com', authCredential['signInMethod']); authCredential.getIdTokenProvider(rpcHandler); assertObjectEquals( { 'oauthAccessToken': 'exampleAccessToken', 'oauthIdToken': 'exampleIdToken', - 'providerId': 'example.com' + 'providerId': 'example.com', + 'signInMethod': 'example.com' }, authCredential.toPlainObject()); assertRpcHandlerVerifyAssertion({ @@ -419,7 +472,8 @@ function testOAuthProvider_getCredentialFromResponse() { fireauth.AuthProvider.getCredentialFromResponse({ 'oauthAccessToken': 'exampleAccessToken', 'oauthIdToken': 'exampleIdToken', - 'providerId': 'example.com' + 'providerId': 'example.com', + 'signInMethod': 'example.com' }).toPlainObject()); } @@ -433,7 +487,8 @@ function testOAuthProvider_getCredentialFromResponse_accessTokenOnly() { authCredential.toPlainObject(), fireauth.AuthProvider.getCredentialFromResponse({ 'oauthAccessToken': 'exampleAccessToken', - 'providerId': 'example.com' + 'providerId': 'example.com', + 'signInMethod': 'example.com' }).toPlainObject()); } @@ -446,7 +501,8 @@ function testOAuthProvider_getCredentialFromResponse_idTokenOnly() { authCredential.toPlainObject(), fireauth.AuthProvider.getCredentialFromResponse({ 'oauthIdToken': 'exampleIdToken', - 'providerId': 'example.com' + 'providerId': 'example.com', + 'signInMethod': 'example.com' }).toPlainObject()); } @@ -491,15 +547,21 @@ function testFacebookAuthCredential() { assertEquals( fireauth.idp.ProviderId.FACEBOOK, fireauth.FacebookAuthProvider['PROVIDER_ID']); + assertEquals( + fireauth.idp.SignInMethod.FACEBOOK, + fireauth.FacebookAuthProvider['FACEBOOK_SIGN_IN_METHOD']); var authCredential = fireauth.FacebookAuthProvider.credential( 'facebookAccessToken'); assertEquals('facebookAccessToken', authCredential['accessToken']); assertEquals(fireauth.idp.ProviderId.FACEBOOK, authCredential['providerId']); + assertEquals( + fireauth.idp.SignInMethod.FACEBOOK, authCredential['signInMethod']); authCredential.getIdTokenProvider(rpcHandler); assertObjectEquals( { 'oauthAccessToken': 'facebookAccessToken', - 'providerId': fireauth.idp.ProviderId.FACEBOOK + 'providerId': fireauth.idp.ProviderId.FACEBOOK, + 'signInMethod': fireauth.idp.SignInMethod.FACEBOOK }, authCredential.toPlainObject()); assertRpcHandlerVerifyAssertion({ @@ -614,10 +676,13 @@ function testFacebookAuthCredential_alternateConstructor() { {'accessToken': 'facebookAccessToken'}); assertEquals('facebookAccessToken', authCredential['accessToken']); assertEquals(fireauth.idp.ProviderId.FACEBOOK, authCredential['providerId']); + assertEquals( + fireauth.idp.SignInMethod.FACEBOOK, authCredential['signInMethod']); assertObjectEquals( { 'oauthAccessToken': 'facebookAccessToken', - 'providerId': fireauth.idp.ProviderId.FACEBOOK + 'providerId': fireauth.idp.ProviderId.FACEBOOK, + 'signInMethod': fireauth.idp.SignInMethod.FACEBOOK }, authCredential.toPlainObject()); @@ -667,11 +732,14 @@ function testFacebookAuthCredential_nonHttp() { 'facebookAccessToken'); assertEquals('facebookAccessToken', authCredential['accessToken']); assertEquals(fireauth.idp.ProviderId.FACEBOOK, authCredential['providerId']); + assertEquals( + fireauth.idp.SignInMethod.FACEBOOK, authCredential['signInMethod']); authCredential.getIdTokenProvider(rpcHandler); assertObjectEquals( { 'oauthAccessToken': 'facebookAccessToken', - 'providerId': fireauth.idp.ProviderId.FACEBOOK + 'providerId': fireauth.idp.ProviderId.FACEBOOK, + 'signInMethod': fireauth.idp.SignInMethod.FACEBOOK }, authCredential.toPlainObject()); // http://localhost should be used instead of the real current URL. @@ -691,15 +759,21 @@ function testGithubAuthCredential() { assertEquals( fireauth.idp.ProviderId.GITHUB, fireauth.GithubAuthProvider['PROVIDER_ID']); + assertEquals( + fireauth.idp.SignInMethod.GITHUB, + fireauth.GithubAuthProvider['GITHUB_SIGN_IN_METHOD']); var authCredential = fireauth.GithubAuthProvider.credential( 'githubAccessToken'); assertEquals('githubAccessToken', authCredential['accessToken']); assertEquals(fireauth.idp.ProviderId.GITHUB, authCredential['providerId']); + assertEquals( + fireauth.idp.SignInMethod.GITHUB, authCredential['signInMethod']); authCredential.getIdTokenProvider(rpcHandler); assertObjectEquals( { 'oauthAccessToken': 'githubAccessToken', - 'providerId': fireauth.idp.ProviderId.GITHUB + 'providerId': fireauth.idp.ProviderId.GITHUB, + 'signInMethod':fireauth.idp.SignInMethod.GITHUB }, authCredential.toPlainObject()); assertRpcHandlerVerifyAssertion({ @@ -805,7 +879,8 @@ function testGithubAuthCredential_alternateConstructor() { assertObjectEquals( { 'oauthAccessToken': 'githubAccessToken', - 'providerId': fireauth.idp.ProviderId.GITHUB + 'providerId': fireauth.idp.ProviderId.GITHUB, + 'signInMethod':fireauth.idp.SignInMethod.GITHUB }, authCredential.toPlainObject()); @@ -873,17 +948,23 @@ function testGoogleAuthCredential() { assertEquals( fireauth.idp.ProviderId.GOOGLE, fireauth.GoogleAuthProvider['PROVIDER_ID']); + assertEquals( + fireauth.idp.SignInMethod.GOOGLE, + fireauth.GoogleAuthProvider['GOOGLE_SIGN_IN_METHOD']); var authCredential = fireauth.GoogleAuthProvider.credential( 'googleIdToken', 'googleAccessToken'); assertEquals('googleIdToken', authCredential['idToken']); assertEquals('googleAccessToken', authCredential['accessToken']); assertEquals(fireauth.idp.ProviderId.GOOGLE, authCredential['providerId']); + assertEquals( + fireauth.idp.SignInMethod.GOOGLE, authCredential['signInMethod']); authCredential.getIdTokenProvider(rpcHandler); assertObjectEquals( { 'oauthAccessToken': 'googleAccessToken', 'oauthIdToken': 'googleIdToken', - 'providerId': fireauth.idp.ProviderId.GOOGLE + 'providerId': fireauth.idp.ProviderId.GOOGLE, + 'signInMethod': fireauth.idp.SignInMethod.GOOGLE }, authCredential.toPlainObject()); assertRpcHandlerVerifyAssertion({ @@ -1059,31 +1140,39 @@ function testGoogleAuthCredential_alternateConstructor() { {'idToken': 'googleIdToken'}); assertEquals('googleIdToken', authCredentialIdToken['idToken']); assertUndefined(authCredentialIdToken['accessToken']); - assertObjectEquals({ - 'oauthIdToken': 'googleIdToken', - 'providerId': fireauth.idp.ProviderId.GOOGLE - }, authCredentialIdToken.toPlainObject()); + assertObjectEquals( + { + 'oauthIdToken': 'googleIdToken', + 'providerId': fireauth.idp.ProviderId.GOOGLE, + 'signInMethod': fireauth.idp.SignInMethod.GOOGLE + }, + authCredentialIdToken.toPlainObject()); // Only access token. var authCredentialAccessToken = fireauth.GoogleAuthProvider.credential( {'accessToken': 'googleAccessToken'}); assertEquals('googleAccessToken', authCredentialAccessToken['accessToken']); assertUndefined(authCredentialAccessToken['idToken']); - assertObjectEquals({ - 'oauthAccessToken': 'googleAccessToken', - 'providerId': fireauth.idp.ProviderId.GOOGLE - }, authCredentialAccessToken.toPlainObject()); + assertObjectEquals( + { + 'oauthIdToken': 'googleIdToken', + 'providerId': fireauth.idp.ProviderId.GOOGLE, + 'signInMethod': fireauth.idp.SignInMethod.GOOGLE + }, + authCredentialIdToken.toPlainObject()); // Both tokens. var authCredentialBoth = fireauth.GoogleAuthProvider.credential( {'idToken': 'googleIdToken', 'accessToken': 'googleAccessToken'}); assertEquals('googleAccessToken', authCredentialBoth['accessToken']); assertEquals('googleIdToken', authCredentialBoth['idToken']); - assertObjectEquals({ - 'oauthAccessToken': 'googleAccessToken', - 'oauthIdToken': 'googleIdToken', - 'providerId': fireauth.idp.ProviderId.GOOGLE - }, authCredentialBoth.toPlainObject()); + assertObjectEquals( + { + 'oauthIdToken': 'googleIdToken', + 'providerId': fireauth.idp.ProviderId.GOOGLE, + 'signInMethod': fireauth.idp.SignInMethod.GOOGLE + }, + authCredentialIdToken.toPlainObject()); // Neither token. var expectedError = new fireauth.AuthError( @@ -1133,16 +1222,22 @@ function testTwitterAuthCredential() { assertEquals( fireauth.idp.ProviderId.TWITTER, fireauth.TwitterAuthProvider['PROVIDER_ID']); + assertEquals( + fireauth.idp.SignInMethod.TWITTER, + fireauth.TwitterAuthProvider['TWITTER_SIGN_IN_METHOD']); var authCredential = fireauth.TwitterAuthProvider.credential( 'twitterOauthToken', 'twitterOauthTokenSecret'); assertEquals('twitterOauthToken', authCredential['accessToken']); assertEquals('twitterOauthTokenSecret', authCredential['secret']); assertEquals(fireauth.idp.ProviderId.TWITTER, authCredential['providerId']); + assertEquals( + fireauth.idp.SignInMethod.TWITTER, authCredential['signInMethod']); assertObjectEquals( { 'oauthAccessToken': 'twitterOauthToken', 'oauthTokenSecret': 'twitterOauthTokenSecret', - 'providerId': fireauth.idp.ProviderId.TWITTER + 'providerId': fireauth.idp.ProviderId.TWITTER, + 'signInMethod': fireauth.idp.SignInMethod.TWITTER }, authCredential.toPlainObject()); authCredential.getIdTokenProvider(rpcHandler); @@ -1275,12 +1370,14 @@ function testTwitterAuthCredential_alternateConstructor() { assertEquals('twitterOauthToken', authCredential['accessToken']); assertEquals('twitterOauthTokenSecret', authCredential['secret']); assertEquals(fireauth.idp.ProviderId.TWITTER, authCredential['providerId']); - assertObjectEquals({ - 'oauthAccessToken': 'twitterOauthToken', - 'oauthTokenSecret': 'twitterOauthTokenSecret', - 'providerId': fireauth.idp.ProviderId.TWITTER - }, - authCredential.toPlainObject()); + assertObjectEquals( + { + 'oauthAccessToken': 'twitterOauthToken', + 'oauthTokenSecret': 'twitterOauthTokenSecret', + 'providerId': fireauth.idp.ProviderId.TWITTER, + 'signInMethod':fireauth.idp.SignInMethod.TWITTER + }, + authCredential.toPlainObject()); // Missing token or secret should be an error. var expectedError = new fireauth.AuthError( @@ -1308,12 +1405,16 @@ function testEmailAuthCredential() { assertEquals( fireauth.idp.ProviderId.PASSWORD, fireauth.EmailAuthProvider['PROVIDER_ID']); + assertEquals( + fireauth.idp.SignInMethod.EMAIL_PASSWORD, + fireauth.EmailAuthProvider['EMAIL_PASSWORD_SIGN_IN_METHOD']); var authCredential = fireauth.EmailAuthProvider.credential( 'user@example.com', 'password'); assertObjectEquals( { 'email': 'user@example.com', - 'password': 'password' + 'password': 'password', + 'signInMethod': 'password' }, authCredential.toPlainObject()); assertEquals(fireauth.idp.ProviderId.PASSWORD, authCredential['providerId']); @@ -1341,6 +1442,15 @@ function testEmailAuthCredential_linkToIdToken() { } +function testEmailAuthCredentialWithEmailLink_linkToIdToken() { + var authCredential = fireauth.EmailAuthProvider.credentialWithLink( + 'user@example.com', 'https://www.example.com?mode=signIn&oobCode=code'); + authCredential.linkToIdToken(rpcHandler, 'myIdToken'); + assertRpcHandlerEmailLinkSignInForLinking( + 'myIdToken', 'user@example.com', 'code'); +} + + function testEmailAuthCredential_matchIdTokenWithUid() { // Mock idToken parsing. initializeIdTokenMocks('ID_TOKEN', '1234'); @@ -1352,6 +1462,82 @@ function testEmailAuthCredential_matchIdTokenWithUid() { } +function testEmailAuthCredentialWithEmailLink_matchIdTokenWithUid() { + // Mock idToken parsing. + initializeIdTokenMocks('ID_TOKEN', '1234'); + var authCredential = fireauth.EmailAuthProvider.credentialWithLink( + 'user@example.com', 'https://www.example.com?mode=signIn&oobCode=code'); + var p = authCredential.matchIdTokenWithUid(rpcHandler, '1234'); + assertRpcHandlerEmailLinkSignIn('user@example.com', 'code'); + return p; +} + + +/** + * Test Email Link Auth credential. + */ +function testEmailAuthCredentialWithLink() { + assertEquals( + fireauth.idp.ProviderId.PASSWORD, + fireauth.EmailAuthProvider['PROVIDER_ID']); + assertEquals( + fireauth.idp.SignInMethod.EMAIL_LINK, + fireauth.EmailAuthProvider['EMAIL_LINK_SIGN_IN_METHOD']); + var authCredential = fireauth.EmailAuthProvider.credentialWithLink( + 'user@example.com', 'https://www.example.com?mode=signIn&oobCode=code'); + assertObjectEquals( + { + 'email': 'user@example.com', + 'password': 'code', + 'signInMethod': 'emailLink' + }, + authCredential.toPlainObject()); + assertEquals(fireauth.idp.ProviderId.PASSWORD, authCredential['providerId']); + assertEquals( + fireauth.idp.SignInMethod.EMAIL_LINK, authCredential['signInMethod']); + authCredential.getIdTokenProvider(rpcHandler); + assertRpcHandlerEmailLinkSignIn('user@example.com', 'code'); + var provider = new fireauth.EmailAuthProvider(); + // Should throw an invalid OAuth provider error. + var error = assertThrows(function() { + fireauth.AuthProvider.checkIfOAuthSupported(provider); + }); + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.INVALID_OAUTH_PROVIDER); + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + assertEquals(fireauth.idp.ProviderId.PASSWORD, provider['providerId']); + assertFalse(provider['isOAuthProvider']); +} + + +function testEmailAuthCredentialWithLink_invalidLink_error() { + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.ARGUMENT_ERROR, 'Invalid email link!'); + var error = assertThrows(function() { + fireauth.EmailAuthProvider.credentialWithLink( + 'user@example.com', 'invalidLink'); + }); + fireauth.common.testHelper.assertErrorEquals(expectedError, error); +} + + +function testEmailAuthProvider_getActionCodeFromSignInEmailLink() { + var emailLink1 = 'https://www.example.com/action?mode=signIn&oobCode=oobCode'; + var emailLink2 = 'https://www.example.com/action?mode=verifyEmail&' + + 'oobCode=oobCode'; + var emailLink3 = 'https://www.example.com/action?mode=signIn'; + var oobCode1 = fireauth.EmailAuthProvider + .getActionCodeFromSignInEmailLink(emailLink1); + assertEquals('oobCode', oobCode1); + var oobCode2 = fireauth.EmailAuthProvider + .getActionCodeFromSignInEmailLink(emailLink2); + assertNull(oobCode2); + var oobCode3 = fireauth.EmailAuthProvider + .getActionCodeFromSignInEmailLink(emailLink3); + assertNull(oobCode3); +} + + function testPhoneAuthProvider() { assertEquals(fireauth.PhoneAuthProvider['PROVIDER_ID'], fireauth.idp.ProviderId.PHONE); @@ -1632,6 +1818,10 @@ function testPhoneAuthCredential() { var credential = fireauth.PhoneAuthProvider.credential( verificationId, verificationCode); assertEquals(fireauth.idp.ProviderId.PHONE, credential['providerId']); + assertEquals(fireauth.idp.SignInMethod.PHONE, credential['signInMethod']); + assertEquals( + fireauth.idp.SignInMethod.PHONE, + fireauth.PhoneAuthProvider['PHONE_SIGN_IN_METHOD']); assertObjectEquals({ 'providerId': fireauth.idp.ProviderId.PHONE, 'verificationId': verificationId, @@ -1819,6 +2009,10 @@ function testPhoneAuthCredential_temporaryProof() { }); assertEquals(fireauth.idp.ProviderId.PHONE, credential['providerId']); + assertEquals(fireauth.idp.SignInMethod.PHONE, credential['signInMethod']); + assertEquals( + fireauth.idp.SignInMethod.PHONE, + fireauth.PhoneAuthProvider['PHONE_SIGN_IN_METHOD']); assertObjectEquals({ 'providerId': fireauth.idp.ProviderId.PHONE, 'temporaryProof': temporaryProof, diff --git a/packages/auth/test/error_test.js b/packages/auth/test/error_test.js index 0a901b27a91..eca90841c33 100644 --- a/packages/auth/test/error_test.js +++ b/packages/auth/test/error_test.js @@ -92,13 +92,16 @@ function testAuthErrorWithCredential() { assertEquals( 'Account already exists, please confirm and link.', error['message']); // Test toJSON(). - assertObjectEquals({ - code: error['code'], - message: error['message'], - email: 'user@example.com', - providerId: 'facebook.com', - oauthAccessToken: 'ACCESS_TOKEN' - }, error.toJSON()); + assertObjectEquals( + { + code: error['code'], + message: error['message'], + email: 'user@example.com', + providerId: 'facebook.com', + oauthAccessToken: 'ACCESS_TOKEN', + signInMethod: fireauth.FacebookAuthProvider['FACEBOOK_SIGN_IN_METHOD'] + }, + error.toJSON()); assertEquals(JSON.stringify(error), JSON.stringify(error.toJSON())); } @@ -202,7 +205,8 @@ function testAuthErrorWithCredential_toPlainObject() { 'email': 'user@example.com', 'message': 'Account already exists, please confirm and link.', 'providerId': 'facebook.com', - 'oauthAccessToken': 'ACCESS_TOKEN' + 'oauthAccessToken': 'ACCESS_TOKEN', + 'signInMethod': fireauth.FacebookAuthProvider['FACEBOOK_SIGN_IN_METHOD'] }; assertObjectEquals( errorObject, @@ -240,7 +244,8 @@ function testAuthErrorWithCredential_toPlainObject() { 'email': 'user@example.com', 'message': 'The email address is already in use by another account.', 'providerId': 'google.com', - 'oauthIdToken': 'ID_TOKEN' + 'oauthIdToken': 'ID_TOKEN', + 'signInMethod': fireauth.GoogleAuthProvider['GOOGLE_SIGN_IN_METHOD'] }; assertObjectEquals( errorObject3, diff --git a/packages/auth/test/rpchandler_test.js b/packages/auth/test/rpchandler_test.js index 61683157519..aaf8f7dbc16 100644 --- a/packages/auth/test/rpchandler_test.js +++ b/packages/auth/test/rpchandler_test.js @@ -1641,6 +1641,132 @@ function testIsOAuthClientIdValid_error() { } +function testFetchSignInMethodsForIdentifier() { + var expectedResponse = ['google.com', 'emailLink']; + var serverResponse = { + 'kind': 'identitytoolkit#CreateAuthUriResponse', + 'allProviders': [ + 'google.com', + "password" + ], + 'signinMethods': [ + 'google.com', + 'emailLink' + ], + 'registered': true, + 'sessionId': 'AXT8iKR2x89y2o7zRnroApio_uo' + }; + var identifier = 'user@example.com'; + + asyncTestCase.waitForSignals(1); + var request = {'identifier': identifier, 'continueUri': CURRENT_URL}; + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/' + + 'createAuthUri?key=apiKey', + 'POST', + goog.json.serialize(request), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + serverResponse); + rpcHandler.fetchSignInMethodsForIdentifier(identifier) + .then(function(response) { + assertArrayEquals(expectedResponse, response); + asyncTestCase.signal(); + }); +} + + +function testFetchSignInMethodsForIdentifier_noSignInMethodsReturned() { + var expectedResponse = []; + var serverResponse = { + 'kind': 'identitytoolkit#CreateAuthUriResponse', + 'registered': true, + 'sessionId': 'AXT8iKR2x89y2o7zRnroApio_uo' + }; + var identifier = 'user@example.com'; + + asyncTestCase.waitForSignals(1); + var request = {'identifier': identifier, 'continueUri': CURRENT_URL}; + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/' + + 'createAuthUri?key=apiKey', + 'POST', + goog.json.serialize(request), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + serverResponse); + rpcHandler.fetchSignInMethodsForIdentifier(identifier) + .then(function(response) { + assertArrayEquals(expectedResponse, response); + asyncTestCase.signal(); + }); +} + + +function testFetchSignInMethodsForIdentifier_nonHttpOrHttps() { + // Simulate non http or https current URL. + stubs.replace(fireauth.util, 'getCurrentUrl', function() { + return 'chrome-extension://234567890/index.html'; + }); + stubs.replace(fireauth.util, 'getCurrentScheme', function() { + return 'chrome-extension:'; + }); + var expectedResponse = ['google.com', 'emailLink']; + var serverResponse = { + 'kind': 'identitytoolkit#CreateAuthUriResponse', + 'allProviders': [ + 'google.com', + 'password' + ], + 'signinMethods': [ + 'google.com', + 'emailLink' + ], + 'registered': true, + 'sessionId': 'AXT8iKR2x89y2o7zRnroApio_uo' + }; + var identifier = 'user@example.com'; + + asyncTestCase.waitForSignals(1); + var request = { + 'identifier': identifier, + // A fallback HTTP URL should be used. + 'continueUri': 'http://localhost' + }; + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/' + + 'createAuthUri?key=apiKey', + 'POST', + goog.json.serialize(request), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + serverResponse); + rpcHandler.fetchSignInMethodsForIdentifier(identifier) + .then(function(response) { + assertArrayEquals(expectedResponse, response); + asyncTestCase.signal(); + }); +} + + +function testFetchSignInMethodsForIdentifier_serverCaughtError() { + var identifier = 'user@example.com'; + var requestBody = {'identifier': identifier, 'continueUri': CURRENT_URL}; + var expectedUrl = 'https://www.googleapis.com/identitytoolkit/v3/' + + 'relyingparty/createAuthUri?key=apiKey'; + + var errorMap = {}; + errorMap[fireauth.RpcHandler.ServerError.INVALID_IDENTIFIER] = + fireauth.authenum.Error.INVALID_EMAIL; + errorMap[fireauth.RpcHandler.ServerError.MISSING_CONTINUE_URI] = + fireauth.authenum.Error.INTERNAL_ERROR; + + assertServerErrorsAreHandled(function() { + return rpcHandler.fetchSignInMethodsForIdentifier(identifier); + }, errorMap, expectedUrl, requestBody); +} + + function testFetchProvidersForIdentifier() { var expectedResponse = [ 'google.com', @@ -2088,6 +2214,107 @@ function testVerifyCustomToken_unknownServerResponse() { } +function testEmailLinkSignIn_success() { + var expectedResponse = {'idToken': 'ID_TOKEN'}; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/emailLinkSi' + + 'gnin?key=apiKey', + 'POST', + goog.json.serialize({ + 'email': 'user@example.com', + 'oobCode': 'OTP_CODE', + 'returnSecureToken': true + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.emailLinkSignIn('user@example.com', 'OTP_CODE') + .then(function(response) { + assertObjectEquals(expectedResponse, response); + asyncTestCase.signal(); + }); +} + + +function testEmailLinkSignIn_serverCaughtError() { + var expectedUrl = 'https://www.googleapis.com/identitytoolkit/v3/' + + 'relyingparty/emailLinkSignin?key=apiKey'; + var email = 'user@example.com'; + var oobCode = 'OTP_CODE'; + var requestBody = { + 'email': email, + 'oobCode': oobCode, + 'returnSecureToken': true + }; + var errorMap = {}; + errorMap[fireauth.RpcHandler.ServerError.INVALID_EMAIL] = + fireauth.authenum.Error.INVALID_EMAIL; + errorMap[fireauth.RpcHandler.ServerError.TOO_MANY_ATTEMPTS_TRY_LATER] = + fireauth.authenum.Error.TOO_MANY_ATTEMPTS_TRY_LATER; + errorMap[fireauth.RpcHandler.ServerError.USER_DISABLED] = + fireauth.authenum.Error.USER_DISABLED; + + assertServerErrorsAreHandled(function() { + return rpcHandler.emailLinkSignIn(email, oobCode); + }, errorMap, expectedUrl, requestBody); +} + + +/** + * Tests invalid server response emailLinkSignIn error. + */ +function testEmailLinkSignIn_unknownServerResponse() { + // Test when server returns unexpected response with no error message. + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/emailLinkSi' + + 'gnin?key=apiKey', + 'POST', + goog.json.serialize({ + 'email': 'user@example.com', + 'oobCode': 'OTP_CODE', + 'returnSecureToken': true + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + {}); + rpcHandler.emailLinkSignIn('user@example.com', 'OTP_CODE') + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR), + error); + asyncTestCase.signal(); + }); +} + + +function testEmailLinkSignIn_emptyActionCodeError() { + // Test when empty action code is passed in emailLinkSignIn request. + asyncTestCase.waitForSignals(1); + // Test when request is invalid. + rpcHandler.emailLinkSignIn('user@example.com', '').thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR), error); + asyncTestCase.signal(); + }); +} + + +function testEmailLinkSignIn_invalidEmailError() { + // Test when invalid email is passed in emailLinkSignIn request. + asyncTestCase.waitForSignals(1); + // Test when request is invalid. + rpcHandler.emailLinkSignIn('user@invalid', 'OTP_CODE') + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INVALID_EMAIL), + error); + asyncTestCase.signal(); + }); +} + + function testVerifyPassword_success() { var expectedResponse = { 'idToken': 'ID_TOKEN' @@ -3093,6 +3320,189 @@ function testVerifyAssertionForExisting_serverCaughtError() { } +/** + * Tests successful sendSignInLinkToEmail RPC call with action code settings. + */ +function testSendSignInLinkToEmail_success_actionCodeSettings() { + var userEmail = 'user@example.com'; + var additionalRequestData = { + 'continueUrl': 'https://www.example.com/?state=abc', + 'iOSBundleId': 'com.example.ios', + 'androidPackageName': 'com.example.android', + 'androidInstallApp': true, + 'androidMinimumVersion': '12', + 'canHandleCodeInApp': true + }; + var expectedResponse = {'email': userEmail}; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/getOobCon' + + 'firmationCode?key=apiKey', + 'POST', + goog.json.serialize({ + 'requestType': fireauth.RpcHandler.GetOobCodeRequestType.EMAIL_SIGNIN, + 'email': userEmail, + 'continueUrl': 'https://www.example.com/?state=abc', + 'iOSBundleId': 'com.example.ios', + 'androidPackageName': 'com.example.android', + 'androidInstallApp': true, + 'androidMinimumVersion': '12', + 'canHandleCodeInApp': true + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.sendSignInLinkToEmail('user@example.com', additionalRequestData) + .then(function(email) { + assertEquals(userEmail, email); + asyncTestCase.signal(); + }); +} + + +/** + * Tests successful sendSignInLinkToEmail RPC call with custom locale. + */ +function testSendSignInLinkToEmail_success_customLocale() { + var userEmail = 'user@example.com'; + var additionalRequestData = { + 'continueUrl': 'https://www.example.com/?state=abc', + 'iOSBundleId': 'com.example.ios', + 'androidPackageName': 'com.example.android', + 'androidInstallApp': true, + 'androidMinimumVersion': '12', + 'canHandleCodeInApp': true + }; + var expectedResponse = {'email': userEmail}; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/getOobCon' + + 'firmationCode?key=apiKey', + 'POST', + goog.json.serialize({ + 'requestType': fireauth.RpcHandler.GetOobCodeRequestType.EMAIL_SIGNIN, + 'email': userEmail, + 'continueUrl': 'https://www.example.com/?state=abc', + 'iOSBundleId': 'com.example.ios', + 'androidPackageName': 'com.example.android', + 'androidInstallApp': true, + 'androidMinimumVersion': '12', + 'canHandleCodeInApp': true + }), + {'Content-Type': 'application/json', 'X-Firebase-Locale': 'es'}, + delay, + expectedResponse); + rpcHandler.updateCustomLocaleHeader('es'); + rpcHandler.sendSignInLinkToEmail('user@example.com', additionalRequestData) + .then(function(email) { + assertEquals(userEmail, email); + asyncTestCase.signal(); + }); +} + + +/** + * Tests invalid email sendSignInLinkToEmail error. + */ +function testSendSignInLinkToEmail_invalidEmailError() { + // Test when invalid email is passed in getOobCode request. + var additionalRequestData = { + 'continueUrl': 'https://www.example.com/?state=abc', + 'iOSBundleId': 'com.example.ios', + 'androidPackageName': 'com.example.android', + 'androidInstallApp': true, + 'androidMinimumVersion': '12', + 'canHandleCodeInApp': true + }; + asyncTestCase.waitForSignals(1); + // Test when request is invalid. + rpcHandler.sendSignInLinkToEmail('user@invalid', additionalRequestData) + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INVALID_EMAIL), + error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests invalid response sendSignInLinkToEmail error. + */ +function testSendSignInLinkToEmail_unknownServerResponse() { + var userEmail = 'user@example.com'; + var additionalRequestData = { + 'continueUrl': 'https://www.example.com/?state=abc', + 'iOSBundleId': 'com.example.ios', + 'androidPackageName': 'com.example.android', + 'androidInstallApp': true, + 'androidMinimumVersion': '12', + 'canHandleCodeInApp': true + }; + var expectedResponse = {}; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/getOobCon' + + 'firmationCode?key=apiKey', + 'POST', + goog.json.serialize({ + 'requestType': fireauth.RpcHandler.GetOobCodeRequestType.EMAIL_SIGNIN, + 'email': userEmail, + 'continueUrl': 'https://www.example.com/?state=abc', + 'iOSBundleId': 'com.example.ios', + 'androidPackageName': 'com.example.android', + 'androidInstallApp': true, + 'androidMinimumVersion': '12', + 'canHandleCodeInApp': true + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.sendSignInLinkToEmail(userEmail, additionalRequestData) + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR), + error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests server side sendSignInLinkToEmail error. + */ +function testSendSignInLinkToEmail_serverCaughtError() { + var userEmail = 'user@example.com'; + var expectedUrl = 'https://www.googleapis.com/identitytoolkit/v3/relyin' + + 'gparty/getOobConfirmationCode?key=apiKey'; + var requestBody = { + 'requestType': fireauth.RpcHandler.GetOobCodeRequestType.EMAIL_SIGNIN, + 'email': userEmail + }; + var errorMap = {}; + errorMap[fireauth.RpcHandler.ServerError.INVALID_RECIPIENT_EMAIL] = + fireauth.authenum.Error.INVALID_RECIPIENT_EMAIL; + errorMap[fireauth.RpcHandler.ServerError.INVALID_SENDER] = + fireauth.authenum.Error.INVALID_SENDER; + errorMap[fireauth.RpcHandler.ServerError.INVALID_MESSAGE_PAYLOAD] = + fireauth.authenum.Error.INVALID_MESSAGE_PAYLOAD; + + // Action code settings related errors. + errorMap[fireauth.RpcHandler.ServerError.INVALID_CONTINUE_URI] = + fireauth.authenum.Error.INVALID_CONTINUE_URI; + errorMap[fireauth.RpcHandler.ServerError.MISSING_ANDROID_PACKAGE_NAME] = + fireauth.authenum.Error.MISSING_ANDROID_PACKAGE_NAME; + errorMap[fireauth.RpcHandler.ServerError.MISSING_IOS_BUNDLE_ID] = + fireauth.authenum.Error.MISSING_IOS_BUNDLE_ID; + errorMap[fireauth.RpcHandler.ServerError.UNAUTHORIZED_DOMAIN] = + fireauth.authenum.Error.UNAUTHORIZED_DOMAIN; + + assertServerErrorsAreHandled(function() { + return rpcHandler.sendSignInLinkToEmail(userEmail, {}); + }, errorMap, expectedUrl, requestBody); +} + + /** * Tests successful sendPasswordResetEmail RPC call with action code settings. */ @@ -4151,6 +4561,131 @@ function testUpdateEmailAndPassword_noPassword() { } +function testEmailLinkSignInForLinking_success() { + var expectedResponse = {'idToken': 'ID_TOKEN'}; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/emailLinkSi' + + 'gnin?key=apiKey', + 'POST', + goog.json.serialize({ + 'idToken': 'ID_TOKEN', + 'email': 'user@example.com', + 'oobCode': 'OTP_CODE', + 'returnSecureToken': true + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.emailLinkSignInForLinking( + 'ID_TOKEN', 'user@example.com', 'OTP_CODE') + .then(function(response) { + assertEquals('ID_TOKEN', response['idToken']); + asyncTestCase.signal(); + }); +} + + +function testEmailLinkSignInForLinking_serverCaughtError() { + var expectedUrl = 'https://www.googleapis.com/identitytoolkit/v3/' + + 'relyingparty/emailLinkSignin?key=apiKey'; + var email = 'user@example.com'; + var oobCode = 'OTP_CODE'; + var id_token = 'ID_TOKEN'; + var requestBody = { + 'idToken': 'ID_TOKEN', + 'email': email, + 'oobCode': oobCode, + 'returnSecureToken': true + }; + var errorMap = {}; + errorMap[fireauth.RpcHandler.ServerError.INVALID_EMAIL] = + fireauth.authenum.Error.INVALID_EMAIL; + errorMap[fireauth.RpcHandler.ServerError.TOO_MANY_ATTEMPTS_TRY_LATER] = + fireauth.authenum.Error.TOO_MANY_ATTEMPTS_TRY_LATER; + errorMap[fireauth.RpcHandler.ServerError.USER_DISABLED] = + fireauth.authenum.Error.USER_DISABLED; + + assertServerErrorsAreHandled(function() { + return rpcHandler.emailLinkSignInForLinking(id_token, email, oobCode); + }, errorMap, expectedUrl, requestBody); +} + + +/** + * Tests invalid server response emailLinkSignInForLinking error. + */ +function testEmailLinkSignInForLinking_unknownServerResponse() { + // Test when server returns unexpected response with no error message. + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/emailLinkSi' + + 'gnin?key=apiKey', + 'POST', + goog.json.serialize({ + 'idToken': 'ID_TOKEN', + 'email': 'user@example.com', + 'oobCode': 'OTP_CODE', + 'returnSecureToken': true + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + {}); + rpcHandler.emailLinkSignInForLinking( + 'ID_TOKEN', 'user@example.com', 'OTP_CODE') + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR), + error); + asyncTestCase.signal(); + }); +} + + +function testEmailLinkSignInForLinking_emptyActionCodeError() { + // Test when empty action code is passed in emailLinkSignInForLinking request. + asyncTestCase.waitForSignals(1); + // Test when request is invalid. + rpcHandler.emailLinkSignInForLinking('ID_TOKEN', 'user@example.com', '') + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR), + error); + asyncTestCase.signal(); + }); +} + + +function testEmailLinkSignInForLinking_invalidEmailError() { + // Test when invalid email is passed in emailLinkSignInForLinking request. + asyncTestCase.waitForSignals(1); + // Test when request is invalid. + rpcHandler.emailLinkSignInForLinking( + 'ID_TOKEN', 'user@invalid', 'OTP_CODE') + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INVALID_EMAIL), + error); + asyncTestCase.signal(); + }); +} + + +function testEmailLinkSignInForLinking_emptyIdTokenError() { + // Test when empty ID token is passed in emailLinkSignInForLinking request. + asyncTestCase.waitForSignals(1); + // Test when request is invalid. + rpcHandler.emailLinkSignInForLinking( + '', 'user@example.com', 'OTP_CODE') + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR), + error); + asyncTestCase.signal(); + }); +} + + function testInvokeRpc() { asyncTestCase.waitForSignals(3); var request = { diff --git a/packages/firebase/externs/firebase-auth-externs.js b/packages/firebase/externs/firebase-auth-externs.js index 4cd37997a16..d6ec6295af0 100644 --- a/packages/firebase/externs/firebase-auth-externs.js +++ b/packages/firebase/externs/firebase-auth-externs.js @@ -75,6 +75,16 @@ firebase.auth.AuthCredential = function() {}; */ firebase.auth.AuthCredential.prototype.providerId; +/** + * The authentication sign in method for the credential. + * For example, 'password', or 'emailLink. This corresponds to the sign-in + * method identifier as returned in + * {@link firebase.auth.Auth#fetchSignInMethodsForEmail}. + * + * @type {string} + */ +firebase.auth.AuthCredential.prototype.signInMethod; + /** * Interface that represents the OAuth credentials returned by an OAuth * provider. Implementations specify the details about each auth provider's @@ -843,6 +853,8 @@ firebase.auth.ActionCodeInfo.prototype.data; * {@link firebase.User#sendEmailVerification}. *
  • `RECOVER_EMAIL`: email change revocation code generated via * {@link firebase.User#updateEmail}.
  • + *
  • `EMAIL_SIGNIN`: email sign in code generated via + * {@link firebase.auth.Auth#sendSignInLinkToEmail}.
  • *
* * @type {string} @@ -1165,6 +1177,31 @@ firebase.auth.Auth.prototype.createUserWithEmailAndPassword = function( */ firebase.auth.Auth.prototype.fetchProvidersForEmail = function(email) {}; +/** + * Gets the list of possible sign in methods for the given email address. This + * is useful to differentiate methods of sign-in for the same provider, + * eg. `EmailAuthProvider` which has 2 methods of sign-in, email/password and + * email/link. + * + *

Error Codes

+ *
+ *
auth/invalid-email
+ *
Thrown if the email address is not valid.
+ *
+ * + * @param {string} email An email address. + * @return {!firebase.Promise>} + */ +firebase.auth.Auth.prototype.fetchSignInMethodsForEmail = function(email) {}; + +/** + * Checks if an incoming link is a sign-in with email link. + * + * @param {string} emailLink Sign-in email link. + * @return {boolean} Whether the link is a sign-in with email link. + */ +firebase.auth.Auth.prototype.isSignInWithEmailLink = function(emailLink) {}; + /** * Adds an observer for changes to the user's sign-in state. * @@ -1222,6 +1259,82 @@ firebase.auth.Auth.prototype.onIdTokenChanged = function( completed ) {}; +/** + * Sends a sign-in email link to the user with the specified email. + * + * The sign-in operation has to always be completed in the app unlike other out + * of band email actions (password reset and email verifications). This is + * because, at the end of the flow, the user is expected to be signed in and + * their Auth state persisted within the app. + * + * To complete sign in with the email link, call + * {@link firebase.auth.Auth#signInWithEmailLink} with the email address and + * the email link supplied in the email sent to the user. + * + *

Error Codes

+ *
+ *
auth/argument-error
+ *
Thrown if handleCodeInApp is false.
+ *
auth/invalid-email
+ *
Thrown if the email address is not valid.
+ *
auth/missing-android-pkg-name
+ *
An Android package name must be provided if the Android app is required + * to be installed.
+ *
auth/missing-continue-uri
+ *
A continue URL must be provided in the request.
+ *
auth/missing-ios-bundle-id
+ *
An iOS Bundle ID must be provided if an App Store ID is provided.
+ *
auth/invalid-continue-uri
+ *
The continue URL provided in the request is invalid.
+ *
auth/unauthorized-continue-uri
+ *
The domain of the continue URL is not whitelisted. Whitelist + * the domain in the Firebase console.
+ *
+ * + * @example + * var actionCodeSettings = { + * // The URL to redirect to for sign-in completion. This is also the deep + * // link for mobile redirects. The domain (www.example.com) for this URL + * // must be whitelisted in the Firebase Console. + * url: 'https://www.example.com/finishSignUp?cartId=1234', + * iOS: { + * bundleId: 'com.example.ios' + * }, + * android: { + * packageName: 'com.example.android', + * installApp: true, + * minimumVersion: '12' + * }, + * // This must be true. + * handleCodeInApp: true + * }; + * firebase.auth().sendSignInLinkToEmail('user@example.com', actionCodeSettings) + * .then(function() { + * // The link was successfully sent. Inform the user. Save the email + * // locally so you don't need to ask the user for it again if they open + * // the link on the same device. + * }) + * .catch(function(error) { + * // Some error occurred, you can inspect the code: error.code + * }); + * + * @param {string} email The email account to sign in with. + * @param {!firebase.auth.ActionCodeSettings} actionCodeSettings The action + * code settings. The action code settings which provides Firebase with + * instructions on how to construct the email link. This includes the + * sign in completion URL or the deep link for mobile redirects, the mobile + * apps to use when the sign-in link is opened on an Android or iOS device. + * Mobile app redirects will only be applicable if the developer configures + * and accepts the Firebase Dynamic Links terms of condition. + * The Android package name and iOS bundle ID will be respected only if they + * are configured in the same Firebase Auth project used. + * @return {!firebase.Promise} + */ +firebase.auth.Auth.prototype.sendSignInLinkToEmail = function( + email, + actionCodeSettings +) {}; + /** * Sends a password reset email to the given email address. * @@ -1623,6 +1736,45 @@ firebase.auth.Auth.prototype.signInWithEmailAndPassword = function( password ) {}; +/** + * Asynchronously signs in using an email and sign-in email link. If no link + * is passed, the link is inferred from the current URL. + * + * Fails with an error if the email address is invalid or OTP in email link + * expires. + * + * Note: Confirm the link is a sign-in email link before calling this method + * {@link firebase.auth.Auth#isSignInWithEmailLink}. + * + *

Error Codes

+ *
+ *
auth/expired-action-code
+ *
Thrown if OTP in email link expires.
+ *
auth/invalid-email
+ *
Thrown if the email address is not valid.
+ *
auth/user-disabled
+ *
Thrown if the user corresponding to the given email has been + * disabled.
+ *
+ * + * @example + * firebase.auth().signInWithEmailLink(email, emailLink) + * .catch(function(error) { + * // Some error occurred, you can inspect the code: error.code + * // Common errors could be invalid email and invalid or expired OTPs. + * }); + * + * @param {string} email The email account to sign in with. + * @param {?string=} emailLink The optional link which contains the OTP needed + * to complete the sign in with email link. If not specified, the current + * URL is used instead. + * @return {!firebase.Promise} + */ +firebase.auth.Auth.prototype.signInWithEmailLink = function( + email, + emailLink +) {}; + /** * Asynchronously signs in using a phone number. This method sends a code via * SMS to the given phone number, and returns a @@ -2025,6 +2177,14 @@ firebase.auth.FacebookAuthProvider = function() {}; /** @type {string} */ firebase.auth.FacebookAuthProvider.PROVIDER_ID; +/** + * This corresponds to the sign-in method identifier as returned in + * {@link firebase.auth.Auth#fetchSignInMethodsForEmail}. + * + * @type {string} + */ +firebase.auth.FacebookAuthProvider.FACEBOOK_SIGN_IN_METHOD; + /** * @example * var cred = firebase.auth.FacebookAuthProvider.credential( @@ -2133,6 +2293,14 @@ firebase.auth.GithubAuthProvider = function() {}; /** @type {string} */ firebase.auth.GithubAuthProvider.PROVIDER_ID; +/** + * This corresponds to the sign-in method identifier as returned in + * {@link firebase.auth.Auth#fetchSignInMethodsForEmail}. + * + * @type {string} + */ +firebase.auth.GithubAuthProvider.GITHUB_SIGN_IN_METHOD; + /** * @example * var cred = firebase.auth.FacebookAuthProvider.credential( @@ -2211,6 +2379,14 @@ firebase.auth.GoogleAuthProvider = function() {}; /** @type {string} */ firebase.auth.GoogleAuthProvider.PROVIDER_ID; +/** + * This corresponds to the sign-in method identifier as returned in + * {@link firebase.auth.Auth#fetchSignInMethodsForEmail}. + * + * @type {string} + */ +firebase.auth.GoogleAuthProvider.GOOGLE_SIGN_IN_METHOD; + /** * Creates a credential for Google. At least one of ID token and access token * is required. @@ -2293,6 +2469,14 @@ firebase.auth.TwitterAuthProvider = function() {}; /** @type {string} */ firebase.auth.TwitterAuthProvider.PROVIDER_ID; +/** + * This corresponds to the sign-in method identifier as returned in + * {@link firebase.auth.Auth#fetchSignInMethodsForEmail}. + * + * @type {string} + */ +firebase.auth.TwitterAuthProvider.TWITTER_SIGN_IN_METHOD; + /** * @param {string} token Twitter access token. * @param {string} secret Twitter secret. @@ -2331,6 +2515,22 @@ firebase.auth.EmailAuthProvider = function() {}; /** @type {string} */ firebase.auth.EmailAuthProvider.PROVIDER_ID; +/** + * This corresponds to the sign-in method identifier as returned in + * {@link firebase.auth.Auth#fetchSignInMethodsForEmail}. + * + * @type {string} + */ +firebase.auth.EmailAuthProvider.EMAIL_PASSWORD_SIGN_IN_METHOD; + +/** + * This corresponds to the sign-in method identifier as returned in + * {@link firebase.auth.Auth#fetchSignInMethodsForEmail}. + * + * @type {string} + */ +firebase.auth.EmailAuthProvider.EMAIL_LINK_SIGN_IN_METHOD; + /** * @example * var cred = firebase.auth.EmailAuthProvider.credential( @@ -2344,6 +2544,25 @@ firebase.auth.EmailAuthProvider.PROVIDER_ID; */ firebase.auth.EmailAuthProvider.credential = function(email, password) {}; +/** + * Initialize an `EmailAuthProvider` credential using an email and an email link + * after a sign in with email link operation. + * + * @example + * var cred = firebase.auth.EmailAuthProvider.credentialWithLink( + * email, + * emailLink + * ); + * + * @param {string} email Email address. + * @param {string} emailLink Sign-in email link. + * @return {!firebase.auth.AuthCredential} The auth provider credential. + */ +firebase.auth.EmailAuthProvider.credentialWithLink = function( + email, + emailLink +) {}; + /** @type {string} */ firebase.auth.EmailAuthProvider.prototype.providerId; @@ -2376,6 +2595,14 @@ firebase.auth.PhoneAuthProvider = function(auth) {}; /** @type {string} */ firebase.auth.PhoneAuthProvider.PROVIDER_ID; +/** + * This corresponds to the sign-in method identifier as returned in + * {@link firebase.auth.Auth#fetchSignInMethodsForEmail}. + * + * @type {string} + */ +firebase.auth.PhoneAuthProvider.PHONE_SIGN_IN_METHOD; + /** * Creates a phone auth credential, given the verification ID from * {@link firebase.auth.PhoneAuthProvider#verifyPhoneNumber} and the code