react-native-nitro-auth 0.5.1 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/README.md +362 -190
  2. package/android/build.gradle +2 -5
  3. package/android/src/main/cpp/PlatformAuth+Android.cpp +84 -18
  4. package/android/src/main/java/com/auth/AuthAdapter.kt +82 -182
  5. package/android/src/main/java/com/auth/NitroAuthPackage.kt +1 -1
  6. package/app.plugin.js +2 -9
  7. package/cpp/AuthCache.cpp +0 -134
  8. package/cpp/AuthCache.hpp +0 -7
  9. package/cpp/HybridAuth.cpp +57 -63
  10. package/cpp/HybridAuth.hpp +3 -4
  11. package/ios/AuthAdapter.swift +23 -25
  12. package/lib/commonjs/Auth.web.js +523 -201
  13. package/lib/commonjs/Auth.web.js.map +1 -1
  14. package/lib/commonjs/index.js +0 -12
  15. package/lib/commonjs/index.js.map +1 -1
  16. package/lib/commonjs/index.web.js +0 -12
  17. package/lib/commonjs/index.web.js.map +1 -1
  18. package/lib/commonjs/js-storage-adapter.js +2 -0
  19. package/lib/commonjs/js-storage-adapter.js.map +1 -0
  20. package/lib/commonjs/service.js +9 -86
  21. package/lib/commonjs/service.js.map +1 -1
  22. package/lib/commonjs/service.web.js +1 -5
  23. package/lib/commonjs/service.web.js.map +1 -1
  24. package/lib/commonjs/ui/social-button.js +44 -29
  25. package/lib/commonjs/ui/social-button.js.map +1 -1
  26. package/lib/commonjs/ui/social-button.web.js +44 -29
  27. package/lib/commonjs/ui/social-button.web.js.map +1 -1
  28. package/lib/commonjs/use-auth.js +56 -42
  29. package/lib/commonjs/use-auth.js.map +1 -1
  30. package/lib/commonjs/utils/logger.js +12 -4
  31. package/lib/commonjs/utils/logger.js.map +1 -1
  32. package/lib/module/Auth.web.js +523 -201
  33. package/lib/module/Auth.web.js.map +1 -1
  34. package/lib/module/index.js +0 -1
  35. package/lib/module/index.js.map +1 -1
  36. package/lib/module/index.web.js +0 -1
  37. package/lib/module/index.web.js.map +1 -1
  38. package/lib/module/js-storage-adapter.js +2 -0
  39. package/lib/module/js-storage-adapter.js.map +1 -0
  40. package/lib/module/service.js +9 -86
  41. package/lib/module/service.js.map +1 -1
  42. package/lib/module/service.web.js +1 -5
  43. package/lib/module/service.web.js.map +1 -1
  44. package/lib/module/ui/social-button.js +44 -29
  45. package/lib/module/ui/social-button.js.map +1 -1
  46. package/lib/module/ui/social-button.web.js +44 -29
  47. package/lib/module/ui/social-button.web.js.map +1 -1
  48. package/lib/module/use-auth.js +56 -42
  49. package/lib/module/use-auth.js.map +1 -1
  50. package/lib/module/utils/logger.js +12 -4
  51. package/lib/module/utils/logger.js.map +1 -1
  52. package/lib/typescript/commonjs/Auth.nitro.d.ts +3 -3
  53. package/lib/typescript/commonjs/Auth.nitro.d.ts.map +1 -1
  54. package/lib/typescript/commonjs/Auth.web.d.ts +25 -6
  55. package/lib/typescript/commonjs/Auth.web.d.ts.map +1 -1
  56. package/lib/typescript/commonjs/index.d.ts +1 -2
  57. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  58. package/lib/typescript/commonjs/index.web.d.ts +0 -1
  59. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  60. package/lib/typescript/commonjs/js-storage-adapter.d.ts +6 -0
  61. package/lib/typescript/commonjs/js-storage-adapter.d.ts.map +1 -0
  62. package/lib/typescript/commonjs/service.d.ts +1 -8
  63. package/lib/typescript/commonjs/service.d.ts.map +1 -1
  64. package/lib/typescript/commonjs/service.web.d.ts +1 -8
  65. package/lib/typescript/commonjs/service.web.d.ts.map +1 -1
  66. package/lib/typescript/commonjs/ui/social-button.d.ts +6 -6
  67. package/lib/typescript/commonjs/ui/social-button.d.ts.map +1 -1
  68. package/lib/typescript/commonjs/ui/social-button.web.d.ts +6 -6
  69. package/lib/typescript/commonjs/ui/social-button.web.d.ts.map +1 -1
  70. package/lib/typescript/commonjs/use-auth.d.ts +4 -4
  71. package/lib/typescript/commonjs/use-auth.d.ts.map +1 -1
  72. package/lib/typescript/commonjs/utils/logger.d.ts +4 -4
  73. package/lib/typescript/commonjs/utils/logger.d.ts.map +1 -1
  74. package/lib/typescript/module/Auth.nitro.d.ts +3 -3
  75. package/lib/typescript/module/Auth.nitro.d.ts.map +1 -1
  76. package/lib/typescript/module/Auth.web.d.ts +25 -6
  77. package/lib/typescript/module/Auth.web.d.ts.map +1 -1
  78. package/lib/typescript/module/index.d.ts +1 -2
  79. package/lib/typescript/module/index.d.ts.map +1 -1
  80. package/lib/typescript/module/index.web.d.ts +0 -1
  81. package/lib/typescript/module/index.web.d.ts.map +1 -1
  82. package/lib/typescript/module/js-storage-adapter.d.ts +6 -0
  83. package/lib/typescript/module/js-storage-adapter.d.ts.map +1 -0
  84. package/lib/typescript/module/service.d.ts +1 -8
  85. package/lib/typescript/module/service.d.ts.map +1 -1
  86. package/lib/typescript/module/service.web.d.ts +1 -8
  87. package/lib/typescript/module/service.web.d.ts.map +1 -1
  88. package/lib/typescript/module/ui/social-button.d.ts +6 -6
  89. package/lib/typescript/module/ui/social-button.d.ts.map +1 -1
  90. package/lib/typescript/module/ui/social-button.web.d.ts +6 -6
  91. package/lib/typescript/module/ui/social-button.web.d.ts.map +1 -1
  92. package/lib/typescript/module/use-auth.d.ts +4 -4
  93. package/lib/typescript/module/use-auth.d.ts.map +1 -1
  94. package/lib/typescript/module/utils/logger.d.ts +4 -4
  95. package/lib/typescript/module/utils/logger.d.ts.map +1 -1
  96. package/nitrogen/generated/android/NitroAuth+autolinking.cmake +0 -1
  97. package/nitrogen/generated/shared/c++/AuthTokens.hpp +5 -1
  98. package/nitrogen/generated/shared/c++/AuthUser.hpp +5 -1
  99. package/nitrogen/generated/shared/c++/HybridAuthSpec.cpp +0 -1
  100. package/nitrogen/generated/shared/c++/HybridAuthSpec.hpp +0 -5
  101. package/package.json +11 -8
  102. package/react-native-nitro-auth.podspec +1 -1
  103. package/src/Auth.nitro.ts +4 -3
  104. package/src/Auth.web.ts +700 -246
  105. package/src/global.d.ts +0 -1
  106. package/src/index.ts +1 -2
  107. package/src/index.web.ts +0 -1
  108. package/src/js-storage-adapter.ts +5 -0
  109. package/src/service.ts +13 -106
  110. package/src/service.web.ts +0 -7
  111. package/src/ui/social-button.tsx +66 -43
  112. package/src/ui/social-button.web.tsx +67 -44
  113. package/src/use-auth.ts +116 -72
  114. package/src/utils/logger.ts +12 -4
  115. package/ios/KeychainStore.swift +0 -43
  116. package/lib/commonjs/AuthStorage.nitro.js +0 -6
  117. package/lib/commonjs/AuthStorage.nitro.js.map +0 -1
  118. package/lib/module/AuthStorage.nitro.js +0 -4
  119. package/lib/module/AuthStorage.nitro.js.map +0 -1
  120. package/lib/typescript/commonjs/AuthStorage.nitro.d.ts +0 -26
  121. package/lib/typescript/commonjs/AuthStorage.nitro.d.ts.map +0 -1
  122. package/lib/typescript/module/AuthStorage.nitro.d.ts +0 -26
  123. package/lib/typescript/module/AuthStorage.nitro.d.ts.map +0 -1
  124. package/nitrogen/generated/shared/c++/HybridAuthStorageAdapterSpec.cpp +0 -23
  125. package/nitrogen/generated/shared/c++/HybridAuthStorageAdapterSpec.hpp +0 -65
  126. package/src/AuthStorage.nitro.ts +0 -26
package/src/Auth.web.ts CHANGED
@@ -5,13 +5,31 @@ import type {
5
5
  LoginOptions,
6
6
  AuthTokens,
7
7
  } from "./Auth.nitro";
8
- import type { AuthStorageAdapter } from "./AuthStorage.nitro";
8
+ import type { JSStorageAdapter } from "./js-storage-adapter";
9
9
  import { logger } from "./utils/logger";
10
10
 
11
11
  const CACHE_KEY = "nitro_auth_user";
12
12
  const SCOPES_KEY = "nitro_auth_scopes";
13
+ const MS_REFRESH_TOKEN_KEY = "nitro_auth_microsoft_refresh_token";
13
14
  const DEFAULT_SCOPES = ["openid", "email", "profile"];
14
15
  const MS_DEFAULT_SCOPES = ["openid", "email", "profile", "User.Read"];
16
+ const STORAGE_MODE_SESSION = "session";
17
+ const STORAGE_MODE_LOCAL = "local";
18
+ const STORAGE_MODE_MEMORY = "memory";
19
+ const POPUP_POLL_INTERVAL_MS = 100;
20
+ const POPUP_TIMEOUT_MS = 120000;
21
+ const WEB_STORAGE_MODES = new Set([
22
+ STORAGE_MODE_SESSION,
23
+ STORAGE_MODE_LOCAL,
24
+ STORAGE_MODE_MEMORY,
25
+ ] as const);
26
+ const inMemoryWebStorage = new Map<string, string>();
27
+
28
+ type WebStorageDriver = {
29
+ save(key: string, value: string): void;
30
+ load(key: string): string | undefined;
31
+ remove(key: string): void;
32
+ };
15
33
 
16
34
  type AppleAuthResponse = {
17
35
  authorization: {
@@ -26,58 +44,364 @@ type AppleAuthResponse = {
26
44
  };
27
45
  };
28
46
 
29
- const getConfig = () => {
47
+ type AuthWebExtraConfig = {
48
+ googleWebClientId?: string;
49
+ microsoftClientId?: string;
50
+ microsoftTenant?: string;
51
+ microsoftB2cDomain?: string;
52
+ appleWebClientId?: string;
53
+ nitroAuthWebStorage?: "session" | "local" | "memory";
54
+ nitroAuthPersistTokensOnWeb?: boolean;
55
+ };
56
+
57
+ type JsonObject = Record<string, unknown>;
58
+
59
+ class AuthWebError extends Error {
60
+ public readonly underlyingError?: string;
61
+
62
+ constructor(message: string, underlyingError?: string) {
63
+ super(message);
64
+ this.name = "AuthWebError";
65
+ this.underlyingError = underlyingError;
66
+ }
67
+ }
68
+
69
+ const isJsonObject = (value: unknown): value is JsonObject =>
70
+ typeof value === "object" && value !== null;
71
+
72
+ const isAuthProvider = (value: unknown): value is AuthProvider =>
73
+ value === "google" || value === "apple" || value === "microsoft";
74
+
75
+ const getOptionalString = (
76
+ source: JsonObject,
77
+ key: string,
78
+ ): string | undefined => {
79
+ const candidate = source[key];
80
+ return typeof candidate === "string" ? candidate : undefined;
81
+ };
82
+
83
+ const getOptionalNumber = (
84
+ source: JsonObject,
85
+ key: string,
86
+ ): number | undefined => {
87
+ const candidate = source[key];
88
+ return typeof candidate === "number" ? candidate : undefined;
89
+ };
90
+
91
+ const parseAuthUser = (value: unknown): AuthUser | undefined => {
92
+ if (!isJsonObject(value) || !isAuthProvider(value.provider)) {
93
+ return undefined;
94
+ }
95
+
96
+ const scopesCandidate = value.scopes;
97
+ const scopes = Array.isArray(scopesCandidate)
98
+ ? scopesCandidate.filter(
99
+ (scope): scope is string => typeof scope === "string",
100
+ )
101
+ : undefined;
102
+
103
+ return {
104
+ provider: value.provider,
105
+ email: getOptionalString(value, "email"),
106
+ name: getOptionalString(value, "name"),
107
+ photo: getOptionalString(value, "photo"),
108
+ idToken: getOptionalString(value, "idToken"),
109
+ accessToken: getOptionalString(value, "accessToken"),
110
+ refreshToken: getOptionalString(value, "refreshToken"),
111
+ serverAuthCode: getOptionalString(value, "serverAuthCode"),
112
+ scopes,
113
+ expirationTime: getOptionalNumber(value, "expirationTime"),
114
+ underlyingError: getOptionalString(value, "underlyingError"),
115
+ };
116
+ };
117
+
118
+ const parseScopes = (value: unknown): string[] | undefined => {
119
+ if (!Array.isArray(value)) {
120
+ return undefined;
121
+ }
122
+
123
+ return value.filter((scope): scope is string => typeof scope === "string");
124
+ };
125
+
126
+ const parseAuthWebExtraConfig = (value: unknown): AuthWebExtraConfig => {
127
+ if (!isJsonObject(value)) {
128
+ return {};
129
+ }
130
+
131
+ const nitroAuthWebStorageCandidate = value.nitroAuthWebStorage;
132
+ const nitroAuthWebStorage =
133
+ nitroAuthWebStorageCandidate === STORAGE_MODE_SESSION ||
134
+ nitroAuthWebStorageCandidate === STORAGE_MODE_LOCAL ||
135
+ nitroAuthWebStorageCandidate === STORAGE_MODE_MEMORY
136
+ ? nitroAuthWebStorageCandidate
137
+ : undefined;
138
+
139
+ return {
140
+ googleWebClientId: getOptionalString(value, "googleWebClientId"),
141
+ microsoftClientId: getOptionalString(value, "microsoftClientId"),
142
+ microsoftTenant: getOptionalString(value, "microsoftTenant"),
143
+ microsoftB2cDomain: getOptionalString(value, "microsoftB2cDomain"),
144
+ appleWebClientId: getOptionalString(value, "appleWebClientId"),
145
+ nitroAuthWebStorage,
146
+ nitroAuthPersistTokensOnWeb:
147
+ typeof value.nitroAuthPersistTokensOnWeb === "boolean"
148
+ ? value.nitroAuthPersistTokensOnWeb
149
+ : undefined,
150
+ };
151
+ };
152
+
153
+ const getConfig = (): AuthWebExtraConfig => {
30
154
  try {
31
155
  const Constants = require("expo-constants").default;
32
- return Constants.expoConfig?.extra || {};
33
- } catch {
156
+ return parseAuthWebExtraConfig(Constants.expoConfig?.extra);
157
+ } catch (error) {
158
+ logger.debug(
159
+ "expo-constants unavailable on web, falling back to defaults",
160
+ {
161
+ error: String(error),
162
+ },
163
+ );
34
164
  return {};
35
165
  }
36
166
  };
37
167
 
38
168
  class AuthWeb implements Auth {
169
+ private readonly _config: AuthWebExtraConfig;
39
170
  private _currentUser: AuthUser | undefined;
40
171
  private _grantedScopes: string[] = [];
41
172
  private _listeners: ((user: AuthUser | undefined) => void)[] = [];
42
173
  private _tokenListeners: ((tokens: AuthTokens) => void)[] = [];
43
- private _storageAdapter: AuthStorageAdapter | undefined;
174
+ private _storageAdapter: WebStorageDriver | undefined;
175
+ private _browserStorageResolved = false;
176
+ private _browserStorageCache: Storage | undefined;
177
+ private _refreshPromise: Promise<AuthTokens> | undefined;
178
+ private _appleSdkLoadPromise: Promise<void> | undefined;
44
179
 
45
180
  constructor() {
181
+ this._config = getConfig();
46
182
  this.loadFromCache();
47
183
  }
48
184
 
185
+ private isPromiseLike(value: unknown): value is PromiseLike<unknown> {
186
+ if (!isJsonObject(value)) {
187
+ return false;
188
+ }
189
+ return typeof value.then === "function";
190
+ }
191
+
192
+ private createWebStorageDriver(adapter: JSStorageAdapter): WebStorageDriver {
193
+ return {
194
+ save: (key, value) => {
195
+ const result = adapter.save(key, value);
196
+ if (this.isPromiseLike(result)) {
197
+ throw new Error("On web, JSStorageAdapter.save must be synchronous.");
198
+ }
199
+ },
200
+ load: (key) => {
201
+ const result = adapter.load(key);
202
+ if (this.isPromiseLike(result)) {
203
+ throw new Error("On web, JSStorageAdapter.load must be synchronous.");
204
+ }
205
+ return result;
206
+ },
207
+ remove: (key) => {
208
+ const result = adapter.remove(key);
209
+ if (this.isPromiseLike(result)) {
210
+ throw new Error(
211
+ "On web, JSStorageAdapter.remove must be synchronous.",
212
+ );
213
+ }
214
+ },
215
+ };
216
+ }
217
+
218
+ private shouldPersistTokensInStorage(): boolean {
219
+ if (this._storageAdapter) {
220
+ return true;
221
+ }
222
+ return this._config.nitroAuthPersistTokensOnWeb === true;
223
+ }
224
+
225
+ private getWebStorageMode(): "session" | "local" | "memory" {
226
+ const configuredMode = this._config.nitroAuthWebStorage;
227
+ if (configuredMode && WEB_STORAGE_MODES.has(configuredMode)) {
228
+ return configuredMode;
229
+ }
230
+ return STORAGE_MODE_SESSION;
231
+ }
232
+
233
+ private getBrowserStorage(): Storage | undefined {
234
+ if (this._browserStorageResolved) {
235
+ return this._browserStorageCache;
236
+ }
237
+
238
+ this._browserStorageResolved = true;
239
+ if (typeof window === "undefined") {
240
+ this._browserStorageCache = undefined;
241
+ return undefined;
242
+ }
243
+
244
+ const mode = this.getWebStorageMode();
245
+ if (mode === STORAGE_MODE_MEMORY) {
246
+ this._browserStorageCache = undefined;
247
+ return undefined;
248
+ }
249
+
250
+ const storage =
251
+ mode === STORAGE_MODE_LOCAL ? window.localStorage : window.sessionStorage;
252
+ try {
253
+ const testKey = "__nitro_auth_storage_probe__";
254
+ storage.setItem(testKey, "1");
255
+ storage.removeItem(testKey);
256
+ this._browserStorageCache = storage;
257
+ return storage;
258
+ } catch (error) {
259
+ logger.warn(
260
+ "Configured web storage is unavailable; using in-memory fallback",
261
+ {
262
+ mode,
263
+ error: String(error),
264
+ },
265
+ );
266
+ this._browserStorageCache = undefined;
267
+ return undefined;
268
+ }
269
+ }
270
+
271
+ private saveValue(key: string, value: string): void {
272
+ if (this._storageAdapter) {
273
+ this._storageAdapter.save(key, value);
274
+ return;
275
+ }
276
+
277
+ const storage = this.getBrowserStorage();
278
+ if (storage) {
279
+ storage.setItem(key, value);
280
+ return;
281
+ }
282
+ inMemoryWebStorage.set(key, value);
283
+ }
284
+
285
+ private loadValue(key: string): string | undefined {
286
+ if (this._storageAdapter) {
287
+ return this._storageAdapter.load(key);
288
+ }
289
+
290
+ const storage = this.getBrowserStorage();
291
+ if (storage) {
292
+ return storage.getItem(key) ?? undefined;
293
+ }
294
+ return inMemoryWebStorage.get(key);
295
+ }
296
+
297
+ private removeValue(key: string): void {
298
+ if (this._storageAdapter) {
299
+ this._storageAdapter.remove(key);
300
+ return;
301
+ }
302
+
303
+ const storage = this.getBrowserStorage();
304
+ if (storage) {
305
+ storage.removeItem(key);
306
+ }
307
+ inMemoryWebStorage.delete(key);
308
+ }
309
+
310
+ private removePersistedBrowserValue(key: string): void {
311
+ if (typeof window === "undefined") {
312
+ return;
313
+ }
314
+
315
+ try {
316
+ window.localStorage.removeItem(key);
317
+ window.sessionStorage.removeItem(key);
318
+ } catch (error) {
319
+ logger.debug("Failed to clear persisted browser value", {
320
+ key,
321
+ error: String(error),
322
+ });
323
+ }
324
+ }
325
+
326
+ private sanitizeUserForPersistence(user: AuthUser): AuthUser {
327
+ if (this.shouldPersistTokensInStorage()) {
328
+ return user;
329
+ }
330
+
331
+ const safeUser = { ...user };
332
+ delete safeUser.accessToken;
333
+ delete safeUser.idToken;
334
+ delete safeUser.serverAuthCode;
335
+ return safeUser;
336
+ }
337
+
338
+ private saveRefreshToken(refreshToken: string): void {
339
+ if (this._storageAdapter || this.shouldPersistTokensInStorage()) {
340
+ this.saveValue(MS_REFRESH_TOKEN_KEY, refreshToken);
341
+ return;
342
+ }
343
+
344
+ // Security-first default: keep refresh tokens in-memory only on web.
345
+ inMemoryWebStorage.set(MS_REFRESH_TOKEN_KEY, refreshToken);
346
+ }
347
+
348
+ private loadRefreshToken(): string | undefined {
349
+ if (this._storageAdapter || this.shouldPersistTokensInStorage()) {
350
+ return this.loadValue(MS_REFRESH_TOKEN_KEY);
351
+ }
352
+ return inMemoryWebStorage.get(MS_REFRESH_TOKEN_KEY);
353
+ }
354
+
49
355
  private loadFromCache() {
50
- const cached = this._storageAdapter
51
- ? this._storageAdapter.load(CACHE_KEY)
52
- : localStorage.getItem(CACHE_KEY);
356
+ const cached = this.loadValue(CACHE_KEY);
53
357
 
54
358
  if (cached) {
55
359
  try {
56
- this._currentUser = JSON.parse(cached);
57
- } catch {
360
+ const parsedUser = parseAuthUser(JSON.parse(cached));
361
+ if (!parsedUser) {
362
+ throw new Error("Expected cached auth user to be a valid AuthUser");
363
+ }
364
+ if (this.shouldPersistTokensInStorage()) {
365
+ this._currentUser = parsedUser;
366
+ } else {
367
+ const safeUser = { ...parsedUser };
368
+ delete safeUser.accessToken;
369
+ delete safeUser.idToken;
370
+ delete safeUser.serverAuthCode;
371
+ this._currentUser = safeUser;
372
+ }
373
+ } catch (error) {
374
+ logger.warn("Failed to parse cached auth user; clearing cache", {
375
+ error: String(error),
376
+ });
58
377
  this.removeFromCache(CACHE_KEY);
59
378
  }
60
379
  }
61
380
 
62
- const scopes = this._storageAdapter
63
- ? this._storageAdapter.load(SCOPES_KEY)
64
- : localStorage.getItem(SCOPES_KEY);
381
+ const scopes = this.loadValue(SCOPES_KEY);
65
382
 
66
383
  if (scopes) {
67
384
  try {
68
- this._grantedScopes = JSON.parse(scopes);
69
- } catch {
385
+ const parsedScopes = parseScopes(JSON.parse(scopes));
386
+ if (!parsedScopes) {
387
+ throw new Error("Expected cached scopes to be an array");
388
+ }
389
+ this._grantedScopes = parsedScopes;
390
+ } catch (error) {
391
+ logger.warn("Failed to parse cached scopes; clearing cache", {
392
+ error: String(error),
393
+ });
70
394
  this.removeFromCache(SCOPES_KEY);
71
395
  }
72
396
  }
397
+
398
+ if (!this.shouldPersistTokensInStorage() && !this._storageAdapter) {
399
+ this.removePersistedBrowserValue(MS_REFRESH_TOKEN_KEY);
400
+ }
73
401
  }
74
402
 
75
403
  private removeFromCache(key: string) {
76
- if (this._storageAdapter) {
77
- this._storageAdapter.remove(key);
78
- } else {
79
- localStorage.removeItem(key);
80
- }
404
+ this.removeValue(key);
81
405
  }
82
406
 
83
407
  get currentUser(): AuthUser | undefined {
@@ -110,7 +434,9 @@ class AuthWeb implements Auth {
110
434
  }
111
435
 
112
436
  private notify() {
113
- this._listeners.forEach((l) => l(this._currentUser));
437
+ this._listeners.forEach((l) => {
438
+ l(this._currentUser);
439
+ });
114
440
  }
115
441
 
116
442
  async login(provider: AuthProvider, options?: LoginOptions): Promise<void> {
@@ -169,14 +495,7 @@ class AuthWeb implements Auth {
169
495
  this._grantedScopes = this._grantedScopes.filter(
170
496
  (s) => !scopes.includes(s),
171
497
  );
172
- if (this._storageAdapter) {
173
- this._storageAdapter.save(
174
- SCOPES_KEY,
175
- JSON.stringify(this._grantedScopes),
176
- );
177
- } else {
178
- localStorage.setItem(SCOPES_KEY, JSON.stringify(this._grantedScopes));
179
- }
498
+ this.saveValue(SCOPES_KEY, JSON.stringify(this._grantedScopes));
180
499
  if (this._currentUser) {
181
500
  this._currentUser.scopes = this._grantedScopes;
182
501
  this.updateUser(this._currentUser);
@@ -194,25 +513,43 @@ class AuthWeb implements Auth {
194
513
  return this._currentUser?.accessToken;
195
514
  }
196
515
 
197
- async refreshToken(): Promise<{ accessToken?: string; idToken?: string }> {
516
+ async refreshToken(): Promise<AuthTokens> {
517
+ if (this._refreshPromise) {
518
+ return this._refreshPromise;
519
+ }
520
+
521
+ const refreshPromise = this.performRefreshToken();
522
+ this._refreshPromise = refreshPromise;
523
+ try {
524
+ return await refreshPromise;
525
+ } finally {
526
+ if (this._refreshPromise === refreshPromise) {
527
+ this._refreshPromise = undefined;
528
+ }
529
+ }
530
+ }
531
+
532
+ private async performRefreshToken(): Promise<AuthTokens> {
198
533
  if (!this._currentUser) {
199
534
  throw new Error("No user logged in");
200
535
  }
201
536
 
202
537
  if (this._currentUser.provider === "microsoft") {
203
538
  logger.log("Refreshing Microsoft tokens...");
204
- const refreshToken = this._storageAdapter
205
- ? this._storageAdapter.load("microsoft_refresh_token")
206
- : localStorage.getItem("nitro_auth_microsoft_refresh_token");
539
+ const refreshToken = this.loadRefreshToken();
207
540
 
208
541
  if (!refreshToken) {
209
542
  throw new Error("No refresh token available");
210
543
  }
211
544
 
212
- const config = getConfig();
213
- const clientId = config.microsoftClientId;
214
- const tenant = config.microsoftTenant ?? "common";
215
- const b2cDomain = config.microsoftB2cDomain;
545
+ const clientId = this._config.microsoftClientId;
546
+ if (!clientId) {
547
+ throw new Error(
548
+ "Microsoft Client ID not configured. Add 'microsoftClientId' to expo.extra in your app.config.js",
549
+ );
550
+ }
551
+ const tenant = this._config.microsoftTenant ?? "common";
552
+ const b2cDomain = this._config.microsoftB2cDomain;
216
553
 
217
554
  const authBaseUrl = this.getMicrosoftAuthBaseUrl(tenant, b2cDomain);
218
555
  const tokenUrl = `${authBaseUrl}oauth2/v2.0/token`;
@@ -230,43 +567,52 @@ class AuthWeb implements Auth {
230
567
  body: body.toString(),
231
568
  });
232
569
 
233
- const json = await response.json();
570
+ const json = await this.parseResponseObject(response);
234
571
  if (!response.ok) {
235
572
  throw new Error(
236
- json.error_description ?? json.error ?? "Token refresh failed",
573
+ getOptionalString(json, "error_description") ??
574
+ getOptionalString(json, "error") ??
575
+ "Token refresh failed",
237
576
  );
238
577
  }
239
578
 
240
- const idToken = json.id_token;
241
- const accessToken = json.access_token;
242
- const newRefreshToken = json.refresh_token;
243
- const expiresIn = json.expires_in;
579
+ const idToken = getOptionalString(json, "id_token");
580
+ const accessToken = getOptionalString(json, "access_token");
581
+ const newRefreshToken = getOptionalString(json, "refresh_token");
582
+ const expiresInSeconds = getOptionalNumber(json, "expires_in");
244
583
 
245
584
  if (newRefreshToken) {
246
- if (this._storageAdapter) {
247
- this._storageAdapter.save("microsoft_refresh_token", newRefreshToken);
248
- } else {
249
- localStorage.setItem(
250
- "nitro_auth_microsoft_refresh_token",
251
- newRefreshToken,
252
- );
253
- }
585
+ this.saveRefreshToken(newRefreshToken);
254
586
  }
255
587
 
256
- const claims = this.decodeMicrosoftJwt(idToken);
588
+ const expirationTime =
589
+ typeof expiresInSeconds === "number"
590
+ ? Date.now() + expiresInSeconds * 1000
591
+ : undefined;
592
+
593
+ const effectiveIdToken = idToken ?? this._currentUser.idToken;
594
+ const claims = effectiveIdToken
595
+ ? this.decodeMicrosoftJwt(effectiveIdToken)
596
+ : {};
257
597
  const user: AuthUser = {
258
598
  ...this._currentUser,
259
- idToken,
599
+ idToken: effectiveIdToken,
260
600
  accessToken: accessToken ?? undefined,
261
- expirationTime: expiresIn
262
- ? Date.now() + parseInt(expiresIn) * 1000
263
- : undefined,
601
+ refreshToken: newRefreshToken ?? this._currentUser.refreshToken,
602
+ expirationTime,
264
603
  ...claims,
265
604
  };
266
605
  this.updateUser(user);
267
606
 
268
- const tokens = { accessToken, idToken };
269
- this._tokenListeners.forEach((l) => l(tokens));
607
+ const tokens: AuthTokens = {
608
+ accessToken: accessToken ?? undefined,
609
+ idToken: effectiveIdToken,
610
+ refreshToken: newRefreshToken ?? undefined,
611
+ expirationTime,
612
+ };
613
+ this._tokenListeners.forEach((l) => {
614
+ l(tokens);
615
+ });
270
616
  return tokens;
271
617
  }
272
618
 
@@ -280,11 +626,15 @@ class AuthWeb implements Auth {
280
626
  await this.loginGoogle(
281
627
  this._grantedScopes.length > 0 ? this._grantedScopes : DEFAULT_SCOPES,
282
628
  );
283
- const tokens = {
629
+ const tokens: AuthTokens = {
284
630
  accessToken: this._currentUser.accessToken,
285
631
  idToken: this._currentUser.idToken,
632
+ refreshToken: this._currentUser.refreshToken,
633
+ expirationTime: this._currentUser.expirationTime,
286
634
  };
287
- this._tokenListeners.forEach((l) => l(tokens));
635
+ this._tokenListeners.forEach((l) => {
636
+ l(tokens);
637
+ });
288
638
  return tokens;
289
639
  }
290
640
 
@@ -295,21 +645,109 @@ class AuthWeb implements Auth {
295
645
 
296
646
  if (msg.includes("cancel") || msg.includes("popup_closed")) {
297
647
  mappedMsg = "cancelled";
648
+ } else if (msg.includes("timeout")) {
649
+ mappedMsg = "timeout";
650
+ } else if (msg.includes("popup blocked")) {
651
+ mappedMsg = "popup_blocked";
298
652
  } else if (msg.includes("network")) {
299
653
  mappedMsg = "network_error";
300
654
  } else if (msg.includes("client id") || msg.includes("config")) {
301
655
  mappedMsg = "configuration_error";
302
656
  }
303
657
 
304
- return Object.assign(new Error(mappedMsg), { underlyingError: rawMessage });
658
+ return new AuthWebError(mappedMsg, rawMessage);
659
+ }
660
+
661
+ private async parseResponseObject(response: Response): Promise<JsonObject> {
662
+ const parsed: unknown = await response.json();
663
+ if (!isJsonObject(parsed)) {
664
+ throw new Error("Expected JSON object response from auth provider");
665
+ }
666
+ return parsed;
667
+ }
668
+
669
+ private parseJwtPayload(token: string): JsonObject {
670
+ const payload = token.split(".")[1];
671
+ if (!payload) {
672
+ throw new Error("Invalid JWT payload");
673
+ }
674
+
675
+ const normalizedPayload = payload.replace(/-/g, "+").replace(/_/g, "/");
676
+ const padding = "=".repeat((4 - (normalizedPayload.length % 4)) % 4);
677
+ const decoded: unknown = JSON.parse(atob(`${normalizedPayload}${padding}`));
678
+ if (!isJsonObject(decoded)) {
679
+ throw new Error("Expected JWT payload to be an object");
680
+ }
681
+ return decoded;
682
+ }
683
+
684
+ private waitForPopupRedirect(
685
+ popup: Window,
686
+ redirectUri: string,
687
+ provider: "Google" | "Microsoft",
688
+ onRedirect: (url: string) => Promise<void> | void,
689
+ ): Promise<void> {
690
+ return new Promise((resolve, reject) => {
691
+ let crossOriginLogShown = false;
692
+
693
+ const cleanup = (
694
+ intervalId: number,
695
+ timeoutId: number,
696
+ shouldClosePopup: boolean,
697
+ ) => {
698
+ window.clearInterval(intervalId);
699
+ window.clearTimeout(timeoutId);
700
+ if (shouldClosePopup && !popup.closed) {
701
+ popup.close();
702
+ }
703
+ };
704
+
705
+ const timeoutId = window.setTimeout(() => {
706
+ cleanup(intervalId, timeoutId, true);
707
+ reject(new Error(`${provider.toLowerCase()}_auth_timeout`));
708
+ }, POPUP_TIMEOUT_MS);
709
+
710
+ const intervalId = window.setInterval(() => {
711
+ if (popup.closed) {
712
+ cleanup(intervalId, timeoutId, false);
713
+ reject(new Error("cancelled"));
714
+ return;
715
+ }
716
+
717
+ let url: string;
718
+ try {
719
+ url = popup.location.href;
720
+ } catch (error) {
721
+ if (!crossOriginLogShown) {
722
+ logger.debug(`Waiting for ${provider} auth redirect`, {
723
+ error: String(error),
724
+ });
725
+ crossOriginLogShown = true;
726
+ }
727
+ return;
728
+ }
729
+
730
+ if (!url.startsWith(redirectUri)) {
731
+ return;
732
+ }
733
+
734
+ cleanup(intervalId, timeoutId, true);
735
+ void Promise.resolve(onRedirect(url))
736
+ .then(() => {
737
+ resolve();
738
+ })
739
+ .catch((error: unknown) => {
740
+ reject(error);
741
+ });
742
+ }, POPUP_POLL_INTERVAL_MS);
743
+ });
305
744
  }
306
745
 
307
746
  private async loginGoogle(
308
747
  scopes: string[],
309
748
  loginHint?: string,
310
749
  ): Promise<void> {
311
- const config = getConfig();
312
- const clientId = config.googleWebClientId;
750
+ const clientId = this._config.googleWebClientId;
313
751
 
314
752
  if (!clientId) {
315
753
  throw new Error(
@@ -324,7 +762,7 @@ class AuthWeb implements Auth {
324
762
  authUrl.searchParams.set("redirect_uri", redirectUri);
325
763
  authUrl.searchParams.set("response_type", "id_token token code");
326
764
  authUrl.searchParams.set("scope", scopes.join(" "));
327
- authUrl.searchParams.set("nonce", Math.random().toString(36).slice(2));
765
+ authUrl.searchParams.set("nonce", crypto.randomUUID());
328
766
  authUrl.searchParams.set("access_type", "offline");
329
767
  authUrl.searchParams.set("prompt", "consent");
330
768
 
@@ -348,66 +786,53 @@ class AuthWeb implements Auth {
348
786
  return;
349
787
  }
350
788
 
351
- const checkInterval = setInterval(() => {
352
- try {
353
- if (popup.closed) {
354
- clearInterval(checkInterval);
355
- reject(new Error("cancelled"));
356
- return;
357
- }
789
+ void this.waitForPopupRedirect(popup, redirectUri, "Google", (url) => {
790
+ const hash = new URL(url).hash.slice(1);
791
+ const params = new URLSearchParams(hash);
792
+ const idToken = params.get("id_token");
793
+ const accessToken = params.get("access_token");
794
+ const expiresIn = params.get("expires_in");
795
+ const code = params.get("code");
358
796
 
359
- const url = popup.location.href;
360
- if (url.startsWith(redirectUri)) {
361
- clearInterval(checkInterval);
362
- popup.close();
363
-
364
- const hash = new URL(url).hash.slice(1);
365
- const params = new URLSearchParams(hash);
366
- const idToken = params.get("id_token");
367
- const accessToken = params.get("access_token");
368
- const expiresIn = params.get("expires_in");
369
- const code = params.get("code");
370
-
371
- if (idToken) {
372
- this._grantedScopes = scopes;
373
- if (this._storageAdapter) {
374
- this._storageAdapter.save(SCOPES_KEY, JSON.stringify(scopes));
375
- } else {
376
- localStorage.setItem(SCOPES_KEY, JSON.stringify(scopes));
377
- }
378
-
379
- const user: AuthUser = {
380
- provider: "google",
381
- idToken,
382
- accessToken: accessToken ?? undefined,
383
- serverAuthCode: code ?? undefined,
384
- scopes,
385
- expirationTime: expiresIn
386
- ? Date.now() + parseInt(expiresIn) * 1000
387
- : undefined,
388
- ...this.decodeGoogleJwt(idToken),
389
- };
390
- this.updateUser(user);
391
- resolve();
392
- } else {
393
- reject(new Error("No id_token in response"));
394
- }
395
- }
396
- } catch {}
397
- }, 100);
797
+ if (!idToken) {
798
+ throw new Error("No id_token in response");
799
+ }
800
+
801
+ this._grantedScopes = scopes;
802
+ this.saveValue(SCOPES_KEY, JSON.stringify(scopes));
803
+
804
+ const user: AuthUser = {
805
+ provider: "google",
806
+ idToken,
807
+ accessToken: accessToken ?? undefined,
808
+ serverAuthCode: code ?? undefined,
809
+ scopes,
810
+ expirationTime: expiresIn
811
+ ? Date.now() + parseInt(expiresIn, 10) * 1000
812
+ : undefined,
813
+ ...this.decodeGoogleJwt(idToken),
814
+ };
815
+ this.updateUser(user);
816
+ })
817
+ .then(() => {
818
+ resolve();
819
+ })
820
+ .catch((error: unknown) => {
821
+ reject(error);
822
+ });
398
823
  });
399
824
  }
400
825
 
401
826
  private decodeGoogleJwt(token: string): Partial<AuthUser> {
402
827
  try {
403
- const payload = token.split(".")[1];
404
- const decoded = JSON.parse(atob(payload));
828
+ const decoded = this.parseJwtPayload(token);
405
829
  return {
406
- email: decoded.email,
407
- name: decoded.name,
408
- photo: decoded.picture,
830
+ email: getOptionalString(decoded, "email"),
831
+ name: getOptionalString(decoded, "name"),
832
+ photo: getOptionalString(decoded, "picture"),
409
833
  };
410
- } catch {
834
+ } catch (error) {
835
+ logger.warn("Failed to decode Google ID token", { error: String(error) });
411
836
  return {};
412
837
  }
413
838
  }
@@ -418,8 +843,7 @@ class AuthWeb implements Auth {
418
843
  tenant?: string,
419
844
  prompt?: string,
420
845
  ): Promise<void> {
421
- const config = getConfig();
422
- const clientId = config.microsoftClientId;
846
+ const clientId = this._config.microsoftClientId;
423
847
 
424
848
  if (!clientId) {
425
849
  throw new Error(
@@ -427,8 +851,8 @@ class AuthWeb implements Auth {
427
851
  );
428
852
  }
429
853
 
430
- const effectiveTenant = tenant ?? config.microsoftTenant ?? "common";
431
- const b2cDomain = config.microsoftB2cDomain;
854
+ const effectiveTenant = tenant ?? this._config.microsoftTenant ?? "common";
855
+ const b2cDomain = this._config.microsoftB2cDomain;
432
856
  const authBaseUrl = this.getMicrosoftAuthBaseUrl(
433
857
  effectiveTenant,
434
858
  b2cDomain,
@@ -477,58 +901,46 @@ class AuthWeb implements Auth {
477
901
  return;
478
902
  }
479
903
 
480
- const checkInterval = setInterval(async () => {
481
- try {
482
- if (popup.closed) {
483
- clearInterval(checkInterval);
484
- reject(new Error("cancelled"));
485
- return;
904
+ void this.waitForPopupRedirect(
905
+ popup,
906
+ redirectUri,
907
+ "Microsoft",
908
+ async (url) => {
909
+ const urlObj = new URL(url);
910
+ const code = urlObj.searchParams.get("code");
911
+ const returnedState = urlObj.searchParams.get("state");
912
+ const error = urlObj.searchParams.get("error");
913
+ const errorDescription = urlObj.searchParams.get("error_description");
914
+
915
+ if (error) {
916
+ throw new Error(errorDescription ?? error);
917
+ }
918
+
919
+ if (returnedState !== state) {
920
+ throw new Error("State mismatch - possible CSRF attack");
486
921
  }
487
922
 
488
- const url = popup.location.href;
489
- if (url.startsWith(redirectUri)) {
490
- clearInterval(checkInterval);
491
- popup.close();
492
-
493
- const urlObj = new URL(url);
494
- const code = urlObj.searchParams.get("code");
495
- const returnedState = urlObj.searchParams.get("state");
496
- const error = urlObj.searchParams.get("error");
497
- const errorDescription =
498
- urlObj.searchParams.get("error_description");
499
-
500
- if (error) {
501
- reject(new Error(errorDescription ?? error));
502
- return;
503
- }
504
-
505
- if (returnedState !== state) {
506
- reject(new Error("State mismatch - possible CSRF attack"));
507
- return;
508
- }
509
-
510
- if (!code) {
511
- reject(new Error("No authorization code in response"));
512
- return;
513
- }
514
-
515
- try {
516
- await this.exchangeMicrosoftCodeForTokens(
517
- code,
518
- codeVerifier,
519
- clientId,
520
- redirectUri,
521
- effectiveTenant,
522
- nonce,
523
- effectiveScopes,
524
- );
525
- resolve();
526
- } catch (e) {
527
- reject(e);
528
- }
923
+ if (!code) {
924
+ throw new Error("No authorization code in response");
529
925
  }
530
- } catch {}
531
- }, 100);
926
+
927
+ await this.exchangeMicrosoftCodeForTokens(
928
+ code,
929
+ codeVerifier,
930
+ clientId,
931
+ redirectUri,
932
+ effectiveTenant,
933
+ nonce,
934
+ effectiveScopes,
935
+ );
936
+ },
937
+ )
938
+ .then(() => {
939
+ resolve();
940
+ })
941
+ .catch((error: unknown) => {
942
+ reject(error);
943
+ });
532
944
  });
533
945
  }
534
946
 
@@ -559,10 +971,9 @@ class AuthWeb implements Auth {
559
971
  expectedNonce: string,
560
972
  scopes: string[],
561
973
  ): Promise<void> {
562
- const config = getConfig();
563
974
  const authBaseUrl = this.getMicrosoftAuthBaseUrl(
564
975
  tenant,
565
- config.microsoftB2cDomain,
976
+ this._config.microsoftB2cDomain,
566
977
  );
567
978
  const tokenUrl = `${authBaseUrl}oauth2/v2.0/token`;
568
979
 
@@ -582,55 +993,48 @@ class AuthWeb implements Auth {
582
993
  body: body.toString(),
583
994
  });
584
995
 
585
- const json = await response.json();
996
+ const json = await this.parseResponseObject(response);
586
997
 
587
998
  if (!response.ok) {
588
999
  throw new Error(
589
- json.error_description ?? json.error ?? "Token exchange failed",
1000
+ getOptionalString(json, "error_description") ??
1001
+ getOptionalString(json, "error") ??
1002
+ "Token exchange failed",
590
1003
  );
591
1004
  }
592
1005
 
593
- const idToken = json.id_token;
1006
+ const idToken = getOptionalString(json, "id_token");
594
1007
  if (!idToken) {
595
1008
  throw new Error("No id_token in token response");
596
1009
  }
597
1010
 
598
1011
  const claims = this.decodeMicrosoftJwt(idToken);
599
- const payload = JSON.parse(atob(idToken.split(".")[1]));
600
- if (payload.nonce !== expectedNonce) {
1012
+ const payload = this.parseJwtPayload(idToken);
1013
+ if (getOptionalString(payload, "nonce") !== expectedNonce) {
601
1014
  throw new Error("Nonce mismatch - token may be replayed");
602
1015
  }
603
1016
 
604
- const accessToken = json.access_token;
605
- const refreshToken = json.refresh_token;
606
- const expiresIn = json.expires_in;
1017
+ const accessToken = getOptionalString(json, "access_token");
1018
+ const refreshToken = getOptionalString(json, "refresh_token");
1019
+ const expiresInSeconds = getOptionalNumber(json, "expires_in");
607
1020
 
608
1021
  if (refreshToken) {
609
- if (this._storageAdapter) {
610
- this._storageAdapter.save("microsoft_refresh_token", refreshToken);
611
- } else {
612
- localStorage.setItem(
613
- "nitro_auth_microsoft_refresh_token",
614
- refreshToken,
615
- );
616
- }
1022
+ this.saveRefreshToken(refreshToken);
617
1023
  }
618
1024
 
619
1025
  this._grantedScopes = scopes;
620
- if (this._storageAdapter) {
621
- this._storageAdapter.save(SCOPES_KEY, JSON.stringify(scopes));
622
- } else {
623
- localStorage.setItem(SCOPES_KEY, JSON.stringify(scopes));
624
- }
1026
+ this.saveValue(SCOPES_KEY, JSON.stringify(scopes));
625
1027
 
626
1028
  const user: AuthUser = {
627
1029
  provider: "microsoft",
628
1030
  idToken,
629
1031
  accessToken: accessToken ?? undefined,
1032
+ refreshToken: refreshToken ?? undefined,
630
1033
  scopes,
631
- expirationTime: expiresIn
632
- ? Date.now() + parseInt(expiresIn) * 1000
633
- : undefined,
1034
+ expirationTime:
1035
+ typeof expiresInSeconds === "number"
1036
+ ? Date.now() + expiresInSeconds * 1000
1037
+ : undefined,
634
1038
  ...claims,
635
1039
  };
636
1040
  this.updateUser(user);
@@ -650,62 +1054,115 @@ class AuthWeb implements Auth {
650
1054
 
651
1055
  private decodeMicrosoftJwt(token: string): Partial<AuthUser> {
652
1056
  try {
653
- const payload = token.split(".")[1];
654
- const decoded = JSON.parse(atob(payload));
1057
+ const decoded = this.parseJwtPayload(token);
655
1058
  return {
656
- email: decoded.preferred_username ?? decoded.email,
657
- name: decoded.name,
1059
+ email:
1060
+ getOptionalString(decoded, "preferred_username") ??
1061
+ getOptionalString(decoded, "email"),
1062
+ name: getOptionalString(decoded, "name"),
658
1063
  };
659
- } catch {
1064
+ } catch (error) {
1065
+ logger.warn("Failed to decode Microsoft ID token", {
1066
+ error: String(error),
1067
+ });
660
1068
  return {};
661
1069
  }
662
1070
  }
663
1071
 
664
- private async loginApple(): Promise<void> {
665
- const config = getConfig();
666
- const clientId = config.appleWebClientId;
1072
+ private async ensureAppleSdkLoaded(): Promise<void> {
1073
+ if (window.AppleID) {
1074
+ return;
1075
+ }
667
1076
 
668
- if (!clientId) {
669
- throw new Error(
670
- "Apple Web Client ID not configured. Add 'APPLE_WEB_CLIENT_ID' to your .env file.",
671
- );
1077
+ if (this._appleSdkLoadPromise) {
1078
+ return this._appleSdkLoadPromise;
672
1079
  }
673
1080
 
674
- return new Promise((resolve, reject) => {
1081
+ this._appleSdkLoadPromise = new Promise<void>((resolve, reject) => {
1082
+ const scriptId = "nitro-auth-apple-sdk";
1083
+ const existingScript = document.getElementById(
1084
+ scriptId,
1085
+ ) as HTMLScriptElement | null;
1086
+
1087
+ if (existingScript) {
1088
+ if (window.AppleID) {
1089
+ resolve();
1090
+ return;
1091
+ }
1092
+
1093
+ existingScript.addEventListener(
1094
+ "load",
1095
+ () => {
1096
+ resolve();
1097
+ },
1098
+ {
1099
+ once: true,
1100
+ },
1101
+ );
1102
+ existingScript.addEventListener(
1103
+ "error",
1104
+ () => {
1105
+ this._appleSdkLoadPromise = undefined;
1106
+ reject(new Error("Failed to load Apple SDK"));
1107
+ },
1108
+ { once: true },
1109
+ );
1110
+ return;
1111
+ }
1112
+
675
1113
  const script = document.createElement("script");
1114
+ script.id = scriptId;
676
1115
  script.src =
677
1116
  "https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js";
678
1117
  script.async = true;
679
1118
  script.onload = () => {
680
- if (!window.AppleID) {
681
- reject(new Error("Apple SDK not loaded"));
682
- return;
683
- }
684
- window.AppleID.auth.init({
685
- clientId,
686
- scope: "name email",
687
- redirectURI: window.location.origin,
688
- usePopup: true,
689
- });
690
- window.AppleID.auth
691
- .signIn()
692
- .then((response: AppleAuthResponse) => {
693
- const user: AuthUser = {
694
- provider: "apple",
695
- idToken: response.authorization.id_token,
696
- email: response.user?.email,
697
- name: response.user?.name
698
- ? `${response.user.name.firstName} ${response.user.name.lastName}`.trim()
699
- : undefined,
700
- };
701
- this.updateUser(user);
702
- resolve();
703
- })
704
- .catch((err: unknown) => reject(this.mapError(err)));
1119
+ resolve();
1120
+ };
1121
+ script.onerror = () => {
1122
+ this._appleSdkLoadPromise = undefined;
1123
+ reject(new Error("Failed to load Apple SDK"));
705
1124
  };
706
- script.onerror = () => reject(new Error("Failed to load Apple SDK"));
707
1125
  document.head.appendChild(script);
708
1126
  });
1127
+
1128
+ return this._appleSdkLoadPromise;
1129
+ }
1130
+
1131
+ private async loginApple(): Promise<void> {
1132
+ const clientId = this._config.appleWebClientId;
1133
+
1134
+ if (!clientId) {
1135
+ throw new Error(
1136
+ "Apple Web Client ID not configured. Add 'APPLE_WEB_CLIENT_ID' to your .env file.",
1137
+ );
1138
+ }
1139
+
1140
+ await this.ensureAppleSdkLoaded();
1141
+ if (!window.AppleID) {
1142
+ throw new Error("Apple SDK not loaded");
1143
+ }
1144
+
1145
+ window.AppleID.auth.init({
1146
+ clientId,
1147
+ scope: "name email",
1148
+ redirectURI: window.location.origin,
1149
+ usePopup: true,
1150
+ });
1151
+
1152
+ try {
1153
+ const response: AppleAuthResponse = await window.AppleID.auth.signIn();
1154
+ const user: AuthUser = {
1155
+ provider: "apple",
1156
+ idToken: response.authorization.id_token,
1157
+ email: response.user?.email,
1158
+ name: response.user?.name
1159
+ ? `${response.user.name.firstName} ${response.user.name.lastName}`.trim()
1160
+ : undefined,
1161
+ };
1162
+ this.updateUser(user);
1163
+ } catch (error) {
1164
+ throw this.mapError(error);
1165
+ }
709
1166
  }
710
1167
 
711
1168
  async silentRestore(): Promise<void> {
@@ -727,17 +1184,14 @@ class AuthWeb implements Auth {
727
1184
  this._grantedScopes = [];
728
1185
  this.removeFromCache(CACHE_KEY);
729
1186
  this.removeFromCache(SCOPES_KEY);
730
- this.removeFromCache("microsoft_refresh_token");
1187
+ this.removeFromCache(MS_REFRESH_TOKEN_KEY);
731
1188
  this.notify();
732
1189
  }
733
1190
 
734
1191
  private updateUser(user: AuthUser) {
735
1192
  this._currentUser = user;
736
- if (this._storageAdapter) {
737
- this._storageAdapter.save(CACHE_KEY, JSON.stringify(user));
738
- } else {
739
- localStorage.setItem(CACHE_KEY, JSON.stringify(user));
740
- }
1193
+ const userToPersist = this.sanitizeUserForPersistence(user);
1194
+ this.saveValue(CACHE_KEY, JSON.stringify(userToPersist));
741
1195
  this.notify();
742
1196
  }
743
1197
 
@@ -745,12 +1199,12 @@ class AuthWeb implements Auth {
745
1199
  logger.setEnabled(enabled);
746
1200
  }
747
1201
 
748
- setStorageAdapter(adapter: AuthStorageAdapter | undefined): void {
749
- this._storageAdapter = adapter;
750
- if (adapter) {
751
- this.loadFromCache();
752
- this.notify();
753
- }
1202
+ setWebStorageAdapter(adapter: JSStorageAdapter | undefined): void {
1203
+ this._storageAdapter = adapter
1204
+ ? this.createWebStorageDriver(adapter)
1205
+ : undefined;
1206
+ this.loadFromCache();
1207
+ this.notify();
754
1208
  }
755
1209
 
756
1210
  name = "Auth";