forked from angular/angularfire
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathauth.ts
More file actions
261 lines (221 loc) · 9.89 KB
/
auth.ts
File metadata and controls
261 lines (221 loc) · 9.89 KB
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
import { Injectable, Inject, Optional, NgZone, PLATFORM_ID, InjectionToken, OnDestroy } from '@angular/core';
import { Observable, of, from, merge, Subject, Subscriber, SubscriptionLike } from 'rxjs';
import { ajax } from 'rxjs/ajax';
import { switchMap, map, observeOn, shareReplay, filter, switchMapTo, subscribeOn, take } from 'rxjs/operators';
import {
FIREBASE_OPTIONS,
FIREBASE_APP_NAME,
FirebaseOptions,
FirebaseAppConfig,
ɵPromiseProxy,
ɵlazySDKProxy,
ɵfirebaseAppFactory,
ɵAngularFireSchedulers,
ɵkeepUnstableUntilFirstFactory,
ɵapplyMixins
} from '@angular/fire';
import firebase from 'firebase/app';
import { isPlatformServer } from '@angular/common';
import { proxyPolyfillCompat } from './base';
import { ɵfetchInstance } from '@angular/fire';
import { REQUEST } from '@nguniversal/express-engine/tokens';
import { get as getCookie, set as setCookie, remove as removeCookie } from 'js-cookie';
import { parse as parseCookies } from 'cookie';
export interface AngularFireAuth extends ɵPromiseProxy<firebase.auth.Auth> {}
type UseEmulatorArguments = [string, number];
export const USE_EMULATOR = new InjectionToken<UseEmulatorArguments>('angularfire2.auth.use-emulator');
export const SETTINGS = new InjectionToken<firebase.auth.AuthSettings>('angularfire2.auth.settings');
export const TENANT_ID = new InjectionToken<string>('angularfire2.auth.tenant-id');
export const LANGUAGE_CODE = new InjectionToken<string>('angularfire2.auth.langugage-code');
export const USE_DEVICE_LANGUAGE = new InjectionToken<boolean>('angularfire2.auth.use-device-language');
export const PERSISTENCE = new InjectionToken<string>('angularfire.auth.persistence');
export const EXPERIMENTAL_COOKIE_AUTH = new InjectionToken<boolean>('angularfire.auth.ssr-auth');
@Injectable({
providedIn: 'any'
})
export class AngularFireAuth implements OnDestroy {
private disposables: SubscriptionLike[] = [];
/**
* Observable of authentication state; as of Firebase 4.0 this is only triggered via sign-in/out
*/
public readonly authState: Observable<firebase.User|null>;
/**
* Observable of the currently signed-in user's JWT token used to identify the user to a Firebase service (or null).
*/
public readonly idToken: Observable<string|null>;
/**
* Observable of the currently signed-in user (or null).
*/
public readonly user: Observable<firebase.User|null>;
/**
* Observable of the currently signed-in user's IdTokenResult object which contains the ID token JWT string and other
* helper properties for getting different data associated with the token as well as all the decoded payload claims
* (or null).
*/
public readonly idTokenResult: Observable<firebase.auth.IdTokenResult|null>;
/**
* Observable of the currently signed-in user's credential, or null
*/
public readonly credential: Observable<firebase.auth.UserCredential|null>;
constructor(
@Inject(FIREBASE_OPTIONS) options: FirebaseOptions,
@Optional() @Inject(FIREBASE_APP_NAME) nameOrConfig: string|FirebaseAppConfig|null|undefined,
// tslint:disable-next-line:ban-types
@Inject(PLATFORM_ID) platformId: Object,
zone: NgZone,
@Optional() @Inject(USE_EMULATOR) _useEmulator: any, // can't use the tuple here
@Optional() @Inject(SETTINGS) _settings: any, // can't use firebase.auth.AuthSettings here
@Optional() @Inject(TENANT_ID) tenantId: string | null,
@Optional() @Inject(LANGUAGE_CODE) languageCode: string | null,
@Optional() @Inject(USE_DEVICE_LANGUAGE) useDeviceLanguage: boolean | null,
@Optional() @Inject(PERSISTENCE) persistence: string | null,
@Optional() @Inject(EXPERIMENTAL_COOKIE_AUTH) experimentalCookieAuth: boolean | null,
@Optional() @Inject(REQUEST) request: any,
) {
const schedulers = new ɵAngularFireSchedulers(zone);
const keepUnstableUntilFirst = ɵkeepUnstableUntilFirstFactory(schedulers);
const logins = new Subject<Required<firebase.auth.UserCredential>>();
const auth = of(undefined).pipe(
observeOn(schedulers.outsideAngular),
switchMap(() => zone.runOutsideAngular(() => import('firebase/auth'))),
map(() => ɵfirebaseAppFactory(options, zone, nameOrConfig)),
map(app => zone.runOutsideAngular(() => {
const useEmulator: UseEmulatorArguments | null = _useEmulator;
const settings: firebase.auth.AuthSettings | null = _settings;
return ɵfetchInstance(`${app.name}.auth`, 'AngularFireAuth', app, () => {
const auth = zone.runOutsideAngular(() => app.auth());
if (useEmulator) {
// Firebase Auth doesn't conform to the useEmulator convention, let's smooth that over
auth.useEmulator(`http://${useEmulator.join(':')}`);
}
if (tenantId) {
auth.tenantId = tenantId;
}
auth.languageCode = languageCode;
if (useDeviceLanguage) {
auth.useDeviceLanguage();
}
if (settings) {
auth.settings = settings;
}
if (persistence) {
auth.setPersistence(persistence);
}
return auth;
}, [useEmulator, tenantId, languageCode, useDeviceLanguage, settings, persistence]);
})),
shareReplay({ bufferSize: 1, refCount: false }),
);
// HACK, as we're exporting auth.Auth, rather than auth, developers importing firebase.auth
// (e.g, `import { auth } from 'firebase/app'`) are getting an undefined auth object unexpectedly
// as we're completely lazy. Let's eagerly load the Auth SDK here.
// There could potentially be race conditions still... but this greatly decreases the odds while
// we reevaluate the API.
this.disposables.push(auth.pipe(take(1)).subscribe());
if (isPlatformServer(platformId)) {
if (experimentalCookieAuth) {
this.credential = auth.pipe(
switchMap(auth => {
const encodedSession = request.signedCookies?.__session;
if (encodedSession) {
const { uid, customToken } = JSON.parse(encodedSession);
if (auth.currentUser?.uid === uid) {
return of({ user: auth.currentUser, credential: null });
} else {
return auth.signInWithCustomToken(customToken).then(it => it, (e) => {
console.warn(e);
return null;
});
}
} else {
return of(null);
}
}),
map(credential => credential?.user ? credential as firebase.auth.UserCredential : null),
shareReplay({ bufferSize: 1, refCount: false }),
);
this.user = this.authState = this.credential.pipe(
map(it => it?.user)
);
} else {
this.user = this.authState = this.credential = of(null);
}
} else {
const redirectResult = auth.pipe(
switchMap(auth => auth.getRedirectResult().then(it => it, () => null)),
keepUnstableUntilFirst,
shareReplay({ bufferSize: 1, refCount: false }),
);
const fromCallback = <T = any>(cb: (sub: Subscriber<T>) => () => void) => new Observable<T>(subscriber =>
({ unsubscribe: zone.runOutsideAngular(() => cb(subscriber)) })
);
const authStateChanged = auth.pipe(
switchMap(auth => fromCallback(auth.onAuthStateChanged.bind(auth))),
);
const idTokenChanged = auth.pipe(
switchMap(auth => fromCallback(auth.onIdTokenChanged.bind(auth)))
);
this.authState = redirectResult.pipe(
switchMapTo(authStateChanged),
subscribeOn(schedulers.outsideAngular),
observeOn(schedulers.insideAngular),
);
this.user = redirectResult.pipe(
switchMapTo(idTokenChanged),
subscribeOn(schedulers.outsideAngular),
observeOn(schedulers.insideAngular),
);
this.credential = merge(
redirectResult,
logins,
// pipe in null authState to make credential zipable, just a weird devexp if
// authState and user go null to still have a credential
this.authState.pipe(filter(it => !it))
).pipe(
// handle the { user: { } } when a user is already logged in, rather have null
// TODO handle the type corcersion better
map(credential => credential?.user ? credential : null),
subscribeOn(schedulers.outsideAngular),
observeOn(schedulers.insideAngular),
);
}
this.idToken = this.user.pipe(
switchMap(user => user ? from(user.getIdToken()) : of(null))
);
this.idTokenResult = this.user.pipe(
switchMap(user => user ? from(user.getIdTokenResult()) : of(null))
);
if (!isPlatformServer(platformId) && experimentalCookieAuth) {
this.disposables.push(
this.idToken.pipe(
// use xhr rather than fetch so set-cookie works
switchMap(idToken => ajax({
url: '/createSession',
method: 'POST',
headers: idToken ? {
authorization: `Bearer ${idToken}`
} : {}
}))
).subscribe()
);
}
return ɵlazySDKProxy(this, auth, zone, { spy: {
apply: (name, _, val) => {
// If they call a signIn or createUser function listen into the promise
// this will give us the user credential, push onto the logins Subject
// to be consumed in .credential
if (name.startsWith('signIn') || name.startsWith('createUser')) {
// TODO fix the types, the trouble is UserCredential has everything optional
val.then((user: firebase.auth.UserCredential) => logins.next(user as any));
}
}
}});
}
ngOnDestroy() {
this.disposables.forEach(it => it.unsubscribe());
// TODO let's take advantage of this, rather than sign-out let's use the cookie uid
// in the app name (if experimentalCookieAuth that is)
firebase.apps.forEach(app => app.auth().signOut());
}
}
ɵapplyMixins(AngularFireAuth, [proxyPolyfillCompat]);