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