react-native-nitro-auth 0.5.1 → 0.5.3

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 (121) hide show
  1. package/README.md +362 -190
  2. package/android/src/main/java/com/auth/AuthAdapter.kt +55 -169
  3. package/android/src/main/java/com/auth/NitroAuthPackage.kt +1 -1
  4. package/app.plugin.js +2 -9
  5. package/cpp/AuthCache.cpp +12 -102
  6. package/cpp/HybridAuth.cpp +37 -61
  7. package/cpp/HybridAuth.hpp +2 -4
  8. package/ios/AuthAdapter.swift +21 -25
  9. package/lib/commonjs/Auth.web.js +433 -164
  10. package/lib/commonjs/Auth.web.js.map +1 -1
  11. package/lib/commonjs/index.js +0 -12
  12. package/lib/commonjs/index.js.map +1 -1
  13. package/lib/commonjs/index.web.js +0 -12
  14. package/lib/commonjs/index.web.js.map +1 -1
  15. package/lib/commonjs/js-storage-adapter.js +2 -0
  16. package/lib/commonjs/js-storage-adapter.js.map +1 -0
  17. package/lib/commonjs/service.js +7 -84
  18. package/lib/commonjs/service.js.map +1 -1
  19. package/lib/commonjs/service.web.js +1 -5
  20. package/lib/commonjs/service.web.js.map +1 -1
  21. package/lib/commonjs/ui/social-button.js +44 -29
  22. package/lib/commonjs/ui/social-button.js.map +1 -1
  23. package/lib/commonjs/ui/social-button.web.js +44 -29
  24. package/lib/commonjs/ui/social-button.web.js.map +1 -1
  25. package/lib/commonjs/use-auth.js +8 -2
  26. package/lib/commonjs/use-auth.js.map +1 -1
  27. package/lib/commonjs/utils/logger.js +12 -4
  28. package/lib/commonjs/utils/logger.js.map +1 -1
  29. package/lib/module/Auth.web.js +433 -164
  30. package/lib/module/Auth.web.js.map +1 -1
  31. package/lib/module/index.js +0 -1
  32. package/lib/module/index.js.map +1 -1
  33. package/lib/module/index.web.js +0 -1
  34. package/lib/module/index.web.js.map +1 -1
  35. package/lib/module/js-storage-adapter.js +2 -0
  36. package/lib/module/js-storage-adapter.js.map +1 -0
  37. package/lib/module/service.js +7 -84
  38. package/lib/module/service.js.map +1 -1
  39. package/lib/module/service.web.js +1 -5
  40. package/lib/module/service.web.js.map +1 -1
  41. package/lib/module/ui/social-button.js +44 -29
  42. package/lib/module/ui/social-button.js.map +1 -1
  43. package/lib/module/ui/social-button.web.js +44 -29
  44. package/lib/module/ui/social-button.web.js.map +1 -1
  45. package/lib/module/use-auth.js +8 -2
  46. package/lib/module/use-auth.js.map +1 -1
  47. package/lib/module/utils/logger.js +12 -4
  48. package/lib/module/utils/logger.js.map +1 -1
  49. package/lib/typescript/commonjs/Auth.nitro.d.ts +3 -3
  50. package/lib/typescript/commonjs/Auth.nitro.d.ts.map +1 -1
  51. package/lib/typescript/commonjs/Auth.web.d.ts +18 -6
  52. package/lib/typescript/commonjs/Auth.web.d.ts.map +1 -1
  53. package/lib/typescript/commonjs/index.d.ts +1 -2
  54. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  55. package/lib/typescript/commonjs/index.web.d.ts +0 -1
  56. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  57. package/lib/typescript/commonjs/js-storage-adapter.d.ts +6 -0
  58. package/lib/typescript/commonjs/js-storage-adapter.d.ts.map +1 -0
  59. package/lib/typescript/commonjs/service.d.ts +1 -8
  60. package/lib/typescript/commonjs/service.d.ts.map +1 -1
  61. package/lib/typescript/commonjs/service.web.d.ts +1 -8
  62. package/lib/typescript/commonjs/service.web.d.ts.map +1 -1
  63. package/lib/typescript/commonjs/ui/social-button.d.ts +6 -6
  64. package/lib/typescript/commonjs/ui/social-button.d.ts.map +1 -1
  65. package/lib/typescript/commonjs/ui/social-button.web.d.ts +6 -6
  66. package/lib/typescript/commonjs/ui/social-button.web.d.ts.map +1 -1
  67. package/lib/typescript/commonjs/use-auth.d.ts +4 -4
  68. package/lib/typescript/commonjs/use-auth.d.ts.map +1 -1
  69. package/lib/typescript/commonjs/utils/logger.d.ts +4 -4
  70. package/lib/typescript/commonjs/utils/logger.d.ts.map +1 -1
  71. package/lib/typescript/module/Auth.nitro.d.ts +3 -3
  72. package/lib/typescript/module/Auth.nitro.d.ts.map +1 -1
  73. package/lib/typescript/module/Auth.web.d.ts +18 -6
  74. package/lib/typescript/module/Auth.web.d.ts.map +1 -1
  75. package/lib/typescript/module/index.d.ts +1 -2
  76. package/lib/typescript/module/index.d.ts.map +1 -1
  77. package/lib/typescript/module/index.web.d.ts +0 -1
  78. package/lib/typescript/module/index.web.d.ts.map +1 -1
  79. package/lib/typescript/module/js-storage-adapter.d.ts +6 -0
  80. package/lib/typescript/module/js-storage-adapter.d.ts.map +1 -0
  81. package/lib/typescript/module/service.d.ts +1 -8
  82. package/lib/typescript/module/service.d.ts.map +1 -1
  83. package/lib/typescript/module/service.web.d.ts +1 -8
  84. package/lib/typescript/module/service.web.d.ts.map +1 -1
  85. package/lib/typescript/module/ui/social-button.d.ts +6 -6
  86. package/lib/typescript/module/ui/social-button.d.ts.map +1 -1
  87. package/lib/typescript/module/ui/social-button.web.d.ts +6 -6
  88. package/lib/typescript/module/ui/social-button.web.d.ts.map +1 -1
  89. package/lib/typescript/module/use-auth.d.ts +4 -4
  90. package/lib/typescript/module/use-auth.d.ts.map +1 -1
  91. package/lib/typescript/module/utils/logger.d.ts +4 -4
  92. package/lib/typescript/module/utils/logger.d.ts.map +1 -1
  93. package/nitrogen/generated/android/NitroAuth+autolinking.cmake +0 -1
  94. package/nitrogen/generated/shared/c++/AuthTokens.hpp +5 -1
  95. package/nitrogen/generated/shared/c++/AuthUser.hpp +5 -1
  96. package/nitrogen/generated/shared/c++/HybridAuthSpec.cpp +0 -1
  97. package/nitrogen/generated/shared/c++/HybridAuthSpec.hpp +0 -5
  98. package/package.json +11 -8
  99. package/src/Auth.nitro.ts +4 -3
  100. package/src/Auth.web.ts +582 -202
  101. package/src/global.d.ts +0 -1
  102. package/src/index.ts +1 -2
  103. package/src/index.web.ts +0 -1
  104. package/src/js-storage-adapter.ts +5 -0
  105. package/src/service.ts +11 -104
  106. package/src/service.web.ts +0 -7
  107. package/src/ui/social-button.tsx +66 -43
  108. package/src/ui/social-button.web.tsx +67 -44
  109. package/src/use-auth.ts +18 -6
  110. package/src/utils/logger.ts +12 -4
  111. package/lib/commonjs/AuthStorage.nitro.js +0 -6
  112. package/lib/commonjs/AuthStorage.nitro.js.map +0 -1
  113. package/lib/module/AuthStorage.nitro.js +0 -4
  114. package/lib/module/AuthStorage.nitro.js.map +0 -1
  115. package/lib/typescript/commonjs/AuthStorage.nitro.d.ts +0 -26
  116. package/lib/typescript/commonjs/AuthStorage.nitro.d.ts.map +0 -1
  117. package/lib/typescript/module/AuthStorage.nitro.d.ts +0 -26
  118. package/lib/typescript/module/AuthStorage.nitro.d.ts.map +0 -1
  119. package/nitrogen/generated/shared/c++/HybridAuthStorageAdapterSpec.cpp +0 -23
  120. package/nitrogen/generated/shared/c++/HybridAuthStorageAdapterSpec.hpp +0 -65
  121. package/src/AuthStorage.nitro.ts +0 -26
@@ -3,13 +3,83 @@
3
3
  import { logger } from "./utils/logger.js";
4
4
  const CACHE_KEY = "nitro_auth_user";
5
5
  const SCOPES_KEY = "nitro_auth_scopes";
6
+ const MS_REFRESH_TOKEN_KEY = "nitro_auth_microsoft_refresh_token";
6
7
  const DEFAULT_SCOPES = ["openid", "email", "profile"];
7
8
  const MS_DEFAULT_SCOPES = ["openid", "email", "profile", "User.Read"];
9
+ const STORAGE_MODE_SESSION = "session";
10
+ const STORAGE_MODE_LOCAL = "local";
11
+ const STORAGE_MODE_MEMORY = "memory";
12
+ const POPUP_POLL_INTERVAL_MS = 100;
13
+ const POPUP_TIMEOUT_MS = 120000;
14
+ const WEB_STORAGE_MODES = new Set([STORAGE_MODE_SESSION, STORAGE_MODE_LOCAL, STORAGE_MODE_MEMORY]);
15
+ const inMemoryWebStorage = new Map();
16
+ class AuthWebError extends Error {
17
+ constructor(message, underlyingError) {
18
+ super(message);
19
+ this.name = "AuthWebError";
20
+ this.underlyingError = underlyingError;
21
+ }
22
+ }
23
+ const isJsonObject = value => typeof value === "object" && value !== null;
24
+ const isAuthProvider = value => value === "google" || value === "apple" || value === "microsoft";
25
+ const getOptionalString = (source, key) => {
26
+ const candidate = source[key];
27
+ return typeof candidate === "string" ? candidate : undefined;
28
+ };
29
+ const getOptionalNumber = (source, key) => {
30
+ const candidate = source[key];
31
+ return typeof candidate === "number" ? candidate : undefined;
32
+ };
33
+ const parseAuthUser = value => {
34
+ if (!isJsonObject(value) || !isAuthProvider(value.provider)) {
35
+ return undefined;
36
+ }
37
+ const scopesCandidate = value.scopes;
38
+ const scopes = Array.isArray(scopesCandidate) ? scopesCandidate.filter(scope => typeof scope === "string") : undefined;
39
+ return {
40
+ provider: value.provider,
41
+ email: getOptionalString(value, "email"),
42
+ name: getOptionalString(value, "name"),
43
+ photo: getOptionalString(value, "photo"),
44
+ idToken: getOptionalString(value, "idToken"),
45
+ accessToken: getOptionalString(value, "accessToken"),
46
+ refreshToken: getOptionalString(value, "refreshToken"),
47
+ serverAuthCode: getOptionalString(value, "serverAuthCode"),
48
+ scopes,
49
+ expirationTime: getOptionalNumber(value, "expirationTime"),
50
+ underlyingError: getOptionalString(value, "underlyingError")
51
+ };
52
+ };
53
+ const parseScopes = value => {
54
+ if (!Array.isArray(value)) {
55
+ return undefined;
56
+ }
57
+ return value.filter(scope => typeof scope === "string");
58
+ };
59
+ const parseAuthWebExtraConfig = value => {
60
+ if (!isJsonObject(value)) {
61
+ return {};
62
+ }
63
+ const nitroAuthWebStorageCandidate = value.nitroAuthWebStorage;
64
+ const nitroAuthWebStorage = nitroAuthWebStorageCandidate === STORAGE_MODE_SESSION || nitroAuthWebStorageCandidate === STORAGE_MODE_LOCAL || nitroAuthWebStorageCandidate === STORAGE_MODE_MEMORY ? nitroAuthWebStorageCandidate : undefined;
65
+ return {
66
+ googleWebClientId: getOptionalString(value, "googleWebClientId"),
67
+ microsoftClientId: getOptionalString(value, "microsoftClientId"),
68
+ microsoftTenant: getOptionalString(value, "microsoftTenant"),
69
+ microsoftB2cDomain: getOptionalString(value, "microsoftB2cDomain"),
70
+ appleWebClientId: getOptionalString(value, "appleWebClientId"),
71
+ nitroAuthWebStorage,
72
+ nitroAuthPersistTokensOnWeb: typeof value.nitroAuthPersistTokensOnWeb === "boolean" ? value.nitroAuthPersistTokensOnWeb : undefined
73
+ };
74
+ };
8
75
  const getConfig = () => {
9
76
  try {
10
77
  const Constants = require("expo-constants").default;
11
- return Constants.expoConfig?.extra || {};
12
- } catch {
78
+ return parseAuthWebExtraConfig(Constants.expoConfig?.extra);
79
+ } catch (error) {
80
+ logger.debug("expo-constants unavailable on web, falling back to defaults", {
81
+ error: String(error)
82
+ });
13
83
  return {};
14
84
  }
15
85
  };
@@ -20,30 +90,191 @@ class AuthWeb {
20
90
  constructor() {
21
91
  this.loadFromCache();
22
92
  }
93
+ isPromiseLike(value) {
94
+ if (!isJsonObject(value)) {
95
+ return false;
96
+ }
97
+ return typeof value.then === "function";
98
+ }
99
+ createWebStorageDriver(adapter) {
100
+ return {
101
+ save: (key, value) => {
102
+ const result = adapter.save(key, value);
103
+ if (this.isPromiseLike(result)) {
104
+ throw new Error("On web, JSStorageAdapter.save must be synchronous.");
105
+ }
106
+ },
107
+ load: key => {
108
+ const result = adapter.load(key);
109
+ if (this.isPromiseLike(result)) {
110
+ throw new Error("On web, JSStorageAdapter.load must be synchronous.");
111
+ }
112
+ return result;
113
+ },
114
+ remove: key => {
115
+ const result = adapter.remove(key);
116
+ if (this.isPromiseLike(result)) {
117
+ throw new Error("On web, JSStorageAdapter.remove must be synchronous.");
118
+ }
119
+ }
120
+ };
121
+ }
122
+ shouldPersistTokensInStorage() {
123
+ if (this._storageAdapter) {
124
+ return true;
125
+ }
126
+ return getConfig().nitroAuthPersistTokensOnWeb === true;
127
+ }
128
+ getWebStorageMode() {
129
+ const configuredMode = getConfig().nitroAuthWebStorage;
130
+ if (configuredMode && WEB_STORAGE_MODES.has(configuredMode)) {
131
+ return configuredMode;
132
+ }
133
+ return STORAGE_MODE_SESSION;
134
+ }
135
+ getBrowserStorage() {
136
+ if (typeof window === "undefined") {
137
+ return undefined;
138
+ }
139
+ const mode = this.getWebStorageMode();
140
+ if (mode === STORAGE_MODE_MEMORY) {
141
+ return undefined;
142
+ }
143
+ const storage = mode === STORAGE_MODE_LOCAL ? window.localStorage : window.sessionStorage;
144
+ try {
145
+ const testKey = "__nitro_auth_storage_probe__";
146
+ storage.setItem(testKey, "1");
147
+ storage.removeItem(testKey);
148
+ return storage;
149
+ } catch (error) {
150
+ logger.warn("Configured web storage is unavailable; using in-memory fallback", {
151
+ mode,
152
+ error: String(error)
153
+ });
154
+ return undefined;
155
+ }
156
+ }
157
+ saveValue(key, value) {
158
+ if (this._storageAdapter) {
159
+ this._storageAdapter.save(key, value);
160
+ return;
161
+ }
162
+ const storage = this.getBrowserStorage();
163
+ if (storage) {
164
+ storage.setItem(key, value);
165
+ return;
166
+ }
167
+ inMemoryWebStorage.set(key, value);
168
+ }
169
+ loadValue(key) {
170
+ if (this._storageAdapter) {
171
+ return this._storageAdapter.load(key);
172
+ }
173
+ const storage = this.getBrowserStorage();
174
+ if (storage) {
175
+ return storage.getItem(key) ?? undefined;
176
+ }
177
+ return inMemoryWebStorage.get(key);
178
+ }
179
+ removeValue(key) {
180
+ if (this._storageAdapter) {
181
+ this._storageAdapter.remove(key);
182
+ return;
183
+ }
184
+ const storage = this.getBrowserStorage();
185
+ if (storage) {
186
+ storage.removeItem(key);
187
+ }
188
+ inMemoryWebStorage.delete(key);
189
+ }
190
+ removePersistedBrowserValue(key) {
191
+ if (typeof window === "undefined") {
192
+ return;
193
+ }
194
+ try {
195
+ window.localStorage.removeItem(key);
196
+ window.sessionStorage.removeItem(key);
197
+ } catch (error) {
198
+ logger.debug("Failed to clear persisted browser value", {
199
+ key,
200
+ error: String(error)
201
+ });
202
+ }
203
+ }
204
+ sanitizeUserForPersistence(user) {
205
+ if (this.shouldPersistTokensInStorage()) {
206
+ return user;
207
+ }
208
+ const safeUser = {
209
+ ...user
210
+ };
211
+ delete safeUser.accessToken;
212
+ delete safeUser.idToken;
213
+ delete safeUser.serverAuthCode;
214
+ return safeUser;
215
+ }
216
+ saveRefreshToken(refreshToken) {
217
+ if (this._storageAdapter || this.shouldPersistTokensInStorage()) {
218
+ this.saveValue(MS_REFRESH_TOKEN_KEY, refreshToken);
219
+ return;
220
+ }
221
+
222
+ // Security-first default: keep refresh tokens in-memory only on web.
223
+ inMemoryWebStorage.set(MS_REFRESH_TOKEN_KEY, refreshToken);
224
+ }
225
+ loadRefreshToken() {
226
+ if (this._storageAdapter || this.shouldPersistTokensInStorage()) {
227
+ return this.loadValue(MS_REFRESH_TOKEN_KEY);
228
+ }
229
+ return inMemoryWebStorage.get(MS_REFRESH_TOKEN_KEY);
230
+ }
23
231
  loadFromCache() {
24
- const cached = this._storageAdapter ? this._storageAdapter.load(CACHE_KEY) : localStorage.getItem(CACHE_KEY);
232
+ const cached = this.loadValue(CACHE_KEY);
25
233
  if (cached) {
26
234
  try {
27
- this._currentUser = JSON.parse(cached);
28
- } catch {
235
+ const parsedUser = parseAuthUser(JSON.parse(cached));
236
+ if (!parsedUser) {
237
+ throw new Error("Expected cached auth user to be a valid AuthUser");
238
+ }
239
+ if (this.shouldPersistTokensInStorage()) {
240
+ this._currentUser = parsedUser;
241
+ } else {
242
+ const safeUser = {
243
+ ...parsedUser
244
+ };
245
+ delete safeUser.accessToken;
246
+ delete safeUser.idToken;
247
+ delete safeUser.serverAuthCode;
248
+ this._currentUser = safeUser;
249
+ }
250
+ } catch (error) {
251
+ logger.warn("Failed to parse cached auth user; clearing cache", {
252
+ error: String(error)
253
+ });
29
254
  this.removeFromCache(CACHE_KEY);
30
255
  }
31
256
  }
32
- const scopes = this._storageAdapter ? this._storageAdapter.load(SCOPES_KEY) : localStorage.getItem(SCOPES_KEY);
257
+ const scopes = this.loadValue(SCOPES_KEY);
33
258
  if (scopes) {
34
259
  try {
35
- this._grantedScopes = JSON.parse(scopes);
36
- } catch {
260
+ const parsedScopes = parseScopes(JSON.parse(scopes));
261
+ if (!parsedScopes) {
262
+ throw new Error("Expected cached scopes to be an array");
263
+ }
264
+ this._grantedScopes = parsedScopes;
265
+ } catch (error) {
266
+ logger.warn("Failed to parse cached scopes; clearing cache", {
267
+ error: String(error)
268
+ });
37
269
  this.removeFromCache(SCOPES_KEY);
38
270
  }
39
271
  }
272
+ if (!this.shouldPersistTokensInStorage() && !this._storageAdapter) {
273
+ this.removePersistedBrowserValue(MS_REFRESH_TOKEN_KEY);
274
+ }
40
275
  }
41
276
  removeFromCache(key) {
42
- if (this._storageAdapter) {
43
- this._storageAdapter.remove(key);
44
- } else {
45
- localStorage.removeItem(key);
46
- }
277
+ this.removeValue(key);
47
278
  }
48
279
  get currentUser() {
49
280
  return this._currentUser;
@@ -68,7 +299,9 @@ class AuthWeb {
68
299
  };
69
300
  }
70
301
  notify() {
71
- this._listeners.forEach(l => l(this._currentUser));
302
+ this._listeners.forEach(l => {
303
+ l(this._currentUser);
304
+ });
72
305
  }
73
306
  async login(provider, options) {
74
307
  const loginHint = options?.loginHint;
@@ -117,11 +350,7 @@ class AuthWeb {
117
350
  async revokeScopes(scopes) {
118
351
  logger.log("Revoking scopes:", scopes);
119
352
  this._grantedScopes = this._grantedScopes.filter(s => !scopes.includes(s));
120
- if (this._storageAdapter) {
121
- this._storageAdapter.save(SCOPES_KEY, JSON.stringify(this._grantedScopes));
122
- } else {
123
- localStorage.setItem(SCOPES_KEY, JSON.stringify(this._grantedScopes));
124
- }
353
+ this.saveValue(SCOPES_KEY, JSON.stringify(this._grantedScopes));
125
354
  if (this._currentUser) {
126
355
  this._currentUser.scopes = this._grantedScopes;
127
356
  this.updateUser(this._currentUser);
@@ -143,12 +372,15 @@ class AuthWeb {
143
372
  }
144
373
  if (this._currentUser.provider === "microsoft") {
145
374
  logger.log("Refreshing Microsoft tokens...");
146
- const refreshToken = this._storageAdapter ? this._storageAdapter.load("microsoft_refresh_token") : localStorage.getItem("nitro_auth_microsoft_refresh_token");
375
+ const refreshToken = this.loadRefreshToken();
147
376
  if (!refreshToken) {
148
377
  throw new Error("No refresh token available");
149
378
  }
150
379
  const config = getConfig();
151
380
  const clientId = config.microsoftClientId;
381
+ if (!clientId) {
382
+ throw new Error("Microsoft Client ID not configured. Add 'microsoftClientId' to expo.extra in your app.config.js");
383
+ }
152
384
  const tenant = config.microsoftTenant ?? "common";
153
385
  const b2cDomain = config.microsoftB2cDomain;
154
386
  const authBaseUrl = this.getMicrosoftAuthBaseUrl(tenant, b2cDomain);
@@ -165,35 +397,38 @@ class AuthWeb {
165
397
  },
166
398
  body: body.toString()
167
399
  });
168
- const json = await response.json();
400
+ const json = await this.parseResponseObject(response);
169
401
  if (!response.ok) {
170
- throw new Error(json.error_description ?? json.error ?? "Token refresh failed");
402
+ throw new Error(getOptionalString(json, "error_description") ?? getOptionalString(json, "error") ?? "Token refresh failed");
171
403
  }
172
- const idToken = json.id_token;
173
- const accessToken = json.access_token;
174
- const newRefreshToken = json.refresh_token;
175
- const expiresIn = json.expires_in;
404
+ const idToken = getOptionalString(json, "id_token");
405
+ const accessToken = getOptionalString(json, "access_token");
406
+ const newRefreshToken = getOptionalString(json, "refresh_token");
407
+ const expiresInSeconds = getOptionalNumber(json, "expires_in");
176
408
  if (newRefreshToken) {
177
- if (this._storageAdapter) {
178
- this._storageAdapter.save("microsoft_refresh_token", newRefreshToken);
179
- } else {
180
- localStorage.setItem("nitro_auth_microsoft_refresh_token", newRefreshToken);
181
- }
409
+ this.saveRefreshToken(newRefreshToken);
182
410
  }
183
- const claims = this.decodeMicrosoftJwt(idToken);
411
+ const expirationTime = typeof expiresInSeconds === "number" ? Date.now() + expiresInSeconds * 1000 : undefined;
412
+ const effectiveIdToken = idToken ?? this._currentUser.idToken;
413
+ const claims = effectiveIdToken ? this.decodeMicrosoftJwt(effectiveIdToken) : {};
184
414
  const user = {
185
415
  ...this._currentUser,
186
- idToken,
416
+ idToken: effectiveIdToken,
187
417
  accessToken: accessToken ?? undefined,
188
- expirationTime: expiresIn ? Date.now() + parseInt(expiresIn) * 1000 : undefined,
418
+ refreshToken: newRefreshToken ?? this._currentUser.refreshToken,
419
+ expirationTime,
189
420
  ...claims
190
421
  };
191
422
  this.updateUser(user);
192
423
  const tokens = {
193
- accessToken,
194
- idToken
424
+ accessToken: accessToken ?? undefined,
425
+ idToken: effectiveIdToken,
426
+ refreshToken: newRefreshToken ?? undefined,
427
+ expirationTime
195
428
  };
196
- this._tokenListeners.forEach(l => l(tokens));
429
+ this._tokenListeners.forEach(l => {
430
+ l(tokens);
431
+ });
197
432
  return tokens;
198
433
  }
199
434
  if (this._currentUser.provider !== "google") {
@@ -203,9 +438,13 @@ class AuthWeb {
203
438
  await this.loginGoogle(this._grantedScopes.length > 0 ? this._grantedScopes : DEFAULT_SCOPES);
204
439
  const tokens = {
205
440
  accessToken: this._currentUser.accessToken,
206
- idToken: this._currentUser.idToken
441
+ idToken: this._currentUser.idToken,
442
+ refreshToken: this._currentUser.refreshToken,
443
+ expirationTime: this._currentUser.expirationTime
207
444
  };
208
- this._tokenListeners.forEach(l => l(tokens));
445
+ this._tokenListeners.forEach(l => {
446
+ l(tokens);
447
+ });
209
448
  return tokens;
210
449
  }
211
450
  mapError(error) {
@@ -214,13 +453,77 @@ class AuthWeb {
214
453
  let mappedMsg = rawMessage;
215
454
  if (msg.includes("cancel") || msg.includes("popup_closed")) {
216
455
  mappedMsg = "cancelled";
456
+ } else if (msg.includes("timeout")) {
457
+ mappedMsg = "timeout";
458
+ } else if (msg.includes("popup blocked")) {
459
+ mappedMsg = "popup_blocked";
217
460
  } else if (msg.includes("network")) {
218
461
  mappedMsg = "network_error";
219
462
  } else if (msg.includes("client id") || msg.includes("config")) {
220
463
  mappedMsg = "configuration_error";
221
464
  }
222
- return Object.assign(new Error(mappedMsg), {
223
- underlyingError: rawMessage
465
+ return new AuthWebError(mappedMsg, rawMessage);
466
+ }
467
+ async parseResponseObject(response) {
468
+ const parsed = await response.json();
469
+ if (!isJsonObject(parsed)) {
470
+ throw new Error("Expected JSON object response from auth provider");
471
+ }
472
+ return parsed;
473
+ }
474
+ parseJwtPayload(token) {
475
+ const payload = token.split(".")[1];
476
+ if (!payload) {
477
+ throw new Error("Invalid JWT payload");
478
+ }
479
+ const decoded = JSON.parse(atob(payload));
480
+ if (!isJsonObject(decoded)) {
481
+ throw new Error("Expected JWT payload to be an object");
482
+ }
483
+ return decoded;
484
+ }
485
+ waitForPopupRedirect(popup, redirectUri, provider, onRedirect) {
486
+ return new Promise((resolve, reject) => {
487
+ let crossOriginLogShown = false;
488
+ const cleanup = (intervalId, timeoutId, shouldClosePopup) => {
489
+ window.clearInterval(intervalId);
490
+ window.clearTimeout(timeoutId);
491
+ if (shouldClosePopup && !popup.closed) {
492
+ popup.close();
493
+ }
494
+ };
495
+ const timeoutId = window.setTimeout(() => {
496
+ cleanup(intervalId, timeoutId, true);
497
+ reject(new Error(`${provider.toLowerCase()}_auth_timeout`));
498
+ }, POPUP_TIMEOUT_MS);
499
+ const intervalId = window.setInterval(() => {
500
+ if (popup.closed) {
501
+ cleanup(intervalId, timeoutId, false);
502
+ reject(new Error("cancelled"));
503
+ return;
504
+ }
505
+ let url;
506
+ try {
507
+ url = popup.location.href;
508
+ } catch (error) {
509
+ if (!crossOriginLogShown) {
510
+ logger.debug(`Waiting for ${provider} auth redirect`, {
511
+ error: String(error)
512
+ });
513
+ crossOriginLogShown = true;
514
+ }
515
+ return;
516
+ }
517
+ if (!url.startsWith(redirectUri)) {
518
+ return;
519
+ }
520
+ cleanup(intervalId, timeoutId, true);
521
+ void Promise.resolve(onRedirect(url)).then(() => {
522
+ resolve();
523
+ }).catch(error => {
524
+ reject(error);
525
+ });
526
+ }, POPUP_POLL_INTERVAL_MS);
224
527
  });
225
528
  }
226
529
  async loginGoogle(scopes, loginHint) {
@@ -236,7 +539,7 @@ class AuthWeb {
236
539
  authUrl.searchParams.set("redirect_uri", redirectUri);
237
540
  authUrl.searchParams.set("response_type", "id_token token code");
238
541
  authUrl.searchParams.set("scope", scopes.join(" "));
239
- authUrl.searchParams.set("nonce", Math.random().toString(36).slice(2));
542
+ authUrl.searchParams.set("nonce", crypto.randomUUID());
240
543
  authUrl.searchParams.set("access_type", "offline");
241
544
  authUrl.searchParams.set("prompt", "consent");
242
545
  if (loginHint) {
@@ -251,59 +554,47 @@ class AuthWeb {
251
554
  reject(new Error("Popup blocked. Please allow popups for this site."));
252
555
  return;
253
556
  }
254
- const checkInterval = setInterval(() => {
255
- try {
256
- if (popup.closed) {
257
- clearInterval(checkInterval);
258
- reject(new Error("cancelled"));
259
- return;
260
- }
261
- const url = popup.location.href;
262
- if (url.startsWith(redirectUri)) {
263
- clearInterval(checkInterval);
264
- popup.close();
265
- const hash = new URL(url).hash.slice(1);
266
- const params = new URLSearchParams(hash);
267
- const idToken = params.get("id_token");
268
- const accessToken = params.get("access_token");
269
- const expiresIn = params.get("expires_in");
270
- const code = params.get("code");
271
- if (idToken) {
272
- this._grantedScopes = scopes;
273
- if (this._storageAdapter) {
274
- this._storageAdapter.save(SCOPES_KEY, JSON.stringify(scopes));
275
- } else {
276
- localStorage.setItem(SCOPES_KEY, JSON.stringify(scopes));
277
- }
278
- const user = {
279
- provider: "google",
280
- idToken,
281
- accessToken: accessToken ?? undefined,
282
- serverAuthCode: code ?? undefined,
283
- scopes,
284
- expirationTime: expiresIn ? Date.now() + parseInt(expiresIn) * 1000 : undefined,
285
- ...this.decodeGoogleJwt(idToken)
286
- };
287
- this.updateUser(user);
288
- resolve();
289
- } else {
290
- reject(new Error("No id_token in response"));
291
- }
292
- }
293
- } catch {}
294
- }, 100);
557
+ void this.waitForPopupRedirect(popup, redirectUri, "Google", url => {
558
+ const hash = new URL(url).hash.slice(1);
559
+ const params = new URLSearchParams(hash);
560
+ const idToken = params.get("id_token");
561
+ const accessToken = params.get("access_token");
562
+ const expiresIn = params.get("expires_in");
563
+ const code = params.get("code");
564
+ if (!idToken) {
565
+ throw new Error("No id_token in response");
566
+ }
567
+ this._grantedScopes = scopes;
568
+ this.saveValue(SCOPES_KEY, JSON.stringify(scopes));
569
+ const user = {
570
+ provider: "google",
571
+ idToken,
572
+ accessToken: accessToken ?? undefined,
573
+ serverAuthCode: code ?? undefined,
574
+ scopes,
575
+ expirationTime: expiresIn ? Date.now() + parseInt(expiresIn, 10) * 1000 : undefined,
576
+ ...this.decodeGoogleJwt(idToken)
577
+ };
578
+ this.updateUser(user);
579
+ }).then(() => {
580
+ resolve();
581
+ }).catch(error => {
582
+ reject(error);
583
+ });
295
584
  });
296
585
  }
297
586
  decodeGoogleJwt(token) {
298
587
  try {
299
- const payload = token.split(".")[1];
300
- const decoded = JSON.parse(atob(payload));
588
+ const decoded = this.parseJwtPayload(token);
301
589
  return {
302
- email: decoded.email,
303
- name: decoded.name,
304
- photo: decoded.picture
590
+ email: getOptionalString(decoded, "email"),
591
+ name: getOptionalString(decoded, "name"),
592
+ photo: getOptionalString(decoded, "picture")
305
593
  };
306
- } catch {
594
+ } catch (error) {
595
+ logger.warn("Failed to decode Google ID token", {
596
+ error: String(error)
597
+ });
307
598
  return {};
308
599
  }
309
600
  }
@@ -346,43 +637,27 @@ class AuthWeb {
346
637
  reject(new Error("Popup blocked. Please allow popups for this site."));
347
638
  return;
348
639
  }
349
- const checkInterval = setInterval(async () => {
350
- try {
351
- if (popup.closed) {
352
- clearInterval(checkInterval);
353
- reject(new Error("cancelled"));
354
- return;
355
- }
356
- const url = popup.location.href;
357
- if (url.startsWith(redirectUri)) {
358
- clearInterval(checkInterval);
359
- popup.close();
360
- const urlObj = new URL(url);
361
- const code = urlObj.searchParams.get("code");
362
- const returnedState = urlObj.searchParams.get("state");
363
- const error = urlObj.searchParams.get("error");
364
- const errorDescription = urlObj.searchParams.get("error_description");
365
- if (error) {
366
- reject(new Error(errorDescription ?? error));
367
- return;
368
- }
369
- if (returnedState !== state) {
370
- reject(new Error("State mismatch - possible CSRF attack"));
371
- return;
372
- }
373
- if (!code) {
374
- reject(new Error("No authorization code in response"));
375
- return;
376
- }
377
- try {
378
- await this.exchangeMicrosoftCodeForTokens(code, codeVerifier, clientId, redirectUri, effectiveTenant, nonce, effectiveScopes);
379
- resolve();
380
- } catch (e) {
381
- reject(e);
382
- }
383
- }
384
- } catch {}
385
- }, 100);
640
+ void this.waitForPopupRedirect(popup, redirectUri, "Microsoft", async url => {
641
+ const urlObj = new URL(url);
642
+ const code = urlObj.searchParams.get("code");
643
+ const returnedState = urlObj.searchParams.get("state");
644
+ const error = urlObj.searchParams.get("error");
645
+ const errorDescription = urlObj.searchParams.get("error_description");
646
+ if (error) {
647
+ throw new Error(errorDescription ?? error);
648
+ }
649
+ if (returnedState !== state) {
650
+ throw new Error("State mismatch - possible CSRF attack");
651
+ }
652
+ if (!code) {
653
+ throw new Error("No authorization code in response");
654
+ }
655
+ await this.exchangeMicrosoftCodeForTokens(code, codeVerifier, clientId, redirectUri, effectiveTenant, nonce, effectiveScopes);
656
+ }).then(() => {
657
+ resolve();
658
+ }).catch(error => {
659
+ reject(error);
660
+ });
386
661
  });
387
662
  }
388
663
  generateCodeVerifier() {
@@ -418,41 +693,34 @@ class AuthWeb {
418
693
  },
419
694
  body: body.toString()
420
695
  });
421
- const json = await response.json();
696
+ const json = await this.parseResponseObject(response);
422
697
  if (!response.ok) {
423
- throw new Error(json.error_description ?? json.error ?? "Token exchange failed");
698
+ throw new Error(getOptionalString(json, "error_description") ?? getOptionalString(json, "error") ?? "Token exchange failed");
424
699
  }
425
- const idToken = json.id_token;
700
+ const idToken = getOptionalString(json, "id_token");
426
701
  if (!idToken) {
427
702
  throw new Error("No id_token in token response");
428
703
  }
429
704
  const claims = this.decodeMicrosoftJwt(idToken);
430
- const payload = JSON.parse(atob(idToken.split(".")[1]));
431
- if (payload.nonce !== expectedNonce) {
705
+ const payload = this.parseJwtPayload(idToken);
706
+ if (getOptionalString(payload, "nonce") !== expectedNonce) {
432
707
  throw new Error("Nonce mismatch - token may be replayed");
433
708
  }
434
- const accessToken = json.access_token;
435
- const refreshToken = json.refresh_token;
436
- const expiresIn = json.expires_in;
709
+ const accessToken = getOptionalString(json, "access_token");
710
+ const refreshToken = getOptionalString(json, "refresh_token");
711
+ const expiresInSeconds = getOptionalNumber(json, "expires_in");
437
712
  if (refreshToken) {
438
- if (this._storageAdapter) {
439
- this._storageAdapter.save("microsoft_refresh_token", refreshToken);
440
- } else {
441
- localStorage.setItem("nitro_auth_microsoft_refresh_token", refreshToken);
442
- }
713
+ this.saveRefreshToken(refreshToken);
443
714
  }
444
715
  this._grantedScopes = scopes;
445
- if (this._storageAdapter) {
446
- this._storageAdapter.save(SCOPES_KEY, JSON.stringify(scopes));
447
- } else {
448
- localStorage.setItem(SCOPES_KEY, JSON.stringify(scopes));
449
- }
716
+ this.saveValue(SCOPES_KEY, JSON.stringify(scopes));
450
717
  const user = {
451
718
  provider: "microsoft",
452
719
  idToken,
453
720
  accessToken: accessToken ?? undefined,
721
+ refreshToken: refreshToken ?? undefined,
454
722
  scopes,
455
- expirationTime: expiresIn ? Date.now() + parseInt(expiresIn) * 1000 : undefined,
723
+ expirationTime: typeof expiresInSeconds === "number" ? Date.now() + expiresInSeconds * 1000 : undefined,
456
724
  ...claims
457
725
  };
458
726
  this.updateUser(user);
@@ -469,13 +737,15 @@ class AuthWeb {
469
737
  }
470
738
  decodeMicrosoftJwt(token) {
471
739
  try {
472
- const payload = token.split(".")[1];
473
- const decoded = JSON.parse(atob(payload));
740
+ const decoded = this.parseJwtPayload(token);
474
741
  return {
475
- email: decoded.preferred_username ?? decoded.email,
476
- name: decoded.name
742
+ email: getOptionalString(decoded, "preferred_username") ?? getOptionalString(decoded, "email"),
743
+ name: getOptionalString(decoded, "name")
477
744
  };
478
- } catch {
745
+ } catch (error) {
746
+ logger.warn("Failed to decode Microsoft ID token", {
747
+ error: String(error)
748
+ });
479
749
  return {};
480
750
  }
481
751
  }
@@ -509,9 +779,13 @@ class AuthWeb {
509
779
  };
510
780
  this.updateUser(user);
511
781
  resolve();
512
- }).catch(err => reject(this.mapError(err)));
782
+ }).catch(err => {
783
+ reject(this.mapError(err));
784
+ });
785
+ };
786
+ script.onerror = () => {
787
+ reject(new Error("Failed to load Apple SDK"));
513
788
  };
514
- script.onerror = () => reject(new Error("Failed to load Apple SDK"));
515
789
  document.head.appendChild(script);
516
790
  });
517
791
  }
@@ -533,27 +807,22 @@ class AuthWeb {
533
807
  this._grantedScopes = [];
534
808
  this.removeFromCache(CACHE_KEY);
535
809
  this.removeFromCache(SCOPES_KEY);
536
- this.removeFromCache("microsoft_refresh_token");
810
+ this.removeFromCache(MS_REFRESH_TOKEN_KEY);
537
811
  this.notify();
538
812
  }
539
813
  updateUser(user) {
540
814
  this._currentUser = user;
541
- if (this._storageAdapter) {
542
- this._storageAdapter.save(CACHE_KEY, JSON.stringify(user));
543
- } else {
544
- localStorage.setItem(CACHE_KEY, JSON.stringify(user));
545
- }
815
+ const userToPersist = this.sanitizeUserForPersistence(user);
816
+ this.saveValue(CACHE_KEY, JSON.stringify(userToPersist));
546
817
  this.notify();
547
818
  }
548
819
  setLoggingEnabled(enabled) {
549
820
  logger.setEnabled(enabled);
550
821
  }
551
- setStorageAdapter(adapter) {
552
- this._storageAdapter = adapter;
553
- if (adapter) {
554
- this.loadFromCache();
555
- this.notify();
556
- }
822
+ setWebStorageAdapter(adapter) {
823
+ this._storageAdapter = adapter ? this.createWebStorageDriver(adapter) : undefined;
824
+ this.loadFromCache();
825
+ this.notify();
557
826
  }
558
827
  name = "Auth";
559
828
  dispose() {}