From 5fad2d95febb798cef317c0297418c7ddae62597 Mon Sep 17 00:00:00 2001 From: Ti Wang Date: Wed, 21 Feb 2018 11:40:42 -0800 Subject: [PATCH 1/5] passwordless signin added new files for passwordless signin added licence exported function --- packages/auth/src/actioncodeinfo.js | 19 +- packages/auth/src/actioncodesettings.js | 16 +- packages/auth/src/actioncodeurl.js | 92 +++ packages/auth/src/auth.js | 75 +++ packages/auth/src/authcredential.js | 100 +++- packages/auth/src/exports_auth.js | 27 + packages/auth/src/idp.js | 16 + packages/auth/src/rpchandler.js | 123 +++- packages/auth/test/actioncodeinfo_test.js | 25 + packages/auth/test/actioncodesettings_test.js | 21 +- packages/auth/test/actioncodeurl_test.js | 66 +++ packages/auth/test/auth_test.js | 306 ++++++++++ packages/auth/test/authcredential_test.js | 262 +++++++-- packages/auth/test/error_test.js | 23 +- packages/auth/test/rpchandler_test.js | 535 ++++++++++++++++++ 15 files changed, 1639 insertions(+), 67 deletions(-) create mode 100644 packages/auth/src/actioncodeurl.js create mode 100644 packages/auth/test/actioncodeurl_test.js 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: * * * @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,80 @@ 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 +1734,43 @@ 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 +2173,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 +2289,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 +2375,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 +2465,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 +2511,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 +2540,23 @@ 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 +2589,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 From a35805568cda1a377acb0700eaeb7de1e0317f5b Mon Sep 17 00:00:00 2001 From: Ti Wang Date: Mon, 12 Mar 2018 17:58:19 -0700 Subject: [PATCH 3/5] [AUTOMATED]: Prettier Code Styling --- .../firebase/externs/firebase-auth-externs.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/firebase/externs/firebase-auth-externs.js b/packages/firebase/externs/firebase-auth-externs.js index f0d75e0ee36..d6ec6295af0 100644 --- a/packages/firebase/externs/firebase-auth-externs.js +++ b/packages/firebase/externs/firebase-auth-externs.js @@ -1330,8 +1330,10 @@ firebase.auth.Auth.prototype.onIdTokenChanged = function( * are configured in the same Firebase Auth project used. * @return {!firebase.Promise} */ -firebase.auth.Auth.prototype.sendSignInLinkToEmail = - function(email, actionCodeSettings) {}; +firebase.auth.Auth.prototype.sendSignInLinkToEmail = function( + email, + actionCodeSettings +) {}; /** * Sends a password reset email to the given email address. @@ -1768,8 +1770,10 @@ firebase.auth.Auth.prototype.signInWithEmailAndPassword = function( * URL is used instead. * @return {!firebase.Promise} */ -firebase.auth.Auth.prototype.signInWithEmailLink = - function(email, emailLink) {}; +firebase.auth.Auth.prototype.signInWithEmailLink = function( + email, + emailLink +) {}; /** * Asynchronously signs in using a phone number. This method sends a code via @@ -2554,8 +2558,10 @@ firebase.auth.EmailAuthProvider.credential = function(email, password) {}; * @param {string} emailLink Sign-in email link. * @return {!firebase.auth.AuthCredential} The auth provider credential. */ -firebase.auth.EmailAuthProvider.credentialWithLink = - function(email, emailLink) {}; +firebase.auth.EmailAuthProvider.credentialWithLink = function( + email, + emailLink +) {}; /** @type {string} */ firebase.auth.EmailAuthProvider.prototype.providerId; From 62210a88f086edb726d3c44e1b5d40fa6aae8f2f Mon Sep 17 00:00:00 2001 From: Ti Wang Date: Wed, 14 Mar 2018 17:09:03 -0700 Subject: [PATCH 4/5] added passwordless sign in in demo and fixed type --- packages/auth-types/index.d.ts | 2 +- packages/auth/demo/public/index.html | 52 ++++++++++ packages/auth/demo/public/script.js | 139 +++++++++++++++++++++++++++ 3 files changed, 192 insertions(+), 1 deletion(-) diff --git a/packages/auth-types/index.d.ts b/packages/auth-types/index.d.ts index d599611ec40..7e085efee5b 100644 --- a/packages/auth-types/index.d.ts +++ b/packages/auth-types/index.d.ts @@ -281,7 +281,7 @@ export class FirebaseAuth { email: string, password: string ): Promise; - signInWithEmailLink(email: string, emailLink: 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..6ee24e87446 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,46 @@ 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', email); + } + 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 +698,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. */ @@ -973,6 +1091,16 @@ function populateActionCodes() { $('#email-verification-code').val(actionCode); } else if (mode == 'resetPassword') { $('#password-reset-code').val(actionCode); + } else if (mode == 'signIn') { + var emailForSignIn = null; + if ('localStorage' in window && window['localStorage'] !== null) { + emailForSignIn = window.localStorage.getItem('emailForSignIn'); + } + if (emailForSignIn) { + $('#sign-in-with-email-link-email').val(emailForSignIn); + $('#sign-in-with-email-link-link').val(window.location.href); + onSignInWithEmailLink(); + } } else { $('#email-verification-code').val(actionCode); $('#password-reset-code').val(actionCode); @@ -1153,11 +1281,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 +1338,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); From a3c6335deedc99a4532d0fe01406241cdb2b88c1 Mon Sep 17 00:00:00 2001 From: Ti Wang Date: Wed, 14 Mar 2018 17:34:35 -0700 Subject: [PATCH 5/5] remove email in localstorage after used for passwordless sign in --- packages/auth/demo/public/script.js | 36 +++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/packages/auth/demo/public/script.js b/packages/auth/demo/public/script.js index 6ee24e87446..c62c2408a92 100644 --- a/packages/auth/demo/public/script.js +++ b/packages/auth/demo/public/script.js @@ -646,7 +646,13 @@ function onSendSignInLinkToEmailCurrentUrl() { auth.sendSignInLinkToEmail(email, actionCodeSettings).then(function() { if ('localStorage' in window && window['localStorage'] !== null) { - window.localStorage.setItem('emailForSignIn', email); + window.localStorage.setItem( + 'emailForSignIn', + // Save the email and the timestamp. + JSON.stringify({ + email: email, + timestamp: new Date().getTime() + })); } alertSuccess('Email sent!'); }, onAuthError); @@ -1084,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'); @@ -1092,14 +1120,12 @@ function populateActionCodes() { } else if (mode == 'resetPassword') { $('#password-reset-code').val(actionCode); } else if (mode == 'signIn') { - var emailForSignIn = null; - if ('localStorage' in window && window['localStorage'] !== null) { - emailForSignIn = window.localStorage.getItem('emailForSignIn'); - } 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);