-
Notifications
You must be signed in to change notification settings - Fork 875
/
auth.js
2099 lines (1928 loc) · 76 KB
/
auth.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/**
* @license
* 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 The headless Auth class used for authenticating Firebase users.
*/
goog.provide('fireauth.Auth');
goog.require('fireauth.ActionCodeInfo');
goog.require('fireauth.ActionCodeSettings');
goog.require('fireauth.AdditionalUserInfo');
goog.require('fireauth.AuthCredential');
goog.require('fireauth.AuthError');
goog.require('fireauth.AuthEvent');
goog.require('fireauth.AuthEventHandler');
goog.require('fireauth.AuthEventManager');
goog.require('fireauth.AuthProvider');
goog.require('fireauth.AuthSettings');
goog.require('fireauth.AuthUser');
goog.require('fireauth.ConfirmationResult');
goog.require('fireauth.EmailAuthProvider');
goog.require('fireauth.MultiFactorError');
goog.require('fireauth.RpcHandler');
goog.require('fireauth.UserEventType');
goog.require('fireauth.authenum.Error');
goog.require('fireauth.constants');
goog.require('fireauth.deprecation');
goog.require('fireauth.idp');
goog.require('fireauth.iframeclient.IfcHandler');
goog.require('fireauth.object');
goog.require('fireauth.storage.RedirectUserManager');
goog.require('fireauth.storage.UserManager');
goog.require('fireauth.util');
goog.require('goog.Promise');
goog.require('goog.array');
goog.require('goog.events');
goog.require('goog.events.Event');
goog.require('goog.events.EventTarget');
goog.require('goog.object');
/**
* Creates the Firebase Auth corresponding for the App provided.
*
* @param {!firebase.app.App} app The corresponding Firebase App.
* @constructor
* @implements {fireauth.AuthEventHandler}
* @implements {firebase.Service}
* @extends {goog.events.EventTarget}
*/
fireauth.Auth = function(app) {
/** @private {boolean} Whether this instance is deleted. */
this.deleted_ = false;
/** The Auth instance's settings object. */
fireauth.object.setReadonlyProperty(
this, 'settings', new fireauth.AuthSettings());
/** Auth's corresponding App. */
fireauth.object.setReadonlyProperty(this, 'app', app);
// Initialize RPC handler.
// API key is required for web client RPC calls.
if (this.app_().options && this.app_().options['apiKey']) {
var clientFullVersion = firebase.SDK_VERSION ?
fireauth.util.getClientVersion(
fireauth.util.ClientImplementation.JSCORE, firebase.SDK_VERSION) :
null;
this.rpcHandler_ = new fireauth.RpcHandler(
this.app_().options && this.app_().options['apiKey'],
// Get the client Auth endpoint used.
fireauth.constants.getEndpointConfig(fireauth.constants.clientEndpoint),
clientFullVersion);
} else {
throw new fireauth.AuthError(fireauth.authenum.Error.INVALID_API_KEY);
}
/** @private {!Array<!goog.Promise<*, *>|!goog.Promise<void>>} List of
* pending promises. */
this.pendingPromises_ = [];
/** @private {!Array<!function(?string)>} Auth token listeners. */
this.authListeners_ = [];
/** @private {!Array<!function(?string)>} User change listeners. */
this.userChangeListeners_ = [];
/**
* @private {!firebase.Subscribe} The subscribe function to the Auth ID token
* change observer. This will trigger on ID token changes, including
* token refresh on the same user.
*/
this.onIdTokenChanged_ = firebase.INTERNAL.createSubscribe(
goog.bind(this.initIdTokenChangeObserver_, this));
/**
* @private {?string|undefined} The UID of the user that last triggered the
* user state change listener.
*/
this.userStateChangeUid_ = undefined;
/**
* @private {!firebase.Subscribe} The subscribe function to the user state
* change observer.
*/
this.onUserStateChanged_ = firebase.INTERNAL.createSubscribe(
goog.bind(this.initUserStateObserver_, this));
// Set currentUser to null.
this.setCurrentUser_(null);
/**
* @private {!fireauth.storage.UserManager} The Auth user storage
* manager instance.
*/
this.userStorageManager_ =
new fireauth.storage.UserManager(this.getStorageKey());
/**
* @private {!fireauth.storage.RedirectUserManager} The redirect user
* storagemanager instance.
*/
this.redirectUserStorageManager_ =
new fireauth.storage.RedirectUserManager(this.getStorageKey());
/**
* @private {!goog.Promise<undefined>} Promise that resolves when initial
* state is loaded from storage.
*/
this.authStateLoaded_ = this.registerPendingPromise_(this.initAuthState_());
/**
* @private {!goog.Promise<undefined>} Promise that resolves when state and
* redirect result is ready, after which sign in and sign out operations
* are safe to execute.
*/
this.redirectStateIsReady_ = this.registerPendingPromise_(
this.initAuthRedirectState_());
/** @private {boolean} Whether initial state has already been resolved. */
this.isStateResolved_ = false;
/**
* @private {!function()} The syncAuthChanges function with context set to
* auth instance.
*/
this.getSyncAuthUserChanges_ = goog.bind(this.syncAuthUserChanges_, this);
/** @private {!function(!fireauth.AuthUser):!goog.Promise} The handler for
* user state changes. */
this.userStateChangeListener_ =
goog.bind(this.handleUserStateChange_, this);
/** @private {!function(!Object)} The handler for user token changes. */
this.userTokenChangeListener_ =
goog.bind(this.handleUserTokenChange_, this);
/** @private {!function(!Object)} The handler for user deletion. */
this.userDeleteListener_ = goog.bind(this.handleUserDelete_, this);
/** @private {!function(!Object)} The handler for user invalidation. */
this.userInvalidatedListener_ = goog.bind(this.handleUserInvalidated_, this);
/**
* @private {?fireauth.AuthEventManager} The Auth event manager instance.
*/
this.authEventManager_ = null;
// TODO: find better way to enable or disable auth event manager.
if (fireauth.AuthEventManager.ENABLED) {
// Initialize Auth event manager to handle popup and redirect operations.
this.initAuthEventManager_();
}
// Export INTERNAL namespace.
this.INTERNAL = {};
this.INTERNAL['delete'] = goog.bind(this.delete, this);
this.INTERNAL['logFramework'] = goog.bind(this.logFramework, this);
/**
* @private {number} The number of Firebase services subscribed to Auth
* changes.
*/
this.firebaseServices_ = 0;
// Add call to superclass constructor.
fireauth.Auth.base(this, 'constructor');
// Initialize readable/writable Auth properties.
this.initializeReadableWritableProps_();
/**
* @private {!Array<string>} List of Firebase frameworks/libraries used. This
* is currently only used to log FirebaseUI.
*/
this.frameworks_ = [];
};
goog.inherits(fireauth.Auth, goog.events.EventTarget);
/**
* Language code change custom event.
* @param {?string} languageCode The new language code.
* @constructor
* @extends {goog.events.Event}
*/
fireauth.Auth.LanguageCodeChangeEvent = function(languageCode) {
goog.events.Event.call(
this, fireauth.constants.AuthEventType.LANGUAGE_CODE_CHANGED);
this.languageCode = languageCode;
};
goog.inherits(fireauth.Auth.LanguageCodeChangeEvent, goog.events.Event);
/**
* Framework change custom event.
* @param {!Array<string>} frameworks The new frameworks array.
* @constructor
* @extends {goog.events.Event}
*/
fireauth.Auth.FrameworkChangeEvent = function(frameworks) {
goog.events.Event.call(
this, fireauth.constants.AuthEventType.FRAMEWORK_CHANGED);
this.frameworks = frameworks;
};
goog.inherits(fireauth.Auth.FrameworkChangeEvent, goog.events.Event);
/**
* Changes the Auth state persistence to the specified one.
* @param {!fireauth.authStorage.Persistence} persistence The Auth state
* persistence mechanism.
* @return {!goog.Promise<void>}
*/
fireauth.Auth.prototype.setPersistence = function(persistence) {
// TODO: fix auth.delete() behavior and how this affects persistence
// change after deletion.
// Throw an error if already destroyed.
// Set current persistence.
var p = this.userStorageManager_.setPersistence(persistence);
return /** @type {!goog.Promise<void>} */ (this.registerPendingPromise_(p));
};
/**
* Get rid of Closure warning - the property is adding in the constructor.
* @type {!firebase.app.App}
*/
fireauth.Auth.prototype.app;
/**
* Sets the language code.
* @param {?string} languageCode
*/
fireauth.Auth.prototype.setLanguageCode = function(languageCode) {
// Don't do anything if no change detected.
if (this.languageCode_ !== languageCode && !this.deleted_) {
this.languageCode_ = languageCode;
// Update custom Firebase locale field.
this.rpcHandler_.updateCustomLocaleHeader(this.languageCode_);
// Notify external language code change listeners.
this.notifyLanguageCodeListeners_();
}
};
/**
* Returns the current auth instance's language code if available.
* @return {?string}
*/
fireauth.Auth.prototype.getLanguageCode = function() {
return this.languageCode_;
};
/**
* Sets the current language to the default device/browser preference.
*/
fireauth.Auth.prototype.useDeviceLanguage = function() {
this.setLanguageCode(fireauth.util.getUserLanguage());
};
/**
* @param {string} frameworkId The framework identifier.
*/
fireauth.Auth.prototype.logFramework = function(frameworkId) {
// Theoretically multiple frameworks could be used
// (angularfire and FirebaseUI). Once a framework is used, it is not going
// to be unused, so no point adding a method to remove the framework ID.
this.frameworks_.push(frameworkId);
// Update the client version in RPC handler with the new frameworks.
this.rpcHandler_.updateClientVersion(firebase.SDK_VERSION ?
fireauth.util.getClientVersion(
fireauth.util.ClientImplementation.JSCORE, firebase.SDK_VERSION,
this.frameworks_) :
null);
this.dispatchEvent(new fireauth.Auth.FrameworkChangeEvent(
this.frameworks_));
};
/** @return {!Array<string>} The current Firebase frameworks. */
fireauth.Auth.prototype.getFramework = function() {
return goog.array.clone(this.frameworks_);
};
/**
* Updates the framework list on the provided user and configures the user to
* listen to the Auth instance for any framework ID changes.
* @param {!fireauth.AuthUser} user The user to whose framework list needs to be
* updated.
* @private
*/
fireauth.Auth.prototype.setUserFramework_ = function(user) {
// Sets the framework ID on the user.
user.setFramework(this.frameworks_);
// Sets current Auth instance as framework list change dispatcher on the user.
user.setFrameworkChangeDispatcher(this);
};
/**
* Sets the tenant ID.
* @param {?string} tenantId The tenant ID of the tenant project if available.
*/
fireauth.Auth.prototype.setTenantId = function(tenantId) {
// Don't do anything if no change detected.
if (this.tenantId_ !== tenantId && !this.deleted_) {
this.tenantId_ = tenantId;
this.rpcHandler_.updateTenantId(this.tenantId_);
}
};
/**
* Returns the current Auth instance's tenant ID.
* @return {?string}
*/
fireauth.Auth.prototype.getTenantId = function() {
return this.tenantId_;
};
/**
* Initializes readable/writable properties on Auth.
* @suppress {invalidCasts}
* @private
*/
fireauth.Auth.prototype.initializeReadableWritableProps_ = function() {
Object.defineProperty(/** @type {!Object} */ (this), 'lc', {
/**
* @this {!Object}
* @return {?string} The current language code.
*/
get: function() {
return this.getLanguageCode();
},
/**
* @this {!Object}
* @param {string} value The new language code.
*/
set: function(value) {
this.setLanguageCode(value);
},
enumerable: false
});
// Initialize to null.
/** @private {?string} The current Auth instance's language code. */
this.languageCode_ = null;
// Initialize tenant ID.
Object.defineProperty(/** @type {!Object} */ (this), 'ti', {
/**
* @this {!Object}
* @return {?string} The current tenant ID.
*/
get: function() {
return this.getTenantId();
},
/**
* @this {!Object}
* @param {?string} value The new tenant ID.
*/
set: function(value) {
this.setTenantId(value);
},
enumerable: false
});
// Initialize to null.
/** @private {?string} The current Auth instance's tenant ID. */
this.tenantId_ = null;
};
/**
* Notifies all external listeners of the language code change.
* @private
*/
fireauth.Auth.prototype.notifyLanguageCodeListeners_ = function() {
// Notify external listeners on the language code change.
this.dispatchEvent(new fireauth.Auth.LanguageCodeChangeEvent(
this.getLanguageCode()));
};
/**
* @return {!Object} The object representation of the Auth instance.
* @override
*/
fireauth.Auth.prototype.toJSON = function() {
// Return the plain object representation in case JSON.stringify is called on
// an Auth instance.
return {
'apiKey': this.app_().options['apiKey'],
'authDomain': this.app_().options['authDomain'],
'appName': this.app_().name,
'currentUser': this.currentUser_() && this.currentUser_().toPlainObject()
};
};
/**
* Returns the Auth event manager promise.
* @return {!goog.Promise<!fireauth.AuthEventManager>}
* @private
*/
fireauth.Auth.prototype.getAuthEventManager_ = function() {
// Either return cached Auth event manager promise provider if available or a
// promise that rejects with missing Auth domain error.
return this.eventManagerProviderPromise_ ||
goog.Promise.reject(
new fireauth.AuthError(fireauth.authenum.Error.MISSING_AUTH_DOMAIN));
};
/**
* Initializes the Auth event manager when state is ready.
* @private
*/
fireauth.Auth.prototype.initAuthEventManager_ = function() {
// Initialize Auth event manager on initState.
var self = this;
var authDomain = this.app_().options['authDomain'];
var apiKey = this.app_().options['apiKey'];
// Make sure environment also supports popup and redirect.
if (authDomain && fireauth.util.isPopupRedirectSupported()) {
// Auth domain is required for Auth event manager to resolve.
// Auth state has to be loaded first. One reason is to process link events.
this.eventManagerProviderPromise_ = this.authStateLoaded_.then(function() {
if (self.deleted_) {
return;
}
// By this time currentUser should be ready if available and will be able
// to resolve linkWithRedirect if detected.
self.authEventManager_ = fireauth.AuthEventManager.getManager(
authDomain, apiKey, self.app_().name);
// Subscribe Auth instance.
self.authEventManager_.subscribe(self);
// Subscribe current user by enabling popup and redirect on that user.
if (self.currentUser_()) {
self.currentUser_().enablePopupRedirect();
}
// If a redirect user is present, subscribe to popup and redirect events.
// In case current user was not available and the developer called link
// with redirect on a signed out user, this will work and the linked
// logged out user will be returned in getRedirectResult.
// current user and redirect user are the same (was already logged in),
// currentUser will have priority as it is subscribed before redirect
// user. This change will also allow further popup and redirect events on
// the redirect user going forward.
if (self.redirectUser_) {
self.redirectUser_.enablePopupRedirect();
// Set the user language for the redirect user.
self.setUserLanguage_(
/** @type {!fireauth.AuthUser} */ (self.redirectUser_));
// Set the user Firebase frameworks for the redirect user.
self.setUserFramework_(
/** @type {!fireauth.AuthUser} */ (self.redirectUser_));
// Reference to redirect user no longer needed.
self.redirectUser_ = null;
}
return self.authEventManager_;
});
}
};
/**
* @param {!fireauth.AuthEvent.Type} mode The Auth type mode.
* @param {?string=} opt_eventId The event ID.
* @return {boolean} Whether the auth event handler can handler the provided
* event.
* @override
*/
fireauth.Auth.prototype.canHandleAuthEvent = function(mode, opt_eventId) {
// Only sign in events are handled.
switch (mode) {
// Accept all general sign in with redirect and unknowns.
// Migrating redirect events to use session storage will prevent this event
// from leaking to other tabs.
case fireauth.AuthEvent.Type.UNKNOWN:
case fireauth.AuthEvent.Type.SIGN_IN_VIA_REDIRECT:
return true;
case fireauth.AuthEvent.Type. SIGN_IN_VIA_POPUP:
// Pending sign in with popup event must match the stored popup event ID.
return this.popupEventId_ == opt_eventId &&
!!this.pendingPopupResolvePromise_;
default:
return false;
}
};
/**
* Completes the pending popup operation. If error is not null, rejects with the
* error. Otherwise, it resolves with the popup redirect result.
* @param {!fireauth.AuthEvent.Type} mode The Auth type mode.
* @param {?fireauth.AuthEventManager.Result} popupRedirectResult The result
* to resolve with when no error supplied.
* @param {?fireauth.AuthError} error When supplied, the promise will reject.
* @param {?string=} opt_eventId The event ID.
* @override
*/
fireauth.Auth.prototype.resolvePendingPopupEvent =
function(mode, popupRedirectResult, error, opt_eventId) {
// Only handles popup events of type sign in and which match popup event ID.
if (mode != fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP ||
this.popupEventId_ != opt_eventId) {
return;
}
if (error && this.pendingPopupRejectPromise_) {
// Reject with error for supplied mode.
this.pendingPopupRejectPromise_(error);
} else if (popupRedirectResult &&
!error &&
this.pendingPopupResolvePromise_) {
// Resolve with result for supplied mode.
this.pendingPopupResolvePromise_(popupRedirectResult);
}
// Now that event is resolved, delete popup timeout promise.
if (this.popupTimeoutPromise_) {
this.popupTimeoutPromise_.cancel();
this.popupTimeoutPromise_ = null;
}
// Delete pending promises.
delete this.pendingPopupResolvePromise_;
delete this.pendingPopupRejectPromise_;
};
/**
* Returns the handler's appropriate popup and redirect sign in operation
* finisher.
* @param {!fireauth.AuthEvent.Type} mode The Auth type mode.
* @param {?string=} opt_eventId The optional event ID.
* @return {?function(string, string, ?string,
* ?string=):!goog.Promise<!fireauth.AuthEventManager.Result>}
* @override
*/
fireauth.Auth.prototype.getAuthEventHandlerFinisher =
function(mode, opt_eventId) {
// Sign in events will be completed by finishPopupAndRedirectSignIn.
if (mode == fireauth.AuthEvent.Type.SIGN_IN_VIA_REDIRECT) {
return goog.bind(this.finishPopupAndRedirectSignIn, this);
} else if (mode == fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP &&
this.popupEventId_ == opt_eventId &&
this.pendingPopupResolvePromise_) {
return goog.bind(this.finishPopupAndRedirectSignIn, this);
}
return null;
};
/**
* Finishes the popup and redirect sign in operations.
* @param {string} requestUri The callback url with the oauth response.
* @param {string} sessionId The session id used to generate the authUri.
* @param {?string} tenantId The tenant ID.
* @param {?string=} opt_postBody The optional POST body content.
* @return {!goog.Promise<!fireauth.AuthEventManager.Result>}
*/
fireauth.Auth.prototype.finishPopupAndRedirectSignIn =
function(requestUri, sessionId, tenantId, opt_postBody) {
var self = this;
// Verify assertion request.
var request = {
'requestUri': requestUri,
'postBody': opt_postBody,
'sessionId': sessionId,
// Even if tenant ID is null, still pass it to RPC handler explicitly so
// that it won't be overridden by RPC handler's tenant ID.
'tenantId': tenantId
};
// Now that popup has responded, delete popup timeout promise.
if (this.popupTimeoutPromise_) {
this.popupTimeoutPromise_.cancel();
this.popupTimeoutPromise_ = null;
}
// When state is ready, run verify assertion request.
// This will only run either after initial and redirect state is ready for
// popups or after initial state is ready for redirect resolution.
return self.authStateLoaded_.then(function() {
return self.signInWithIdTokenProvider_(
self.rpcHandler_.verifyAssertion(request));
});
};
/**
* @return {string} The generated event ID used to identify a popup event.
* @private
*/
fireauth.Auth.prototype.generateEventId_ = function() {
return fireauth.util.generateEventId();
};
/**
* Signs in to Auth provider via popup.
* @param {!fireauth.AuthProvider} provider The Auth provider to sign in with.
* @return {!goog.Promise<!fireauth.AuthEventManager.Result>}
*/
fireauth.Auth.prototype.signInWithPopup = function(provider) {
// Check if popup and redirect are supported in this environment.
if (!fireauth.util.isPopupRedirectSupported()) {
return goog.Promise.reject(new fireauth.AuthError(
fireauth.authenum.Error.OPERATION_NOT_SUPPORTED));
}
var mode = fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP;
var self = this;
// Popup the window immediately to make sure the browser associates the
// popup with the click that triggered it.
// Get provider settings.
var settings = fireauth.idp.getIdpSettings(provider['providerId']);
// There could multiple sign in with popup events in different windows.
// We need to match the correct popup to the correct pending promise.
var eventId = this.generateEventId_();
// If incapable of redirecting popup from opener, popup destination URL
// directly. This could also happen in a sandboxed iframe.
var oauthHelperWidgetUrl = null;
if ((!fireauth.util.runsInBackground() || fireauth.util.isIframe()) &&
this.app_().options['authDomain'] &&
provider['isOAuthProvider']) {
oauthHelperWidgetUrl =
fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl(
this.app_().options['authDomain'],
this.app_().options['apiKey'],
this.app_().name,
mode,
provider,
null,
eventId,
firebase.SDK_VERSION || null,
null,
null,
this.getTenantId());
}
// The popup must have a name, otherwise when successive popups are triggered
// they will all render in the same instance and none will succeed since the
// popup cancel of first window will close the shared popup window instance.
var popupWin =
fireauth.util.popup(
oauthHelperWidgetUrl,
fireauth.util.generateRandomString(),
settings && settings.popupWidth,
settings && settings.popupHeight);
// Auth event manager must be available for popup sign in to be possible.
var p = this.getAuthEventManager_().then(function(manager) {
// Process popup request tagging it with newly created event ID.
return manager.processPopup(
popupWin, mode, provider, eventId, !!oauthHelperWidgetUrl,
self.getTenantId());
}).then(function() {
return new goog.Promise(function(resolve, reject) {
// Expire other pending promises if still available..
self.resolvePendingPopupEvent(
mode,
null,
new fireauth.AuthError(fireauth.authenum.Error.EXPIRED_POPUP_REQUEST),
// Existing pending popup event ID.
self.popupEventId_);
// Save current pending promises.
self.pendingPopupResolvePromise_ = resolve;
self.pendingPopupRejectPromise_ = reject;
// Overwrite popup event ID with new one corresponding to popup.
self.popupEventId_ = eventId;
// Keep track of timeout promise to cancel it on promise resolution before
// it times out.
self.popupTimeoutPromise_ =
self.authEventManager_.startPopupTimeout(
self, mode, /** @type {!Window} */ (popupWin), eventId);
});
}).then(function(result) {
// On resolution, close popup if still opened and pass result through.
if (popupWin) {
fireauth.util.closeWindow(popupWin);
}
if (result) {
return fireauth.object.makeReadonlyCopy(result);
}
return null;
}).thenCatch(function(error) {
if (popupWin) {
fireauth.util.closeWindow(popupWin);
}
throw error;
});
return /** @type {!goog.Promise<!fireauth.AuthEventManager.Result>} */ (
this.registerPendingPromise_(p));
};
/**
* Signs in to Auth provider via redirect.
* @param {!fireauth.AuthProvider} provider The Auth provider to sign in with.
* @return {!goog.Promise<void>}
*/
fireauth.Auth.prototype.signInWithRedirect = function(provider) {
// Check if popup and redirect are supported in this environment.
if (!fireauth.util.isPopupRedirectSupported()) {
return goog.Promise.reject(new fireauth.AuthError(
fireauth.authenum.Error.OPERATION_NOT_SUPPORTED));
}
var self = this;
var mode = fireauth.AuthEvent.Type.SIGN_IN_VIA_REDIRECT;
// Auth event manager must be available for sign in via redirect to be
// possible.
var p = this.getAuthEventManager_().then(function(manager) {
// Remember current persistence to apply it on the next page.
// This is the only time the state is passed to the next page (when user is
// not already logged in).
// This is not needed for link and reauthenticate as the user is already
// stored with specified persistence.
return self.userStorageManager_.savePersistenceForRedirect();
}).then(function() {
// Process redirect operation.
return self.authEventManager_.processRedirect(
mode, provider, undefined, self.getTenantId());
});
return /** @type {!goog.Promise<void>} */ (this.registerPendingPromise_(p));
};
/**
* Returns the redirect result. If coming back from a successful redirect sign
* in, will resolve to the signed in user. If coming back from an unsuccessful
* redirect sign, will reject with the proper error. If no redirect operation
* called, resolves with null.
* @return {!goog.Promise<!fireauth.AuthEventManager.Result>}
* @private
*/
fireauth.Auth.prototype.getRedirectResultWithoutClearing_ = function() {
// Check if popup and redirect are supported in this environment.
if (!fireauth.util.isPopupRedirectSupported()) {
return goog.Promise.reject(new fireauth.AuthError(
fireauth.authenum.Error.OPERATION_NOT_SUPPORTED));
}
var self = this;
// Auth event manager must be available for get redirect result to be
// possible.
var p = this.getAuthEventManager_().then(function(manager) {
// Return redirect result when resolved.
return self.authEventManager_.getRedirectResult();
}).then(function(result) {
if (result) {
return fireauth.object.makeReadonlyCopy(result);
}
return null;
});
return /** @type {!goog.Promise<!fireauth.AuthEventManager.Result>} */ (
this.registerPendingPromise_(p));
};
/**
* In addition to returning the redirect result as in
* `getRedirectResultWithoutClearing_`, this will also clear the cached
* redirect result for security reasons.
* @return {!goog.Promise<!fireauth.AuthEventManager.Result>}
*/
fireauth.Auth.prototype.getRedirectResult = function() {
return this.getRedirectResultWithoutClearing_()
.then((result) => {
if (this.authEventManager_) {
this.authEventManager_.clearRedirectResult();
}
return result;
})
.thenCatch((error) => {
if (this.authEventManager_) {
this.authEventManager_.clearRedirectResult();
}
throw error;
});
};
/**
* Asynchronously sets the provided user as currentUser on the current Auth
* instance.
* @param {?fireauth.AuthUser} user The user to be copied to Auth instance.
* @return {!goog.Promise<void>}
*/
fireauth.Auth.prototype.updateCurrentUser = function(user) {
if (!user) {
return goog.Promise.reject(new fireauth.AuthError(
fireauth.authenum.Error.NULL_USER));
}
if (this.tenantId_ != user['tenantId']) {
return goog.Promise.reject(new fireauth.AuthError(
fireauth.authenum.Error.TENANT_ID_MISMATCH));
}
var self = this;
var options = {};
options['apiKey'] = this.app_().options['apiKey'];
options['authDomain'] = this.app_().options['authDomain'];
options['appName'] = this.app_().name;
var newUser = fireauth.AuthUser.copyUser(user, options,
self.redirectUserStorageManager_, self.getFramework());
return this.registerPendingPromise_(
this.redirectStateIsReady_.then(function() {
if (self.app_().options['apiKey'] != user.getApiKey()) {
// Throws auth/invalid-user-token if user doesn't belong to app.
// Throws auth/user-token-expired if token expires.
return newUser.reload();
}
}).then(function() {
if (self.currentUser_() && user['uid'] == self.currentUser_()['uid']) {
// Same user signed in. Update user data and notify Auth listeners.
// No need to resubscribe to user events.
// TODO: Check if the user to copy is older than current user and skip
// the copy logic in that case.
self.currentUser_().copy(user);
return self.handleUserStateChange_(user);
}
self.setCurrentUser_(newUser);
// Enable popup and redirect events.
newUser.enablePopupRedirect();
// Save user changes.
return self.handleUserStateChange_(newUser);
}).then(function(user) {
self.notifyAuthListeners_();
}));
};
/**
* Completes the headless sign in with the server response containing the STS
* access and refresh tokens, and sets the Auth user as current user while
* setting all listeners to it and saving it to storage.
* @param {!Object<string, string>} idTokenResponse The ID token response from
* the server.
* @return {!goog.Promise<void>}
*/
fireauth.Auth.prototype.signInWithIdTokenResponse =
function(idTokenResponse) {
var self = this;
var options = {};
options['apiKey'] = self.app_().options['apiKey'];
options['authDomain'] = self.app_().options['authDomain'];
options['appName'] = self.app_().name;
// Wait for state to be ready.
// This is used internally and is also used for redirect sign in so there is
// no need for waiting for redirect result to resolve since redirect result
// depends on it.
return this.authStateLoaded_.then(function() {
// Initialize an Auth user using the provided ID token response.
return fireauth.AuthUser.initializeFromIdTokenResponse(
options,
idTokenResponse,
/** @type {!fireauth.storage.RedirectUserManager} */ (
self.redirectUserStorageManager_),
// Pass frameworks so they are logged in getAccountInfo while populating
// the user info.
self.getFramework());
}).then(function(user) {
// Check if the same user is already signed in.
if (self.currentUser_() &&
user['uid'] == self.currentUser_()['uid']) {
// Same user signed in. Update user data and notify Auth listeners.
// No need to resubscribe to user events.
self.currentUser_().copy(user);
return self.handleUserStateChange_(user);
}
// New user.
// Set current user and attach all listeners to it.
self.setCurrentUser_(user);
// Enable popup and redirect events.
user.enablePopupRedirect();
// Save user changes.
return self.handleUserStateChange_(user);
}).then(function() {
// Notify external Auth listeners only when state is ready.
self.notifyAuthListeners_();
});
};
/**
* Updates the current auth user and attaches event listeners to changes on it.
* Also removes all event listeners from previously signed in user.
* @param {?fireauth.AuthUser} user The current user instance.
* @private
*/
fireauth.Auth.prototype.setCurrentUser_ = function(user) {
// Must be called first before updating currentUser reference.
this.attachEventListeners_(user);
// Update currentUser property.
fireauth.object.setReadonlyProperty(this, 'currentUser', user);
if (user) {
// If a user is available, set the language code on it and set current Auth
// instance as language code change dispatcher.
this.setUserLanguage_(user);
// Set the current frameworks used on the user and set current Auth instance
// as the framework change dispatcher.
this.setUserFramework_(user);
}
};
/**
* Signs out the current user while deleting the Auth user from storage and
* removing all listeners from it.
* @return {!goog.Promise<void>}
*/
fireauth.Auth.prototype.signOut = function() {
var self = this;
// Wait for final state to be ready first, otherwise a signed out user could
// come back to life.
var p = this.redirectStateIsReady_.then(function() {
// Clear any cached redirect result on sign out, even if user is already
// signed out. For example, sign in could fail due to account conflict
// error, the error in redirect result should still be cleared. There is
// also the use case where you keep a reference to a signed out user and
// call signedOutUser.linkWithRedirect(provider). Even though the user is
// signed out, getRedirectResult() will resolve with the modified signed
// out user. This could also throw an error
// (provider already linked, etc).
if (self.authEventManager_) {
self.authEventManager_.clearRedirectResult();
}
// Ignore if already signed out.
if (!self.currentUser_()) {
return goog.Promise.resolve();
}
// Detach all event listeners.
// Set current user to null.
self.setCurrentUser_(null);
// Remove current user from storage
return /** @type {!fireauth.storage.UserManager} */ (
self.userStorageManager_).removeCurrentUser()
.then(function() {
// Notify external Auth listeners of this Auth change event.
self.notifyAuthListeners_();
});
});
return /** @type {!goog.Promise<void>} */ (this.registerPendingPromise_(p));
};
/**
* @return {!goog.Promise} A promise that resolved when any stored redirect user
* is loaded and removed from session storage and then stored locally.
* @private
*/
fireauth.Auth.prototype.initRedirectUser_ = function() {
var self = this;
var authDomain = this.app_().options['authDomain'];
// Get any saved redirect user and delete from session storage.
// Override user's authDomain with app's authDomain if there is a mismatch.
var p = /** @type {!fireauth.storage.RedirectUserManager} */ (
this.redirectUserStorageManager_).getRedirectUser(authDomain)
.then(function(user) {
// Save redirect user.
self.redirectUser_ = user;
if (user) {
// Set redirect storage manager on user.
user.setRedirectStorageManager(
/** @type {!fireauth.storage.RedirectUserManager} */ (
self.redirectUserStorageManager_));
}
// Delete redirect user.
return /** @type {!fireauth.storage.RedirectUserManager} */ (
self.redirectUserStorageManager_).removeRedirectUser();
});
return /** @type {!goog.Promise<undefined>} */ (
this.registerPendingPromise_(p));
};
/**
* Loads the initial Auth state for current application from web storage and
* initializes Auth user accordingly to reflect that state. This routine does
* not wait for any pending redirect result to be resolved.
* @return {!goog.Promise<undefined>} Promise that resolves when state is ready,
* loaded from storage.
* @private
*/
fireauth.Auth.prototype.initAuthState_ = function() {
// Load current user from storage.
var self = this;
var authDomain = this.app_().options['authDomain'];
// Get any saved redirected user first.