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.
- package/README.md +362 -190
- package/android/build.gradle +2 -5
- package/android/src/main/cpp/PlatformAuth+Android.cpp +84 -18
- package/android/src/main/java/com/auth/AuthAdapter.kt +82 -182
- package/android/src/main/java/com/auth/NitroAuthPackage.kt +1 -1
- package/app.plugin.js +2 -9
- package/cpp/AuthCache.cpp +0 -134
- package/cpp/AuthCache.hpp +0 -7
- package/cpp/HybridAuth.cpp +57 -63
- package/cpp/HybridAuth.hpp +3 -4
- package/ios/AuthAdapter.swift +23 -25
- package/lib/commonjs/Auth.web.js +523 -201
- package/lib/commonjs/Auth.web.js.map +1 -1
- package/lib/commonjs/index.js +0 -12
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +0 -12
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/js-storage-adapter.js +2 -0
- package/lib/commonjs/js-storage-adapter.js.map +1 -0
- package/lib/commonjs/service.js +9 -86
- package/lib/commonjs/service.js.map +1 -1
- package/lib/commonjs/service.web.js +1 -5
- package/lib/commonjs/service.web.js.map +1 -1
- package/lib/commonjs/ui/social-button.js +44 -29
- package/lib/commonjs/ui/social-button.js.map +1 -1
- package/lib/commonjs/ui/social-button.web.js +44 -29
- package/lib/commonjs/ui/social-button.web.js.map +1 -1
- package/lib/commonjs/use-auth.js +56 -42
- package/lib/commonjs/use-auth.js.map +1 -1
- package/lib/commonjs/utils/logger.js +12 -4
- package/lib/commonjs/utils/logger.js.map +1 -1
- package/lib/module/Auth.web.js +523 -201
- package/lib/module/Auth.web.js.map +1 -1
- package/lib/module/index.js +0 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +0 -1
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/js-storage-adapter.js +2 -0
- package/lib/module/js-storage-adapter.js.map +1 -0
- package/lib/module/service.js +9 -86
- package/lib/module/service.js.map +1 -1
- package/lib/module/service.web.js +1 -5
- package/lib/module/service.web.js.map +1 -1
- package/lib/module/ui/social-button.js +44 -29
- package/lib/module/ui/social-button.js.map +1 -1
- package/lib/module/ui/social-button.web.js +44 -29
- package/lib/module/ui/social-button.web.js.map +1 -1
- package/lib/module/use-auth.js +56 -42
- package/lib/module/use-auth.js.map +1 -1
- package/lib/module/utils/logger.js +12 -4
- package/lib/module/utils/logger.js.map +1 -1
- package/lib/typescript/commonjs/Auth.nitro.d.ts +3 -3
- package/lib/typescript/commonjs/Auth.nitro.d.ts.map +1 -1
- package/lib/typescript/commonjs/Auth.web.d.ts +25 -6
- package/lib/typescript/commonjs/Auth.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +1 -2
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.web.d.ts +0 -1
- package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/js-storage-adapter.d.ts +6 -0
- package/lib/typescript/commonjs/js-storage-adapter.d.ts.map +1 -0
- package/lib/typescript/commonjs/service.d.ts +1 -8
- package/lib/typescript/commonjs/service.d.ts.map +1 -1
- package/lib/typescript/commonjs/service.web.d.ts +1 -8
- package/lib/typescript/commonjs/service.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/social-button.d.ts +6 -6
- package/lib/typescript/commonjs/ui/social-button.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/social-button.web.d.ts +6 -6
- package/lib/typescript/commonjs/ui/social-button.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/use-auth.d.ts +4 -4
- package/lib/typescript/commonjs/use-auth.d.ts.map +1 -1
- package/lib/typescript/commonjs/utils/logger.d.ts +4 -4
- package/lib/typescript/commonjs/utils/logger.d.ts.map +1 -1
- package/lib/typescript/module/Auth.nitro.d.ts +3 -3
- package/lib/typescript/module/Auth.nitro.d.ts.map +1 -1
- package/lib/typescript/module/Auth.web.d.ts +25 -6
- package/lib/typescript/module/Auth.web.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +1 -2
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/index.web.d.ts +0 -1
- package/lib/typescript/module/index.web.d.ts.map +1 -1
- package/lib/typescript/module/js-storage-adapter.d.ts +6 -0
- package/lib/typescript/module/js-storage-adapter.d.ts.map +1 -0
- package/lib/typescript/module/service.d.ts +1 -8
- package/lib/typescript/module/service.d.ts.map +1 -1
- package/lib/typescript/module/service.web.d.ts +1 -8
- package/lib/typescript/module/service.web.d.ts.map +1 -1
- package/lib/typescript/module/ui/social-button.d.ts +6 -6
- package/lib/typescript/module/ui/social-button.d.ts.map +1 -1
- package/lib/typescript/module/ui/social-button.web.d.ts +6 -6
- package/lib/typescript/module/ui/social-button.web.d.ts.map +1 -1
- package/lib/typescript/module/use-auth.d.ts +4 -4
- package/lib/typescript/module/use-auth.d.ts.map +1 -1
- package/lib/typescript/module/utils/logger.d.ts +4 -4
- package/lib/typescript/module/utils/logger.d.ts.map +1 -1
- package/nitrogen/generated/android/NitroAuth+autolinking.cmake +0 -1
- package/nitrogen/generated/shared/c++/AuthTokens.hpp +5 -1
- package/nitrogen/generated/shared/c++/AuthUser.hpp +5 -1
- package/nitrogen/generated/shared/c++/HybridAuthSpec.cpp +0 -1
- package/nitrogen/generated/shared/c++/HybridAuthSpec.hpp +0 -5
- package/package.json +11 -8
- package/react-native-nitro-auth.podspec +1 -1
- package/src/Auth.nitro.ts +4 -3
- package/src/Auth.web.ts +700 -246
- package/src/global.d.ts +0 -1
- package/src/index.ts +1 -2
- package/src/index.web.ts +0 -1
- package/src/js-storage-adapter.ts +5 -0
- package/src/service.ts +13 -106
- package/src/service.web.ts +0 -7
- package/src/ui/social-button.tsx +66 -43
- package/src/ui/social-button.web.tsx +67 -44
- package/src/use-auth.ts +116 -72
- package/src/utils/logger.ts +12 -4
- package/ios/KeychainStore.swift +0 -43
- package/lib/commonjs/AuthStorage.nitro.js +0 -6
- package/lib/commonjs/AuthStorage.nitro.js.map +0 -1
- package/lib/module/AuthStorage.nitro.js +0 -4
- package/lib/module/AuthStorage.nitro.js.map +0 -1
- package/lib/typescript/commonjs/AuthStorage.nitro.d.ts +0 -26
- package/lib/typescript/commonjs/AuthStorage.nitro.d.ts.map +0 -1
- package/lib/typescript/module/AuthStorage.nitro.d.ts +0 -26
- package/lib/typescript/module/AuthStorage.nitro.d.ts.map +0 -1
- package/nitrogen/generated/shared/c++/HybridAuthStorageAdapterSpec.cpp +0 -23
- package/nitrogen/generated/shared/c++/HybridAuthStorageAdapterSpec.hpp +0 -65
- package/src/AuthStorage.nitro.ts +0 -26
package/lib/commonjs/Auth.web.js
CHANGED
|
@@ -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
|
};
|
|
@@ -21,33 +91,204 @@ class AuthWeb {
|
|
|
21
91
|
_grantedScopes = [];
|
|
22
92
|
_listeners = [];
|
|
23
93
|
_tokenListeners = [];
|
|
94
|
+
_browserStorageResolved = false;
|
|
24
95
|
constructor() {
|
|
96
|
+
this._config = getConfig();
|
|
25
97
|
this.loadFromCache();
|
|
26
98
|
}
|
|
99
|
+
isPromiseLike(value) {
|
|
100
|
+
if (!isJsonObject(value)) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
return typeof value.then === "function";
|
|
104
|
+
}
|
|
105
|
+
createWebStorageDriver(adapter) {
|
|
106
|
+
return {
|
|
107
|
+
save: (key, value) => {
|
|
108
|
+
const result = adapter.save(key, value);
|
|
109
|
+
if (this.isPromiseLike(result)) {
|
|
110
|
+
throw new Error("On web, JSStorageAdapter.save must be synchronous.");
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
load: key => {
|
|
114
|
+
const result = adapter.load(key);
|
|
115
|
+
if (this.isPromiseLike(result)) {
|
|
116
|
+
throw new Error("On web, JSStorageAdapter.load must be synchronous.");
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
},
|
|
120
|
+
remove: key => {
|
|
121
|
+
const result = adapter.remove(key);
|
|
122
|
+
if (this.isPromiseLike(result)) {
|
|
123
|
+
throw new Error("On web, JSStorageAdapter.remove must be synchronous.");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
shouldPersistTokensInStorage() {
|
|
129
|
+
if (this._storageAdapter) {
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
return this._config.nitroAuthPersistTokensOnWeb === true;
|
|
133
|
+
}
|
|
134
|
+
getWebStorageMode() {
|
|
135
|
+
const configuredMode = this._config.nitroAuthWebStorage;
|
|
136
|
+
if (configuredMode && WEB_STORAGE_MODES.has(configuredMode)) {
|
|
137
|
+
return configuredMode;
|
|
138
|
+
}
|
|
139
|
+
return STORAGE_MODE_SESSION;
|
|
140
|
+
}
|
|
141
|
+
getBrowserStorage() {
|
|
142
|
+
if (this._browserStorageResolved) {
|
|
143
|
+
return this._browserStorageCache;
|
|
144
|
+
}
|
|
145
|
+
this._browserStorageResolved = true;
|
|
146
|
+
if (typeof window === "undefined") {
|
|
147
|
+
this._browserStorageCache = undefined;
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
const mode = this.getWebStorageMode();
|
|
151
|
+
if (mode === STORAGE_MODE_MEMORY) {
|
|
152
|
+
this._browserStorageCache = undefined;
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
const storage = mode === STORAGE_MODE_LOCAL ? window.localStorage : window.sessionStorage;
|
|
156
|
+
try {
|
|
157
|
+
const testKey = "__nitro_auth_storage_probe__";
|
|
158
|
+
storage.setItem(testKey, "1");
|
|
159
|
+
storage.removeItem(testKey);
|
|
160
|
+
this._browserStorageCache = storage;
|
|
161
|
+
return storage;
|
|
162
|
+
} catch (error) {
|
|
163
|
+
_logger.logger.warn("Configured web storage is unavailable; using in-memory fallback", {
|
|
164
|
+
mode,
|
|
165
|
+
error: String(error)
|
|
166
|
+
});
|
|
167
|
+
this._browserStorageCache = undefined;
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
saveValue(key, value) {
|
|
172
|
+
if (this._storageAdapter) {
|
|
173
|
+
this._storageAdapter.save(key, value);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const storage = this.getBrowserStorage();
|
|
177
|
+
if (storage) {
|
|
178
|
+
storage.setItem(key, value);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
inMemoryWebStorage.set(key, value);
|
|
182
|
+
}
|
|
183
|
+
loadValue(key) {
|
|
184
|
+
if (this._storageAdapter) {
|
|
185
|
+
return this._storageAdapter.load(key);
|
|
186
|
+
}
|
|
187
|
+
const storage = this.getBrowserStorage();
|
|
188
|
+
if (storage) {
|
|
189
|
+
return storage.getItem(key) ?? undefined;
|
|
190
|
+
}
|
|
191
|
+
return inMemoryWebStorage.get(key);
|
|
192
|
+
}
|
|
193
|
+
removeValue(key) {
|
|
194
|
+
if (this._storageAdapter) {
|
|
195
|
+
this._storageAdapter.remove(key);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const storage = this.getBrowserStorage();
|
|
199
|
+
if (storage) {
|
|
200
|
+
storage.removeItem(key);
|
|
201
|
+
}
|
|
202
|
+
inMemoryWebStorage.delete(key);
|
|
203
|
+
}
|
|
204
|
+
removePersistedBrowserValue(key) {
|
|
205
|
+
if (typeof window === "undefined") {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
window.localStorage.removeItem(key);
|
|
210
|
+
window.sessionStorage.removeItem(key);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
_logger.logger.debug("Failed to clear persisted browser value", {
|
|
213
|
+
key,
|
|
214
|
+
error: String(error)
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
sanitizeUserForPersistence(user) {
|
|
219
|
+
if (this.shouldPersistTokensInStorage()) {
|
|
220
|
+
return user;
|
|
221
|
+
}
|
|
222
|
+
const safeUser = {
|
|
223
|
+
...user
|
|
224
|
+
};
|
|
225
|
+
delete safeUser.accessToken;
|
|
226
|
+
delete safeUser.idToken;
|
|
227
|
+
delete safeUser.serverAuthCode;
|
|
228
|
+
return safeUser;
|
|
229
|
+
}
|
|
230
|
+
saveRefreshToken(refreshToken) {
|
|
231
|
+
if (this._storageAdapter || this.shouldPersistTokensInStorage()) {
|
|
232
|
+
this.saveValue(MS_REFRESH_TOKEN_KEY, refreshToken);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Security-first default: keep refresh tokens in-memory only on web.
|
|
237
|
+
inMemoryWebStorage.set(MS_REFRESH_TOKEN_KEY, refreshToken);
|
|
238
|
+
}
|
|
239
|
+
loadRefreshToken() {
|
|
240
|
+
if (this._storageAdapter || this.shouldPersistTokensInStorage()) {
|
|
241
|
+
return this.loadValue(MS_REFRESH_TOKEN_KEY);
|
|
242
|
+
}
|
|
243
|
+
return inMemoryWebStorage.get(MS_REFRESH_TOKEN_KEY);
|
|
244
|
+
}
|
|
27
245
|
loadFromCache() {
|
|
28
|
-
const cached = this.
|
|
246
|
+
const cached = this.loadValue(CACHE_KEY);
|
|
29
247
|
if (cached) {
|
|
30
248
|
try {
|
|
31
|
-
|
|
32
|
-
|
|
249
|
+
const parsedUser = parseAuthUser(JSON.parse(cached));
|
|
250
|
+
if (!parsedUser) {
|
|
251
|
+
throw new Error("Expected cached auth user to be a valid AuthUser");
|
|
252
|
+
}
|
|
253
|
+
if (this.shouldPersistTokensInStorage()) {
|
|
254
|
+
this._currentUser = parsedUser;
|
|
255
|
+
} else {
|
|
256
|
+
const safeUser = {
|
|
257
|
+
...parsedUser
|
|
258
|
+
};
|
|
259
|
+
delete safeUser.accessToken;
|
|
260
|
+
delete safeUser.idToken;
|
|
261
|
+
delete safeUser.serverAuthCode;
|
|
262
|
+
this._currentUser = safeUser;
|
|
263
|
+
}
|
|
264
|
+
} catch (error) {
|
|
265
|
+
_logger.logger.warn("Failed to parse cached auth user; clearing cache", {
|
|
266
|
+
error: String(error)
|
|
267
|
+
});
|
|
33
268
|
this.removeFromCache(CACHE_KEY);
|
|
34
269
|
}
|
|
35
270
|
}
|
|
36
|
-
const scopes = this.
|
|
271
|
+
const scopes = this.loadValue(SCOPES_KEY);
|
|
37
272
|
if (scopes) {
|
|
38
273
|
try {
|
|
39
|
-
|
|
40
|
-
|
|
274
|
+
const parsedScopes = parseScopes(JSON.parse(scopes));
|
|
275
|
+
if (!parsedScopes) {
|
|
276
|
+
throw new Error("Expected cached scopes to be an array");
|
|
277
|
+
}
|
|
278
|
+
this._grantedScopes = parsedScopes;
|
|
279
|
+
} catch (error) {
|
|
280
|
+
_logger.logger.warn("Failed to parse cached scopes; clearing cache", {
|
|
281
|
+
error: String(error)
|
|
282
|
+
});
|
|
41
283
|
this.removeFromCache(SCOPES_KEY);
|
|
42
284
|
}
|
|
43
285
|
}
|
|
286
|
+
if (!this.shouldPersistTokensInStorage() && !this._storageAdapter) {
|
|
287
|
+
this.removePersistedBrowserValue(MS_REFRESH_TOKEN_KEY);
|
|
288
|
+
}
|
|
44
289
|
}
|
|
45
290
|
removeFromCache(key) {
|
|
46
|
-
|
|
47
|
-
this._storageAdapter.remove(key);
|
|
48
|
-
} else {
|
|
49
|
-
localStorage.removeItem(key);
|
|
50
|
-
}
|
|
291
|
+
this.removeValue(key);
|
|
51
292
|
}
|
|
52
293
|
get currentUser() {
|
|
53
294
|
return this._currentUser;
|
|
@@ -72,7 +313,9 @@ class AuthWeb {
|
|
|
72
313
|
};
|
|
73
314
|
}
|
|
74
315
|
notify() {
|
|
75
|
-
this._listeners.forEach(l =>
|
|
316
|
+
this._listeners.forEach(l => {
|
|
317
|
+
l(this._currentUser);
|
|
318
|
+
});
|
|
76
319
|
}
|
|
77
320
|
async login(provider, options) {
|
|
78
321
|
const loginHint = options?.loginHint;
|
|
@@ -121,11 +364,7 @@ class AuthWeb {
|
|
|
121
364
|
async revokeScopes(scopes) {
|
|
122
365
|
_logger.logger.log("Revoking scopes:", scopes);
|
|
123
366
|
this._grantedScopes = this._grantedScopes.filter(s => !scopes.includes(s));
|
|
124
|
-
|
|
125
|
-
this._storageAdapter.save(SCOPES_KEY, JSON.stringify(this._grantedScopes));
|
|
126
|
-
} else {
|
|
127
|
-
localStorage.setItem(SCOPES_KEY, JSON.stringify(this._grantedScopes));
|
|
128
|
-
}
|
|
367
|
+
this.saveValue(SCOPES_KEY, JSON.stringify(this._grantedScopes));
|
|
129
368
|
if (this._currentUser) {
|
|
130
369
|
this._currentUser.scopes = this._grantedScopes;
|
|
131
370
|
this.updateUser(this._currentUser);
|
|
@@ -142,19 +381,35 @@ class AuthWeb {
|
|
|
142
381
|
return this._currentUser?.accessToken;
|
|
143
382
|
}
|
|
144
383
|
async refreshToken() {
|
|
384
|
+
if (this._refreshPromise) {
|
|
385
|
+
return this._refreshPromise;
|
|
386
|
+
}
|
|
387
|
+
const refreshPromise = this.performRefreshToken();
|
|
388
|
+
this._refreshPromise = refreshPromise;
|
|
389
|
+
try {
|
|
390
|
+
return await refreshPromise;
|
|
391
|
+
} finally {
|
|
392
|
+
if (this._refreshPromise === refreshPromise) {
|
|
393
|
+
this._refreshPromise = undefined;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
async performRefreshToken() {
|
|
145
398
|
if (!this._currentUser) {
|
|
146
399
|
throw new Error("No user logged in");
|
|
147
400
|
}
|
|
148
401
|
if (this._currentUser.provider === "microsoft") {
|
|
149
402
|
_logger.logger.log("Refreshing Microsoft tokens...");
|
|
150
|
-
const refreshToken = this.
|
|
403
|
+
const refreshToken = this.loadRefreshToken();
|
|
151
404
|
if (!refreshToken) {
|
|
152
405
|
throw new Error("No refresh token available");
|
|
153
406
|
}
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
407
|
+
const clientId = this._config.microsoftClientId;
|
|
408
|
+
if (!clientId) {
|
|
409
|
+
throw new Error("Microsoft Client ID not configured. Add 'microsoftClientId' to expo.extra in your app.config.js");
|
|
410
|
+
}
|
|
411
|
+
const tenant = this._config.microsoftTenant ?? "common";
|
|
412
|
+
const b2cDomain = this._config.microsoftB2cDomain;
|
|
158
413
|
const authBaseUrl = this.getMicrosoftAuthBaseUrl(tenant, b2cDomain);
|
|
159
414
|
const tokenUrl = `${authBaseUrl}oauth2/v2.0/token`;
|
|
160
415
|
const body = new URLSearchParams({
|
|
@@ -169,35 +424,38 @@ class AuthWeb {
|
|
|
169
424
|
},
|
|
170
425
|
body: body.toString()
|
|
171
426
|
});
|
|
172
|
-
const json = await
|
|
427
|
+
const json = await this.parseResponseObject(response);
|
|
173
428
|
if (!response.ok) {
|
|
174
|
-
throw new Error(json
|
|
429
|
+
throw new Error(getOptionalString(json, "error_description") ?? getOptionalString(json, "error") ?? "Token refresh failed");
|
|
175
430
|
}
|
|
176
|
-
const idToken = json
|
|
177
|
-
const accessToken = json
|
|
178
|
-
const newRefreshToken = json
|
|
179
|
-
const
|
|
431
|
+
const idToken = getOptionalString(json, "id_token");
|
|
432
|
+
const accessToken = getOptionalString(json, "access_token");
|
|
433
|
+
const newRefreshToken = getOptionalString(json, "refresh_token");
|
|
434
|
+
const expiresInSeconds = getOptionalNumber(json, "expires_in");
|
|
180
435
|
if (newRefreshToken) {
|
|
181
|
-
|
|
182
|
-
this._storageAdapter.save("microsoft_refresh_token", newRefreshToken);
|
|
183
|
-
} else {
|
|
184
|
-
localStorage.setItem("nitro_auth_microsoft_refresh_token", newRefreshToken);
|
|
185
|
-
}
|
|
436
|
+
this.saveRefreshToken(newRefreshToken);
|
|
186
437
|
}
|
|
187
|
-
const
|
|
438
|
+
const expirationTime = typeof expiresInSeconds === "number" ? Date.now() + expiresInSeconds * 1000 : undefined;
|
|
439
|
+
const effectiveIdToken = idToken ?? this._currentUser.idToken;
|
|
440
|
+
const claims = effectiveIdToken ? this.decodeMicrosoftJwt(effectiveIdToken) : {};
|
|
188
441
|
const user = {
|
|
189
442
|
...this._currentUser,
|
|
190
|
-
idToken,
|
|
443
|
+
idToken: effectiveIdToken,
|
|
191
444
|
accessToken: accessToken ?? undefined,
|
|
192
|
-
|
|
445
|
+
refreshToken: newRefreshToken ?? this._currentUser.refreshToken,
|
|
446
|
+
expirationTime,
|
|
193
447
|
...claims
|
|
194
448
|
};
|
|
195
449
|
this.updateUser(user);
|
|
196
450
|
const tokens = {
|
|
197
|
-
accessToken,
|
|
198
|
-
idToken
|
|
451
|
+
accessToken: accessToken ?? undefined,
|
|
452
|
+
idToken: effectiveIdToken,
|
|
453
|
+
refreshToken: newRefreshToken ?? undefined,
|
|
454
|
+
expirationTime
|
|
199
455
|
};
|
|
200
|
-
this._tokenListeners.forEach(l =>
|
|
456
|
+
this._tokenListeners.forEach(l => {
|
|
457
|
+
l(tokens);
|
|
458
|
+
});
|
|
201
459
|
return tokens;
|
|
202
460
|
}
|
|
203
461
|
if (this._currentUser.provider !== "google") {
|
|
@@ -207,9 +465,13 @@ class AuthWeb {
|
|
|
207
465
|
await this.loginGoogle(this._grantedScopes.length > 0 ? this._grantedScopes : DEFAULT_SCOPES);
|
|
208
466
|
const tokens = {
|
|
209
467
|
accessToken: this._currentUser.accessToken,
|
|
210
|
-
idToken: this._currentUser.idToken
|
|
468
|
+
idToken: this._currentUser.idToken,
|
|
469
|
+
refreshToken: this._currentUser.refreshToken,
|
|
470
|
+
expirationTime: this._currentUser.expirationTime
|
|
211
471
|
};
|
|
212
|
-
this._tokenListeners.forEach(l =>
|
|
472
|
+
this._tokenListeners.forEach(l => {
|
|
473
|
+
l(tokens);
|
|
474
|
+
});
|
|
213
475
|
return tokens;
|
|
214
476
|
}
|
|
215
477
|
mapError(error) {
|
|
@@ -218,18 +480,83 @@ class AuthWeb {
|
|
|
218
480
|
let mappedMsg = rawMessage;
|
|
219
481
|
if (msg.includes("cancel") || msg.includes("popup_closed")) {
|
|
220
482
|
mappedMsg = "cancelled";
|
|
483
|
+
} else if (msg.includes("timeout")) {
|
|
484
|
+
mappedMsg = "timeout";
|
|
485
|
+
} else if (msg.includes("popup blocked")) {
|
|
486
|
+
mappedMsg = "popup_blocked";
|
|
221
487
|
} else if (msg.includes("network")) {
|
|
222
488
|
mappedMsg = "network_error";
|
|
223
489
|
} else if (msg.includes("client id") || msg.includes("config")) {
|
|
224
490
|
mappedMsg = "configuration_error";
|
|
225
491
|
}
|
|
226
|
-
return
|
|
227
|
-
|
|
492
|
+
return new AuthWebError(mappedMsg, rawMessage);
|
|
493
|
+
}
|
|
494
|
+
async parseResponseObject(response) {
|
|
495
|
+
const parsed = await response.json();
|
|
496
|
+
if (!isJsonObject(parsed)) {
|
|
497
|
+
throw new Error("Expected JSON object response from auth provider");
|
|
498
|
+
}
|
|
499
|
+
return parsed;
|
|
500
|
+
}
|
|
501
|
+
parseJwtPayload(token) {
|
|
502
|
+
const payload = token.split(".")[1];
|
|
503
|
+
if (!payload) {
|
|
504
|
+
throw new Error("Invalid JWT payload");
|
|
505
|
+
}
|
|
506
|
+
const normalizedPayload = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
507
|
+
const padding = "=".repeat((4 - normalizedPayload.length % 4) % 4);
|
|
508
|
+
const decoded = JSON.parse(atob(`${normalizedPayload}${padding}`));
|
|
509
|
+
if (!isJsonObject(decoded)) {
|
|
510
|
+
throw new Error("Expected JWT payload to be an object");
|
|
511
|
+
}
|
|
512
|
+
return decoded;
|
|
513
|
+
}
|
|
514
|
+
waitForPopupRedirect(popup, redirectUri, provider, onRedirect) {
|
|
515
|
+
return new Promise((resolve, reject) => {
|
|
516
|
+
let crossOriginLogShown = false;
|
|
517
|
+
const cleanup = (intervalId, timeoutId, shouldClosePopup) => {
|
|
518
|
+
window.clearInterval(intervalId);
|
|
519
|
+
window.clearTimeout(timeoutId);
|
|
520
|
+
if (shouldClosePopup && !popup.closed) {
|
|
521
|
+
popup.close();
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
const timeoutId = window.setTimeout(() => {
|
|
525
|
+
cleanup(intervalId, timeoutId, true);
|
|
526
|
+
reject(new Error(`${provider.toLowerCase()}_auth_timeout`));
|
|
527
|
+
}, POPUP_TIMEOUT_MS);
|
|
528
|
+
const intervalId = window.setInterval(() => {
|
|
529
|
+
if (popup.closed) {
|
|
530
|
+
cleanup(intervalId, timeoutId, false);
|
|
531
|
+
reject(new Error("cancelled"));
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
let url;
|
|
535
|
+
try {
|
|
536
|
+
url = popup.location.href;
|
|
537
|
+
} catch (error) {
|
|
538
|
+
if (!crossOriginLogShown) {
|
|
539
|
+
_logger.logger.debug(`Waiting for ${provider} auth redirect`, {
|
|
540
|
+
error: String(error)
|
|
541
|
+
});
|
|
542
|
+
crossOriginLogShown = true;
|
|
543
|
+
}
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
if (!url.startsWith(redirectUri)) {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
cleanup(intervalId, timeoutId, true);
|
|
550
|
+
void Promise.resolve(onRedirect(url)).then(() => {
|
|
551
|
+
resolve();
|
|
552
|
+
}).catch(error => {
|
|
553
|
+
reject(error);
|
|
554
|
+
});
|
|
555
|
+
}, POPUP_POLL_INTERVAL_MS);
|
|
228
556
|
});
|
|
229
557
|
}
|
|
230
558
|
async loginGoogle(scopes, loginHint) {
|
|
231
|
-
const
|
|
232
|
-
const clientId = config.googleWebClientId;
|
|
559
|
+
const clientId = this._config.googleWebClientId;
|
|
233
560
|
if (!clientId) {
|
|
234
561
|
throw new Error("Google Web Client ID not configured. Add 'GOOGLE_WEB_CLIENT_ID' to your .env file.");
|
|
235
562
|
}
|
|
@@ -240,7 +567,7 @@ class AuthWeb {
|
|
|
240
567
|
authUrl.searchParams.set("redirect_uri", redirectUri);
|
|
241
568
|
authUrl.searchParams.set("response_type", "id_token token code");
|
|
242
569
|
authUrl.searchParams.set("scope", scopes.join(" "));
|
|
243
|
-
authUrl.searchParams.set("nonce",
|
|
570
|
+
authUrl.searchParams.set("nonce", crypto.randomUUID());
|
|
244
571
|
authUrl.searchParams.set("access_type", "offline");
|
|
245
572
|
authUrl.searchParams.set("prompt", "consent");
|
|
246
573
|
if (loginHint) {
|
|
@@ -255,70 +582,57 @@ class AuthWeb {
|
|
|
255
582
|
reject(new Error("Popup blocked. Please allow popups for this site."));
|
|
256
583
|
return;
|
|
257
584
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
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);
|
|
585
|
+
void this.waitForPopupRedirect(popup, redirectUri, "Google", url => {
|
|
586
|
+
const hash = new URL(url).hash.slice(1);
|
|
587
|
+
const params = new URLSearchParams(hash);
|
|
588
|
+
const idToken = params.get("id_token");
|
|
589
|
+
const accessToken = params.get("access_token");
|
|
590
|
+
const expiresIn = params.get("expires_in");
|
|
591
|
+
const code = params.get("code");
|
|
592
|
+
if (!idToken) {
|
|
593
|
+
throw new Error("No id_token in response");
|
|
594
|
+
}
|
|
595
|
+
this._grantedScopes = scopes;
|
|
596
|
+
this.saveValue(SCOPES_KEY, JSON.stringify(scopes));
|
|
597
|
+
const user = {
|
|
598
|
+
provider: "google",
|
|
599
|
+
idToken,
|
|
600
|
+
accessToken: accessToken ?? undefined,
|
|
601
|
+
serverAuthCode: code ?? undefined,
|
|
602
|
+
scopes,
|
|
603
|
+
expirationTime: expiresIn ? Date.now() + parseInt(expiresIn, 10) * 1000 : undefined,
|
|
604
|
+
...this.decodeGoogleJwt(idToken)
|
|
605
|
+
};
|
|
606
|
+
this.updateUser(user);
|
|
607
|
+
}).then(() => {
|
|
608
|
+
resolve();
|
|
609
|
+
}).catch(error => {
|
|
610
|
+
reject(error);
|
|
611
|
+
});
|
|
299
612
|
});
|
|
300
613
|
}
|
|
301
614
|
decodeGoogleJwt(token) {
|
|
302
615
|
try {
|
|
303
|
-
const
|
|
304
|
-
const decoded = JSON.parse(atob(payload));
|
|
616
|
+
const decoded = this.parseJwtPayload(token);
|
|
305
617
|
return {
|
|
306
|
-
email: decoded
|
|
307
|
-
name: decoded
|
|
308
|
-
photo: decoded
|
|
618
|
+
email: getOptionalString(decoded, "email"),
|
|
619
|
+
name: getOptionalString(decoded, "name"),
|
|
620
|
+
photo: getOptionalString(decoded, "picture")
|
|
309
621
|
};
|
|
310
|
-
} catch {
|
|
622
|
+
} catch (error) {
|
|
623
|
+
_logger.logger.warn("Failed to decode Google ID token", {
|
|
624
|
+
error: String(error)
|
|
625
|
+
});
|
|
311
626
|
return {};
|
|
312
627
|
}
|
|
313
628
|
}
|
|
314
629
|
async loginMicrosoft(scopes, loginHint, tenant, prompt) {
|
|
315
|
-
const
|
|
316
|
-
const clientId = config.microsoftClientId;
|
|
630
|
+
const clientId = this._config.microsoftClientId;
|
|
317
631
|
if (!clientId) {
|
|
318
632
|
throw new Error("Microsoft Client ID not configured. Add 'microsoftClientId' to expo.extra in your app.config.js");
|
|
319
633
|
}
|
|
320
|
-
const effectiveTenant = tenant ??
|
|
321
|
-
const b2cDomain =
|
|
634
|
+
const effectiveTenant = tenant ?? this._config.microsoftTenant ?? "common";
|
|
635
|
+
const b2cDomain = this._config.microsoftB2cDomain;
|
|
322
636
|
const authBaseUrl = this.getMicrosoftAuthBaseUrl(effectiveTenant, b2cDomain);
|
|
323
637
|
const effectiveScopes = scopes.length ? scopes : ["openid", "email", "profile", "offline_access", "User.Read"];
|
|
324
638
|
const codeVerifier = this.generateCodeVerifier();
|
|
@@ -350,43 +664,27 @@ class AuthWeb {
|
|
|
350
664
|
reject(new Error("Popup blocked. Please allow popups for this site."));
|
|
351
665
|
return;
|
|
352
666
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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);
|
|
667
|
+
void this.waitForPopupRedirect(popup, redirectUri, "Microsoft", async url => {
|
|
668
|
+
const urlObj = new URL(url);
|
|
669
|
+
const code = urlObj.searchParams.get("code");
|
|
670
|
+
const returnedState = urlObj.searchParams.get("state");
|
|
671
|
+
const error = urlObj.searchParams.get("error");
|
|
672
|
+
const errorDescription = urlObj.searchParams.get("error_description");
|
|
673
|
+
if (error) {
|
|
674
|
+
throw new Error(errorDescription ?? error);
|
|
675
|
+
}
|
|
676
|
+
if (returnedState !== state) {
|
|
677
|
+
throw new Error("State mismatch - possible CSRF attack");
|
|
678
|
+
}
|
|
679
|
+
if (!code) {
|
|
680
|
+
throw new Error("No authorization code in response");
|
|
681
|
+
}
|
|
682
|
+
await this.exchangeMicrosoftCodeForTokens(code, codeVerifier, clientId, redirectUri, effectiveTenant, nonce, effectiveScopes);
|
|
683
|
+
}).then(() => {
|
|
684
|
+
resolve();
|
|
685
|
+
}).catch(error => {
|
|
686
|
+
reject(error);
|
|
687
|
+
});
|
|
390
688
|
});
|
|
391
689
|
}
|
|
392
690
|
generateCodeVerifier() {
|
|
@@ -405,8 +703,7 @@ class AuthWeb {
|
|
|
405
703
|
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
406
704
|
}
|
|
407
705
|
async exchangeMicrosoftCodeForTokens(code, codeVerifier, clientId, redirectUri, tenant, expectedNonce, scopes) {
|
|
408
|
-
const
|
|
409
|
-
const authBaseUrl = this.getMicrosoftAuthBaseUrl(tenant, config.microsoftB2cDomain);
|
|
706
|
+
const authBaseUrl = this.getMicrosoftAuthBaseUrl(tenant, this._config.microsoftB2cDomain);
|
|
410
707
|
const tokenUrl = `${authBaseUrl}oauth2/v2.0/token`;
|
|
411
708
|
const body = new URLSearchParams({
|
|
412
709
|
client_id: clientId,
|
|
@@ -422,41 +719,34 @@ class AuthWeb {
|
|
|
422
719
|
},
|
|
423
720
|
body: body.toString()
|
|
424
721
|
});
|
|
425
|
-
const json = await
|
|
722
|
+
const json = await this.parseResponseObject(response);
|
|
426
723
|
if (!response.ok) {
|
|
427
|
-
throw new Error(json
|
|
724
|
+
throw new Error(getOptionalString(json, "error_description") ?? getOptionalString(json, "error") ?? "Token exchange failed");
|
|
428
725
|
}
|
|
429
|
-
const idToken = json
|
|
726
|
+
const idToken = getOptionalString(json, "id_token");
|
|
430
727
|
if (!idToken) {
|
|
431
728
|
throw new Error("No id_token in token response");
|
|
432
729
|
}
|
|
433
730
|
const claims = this.decodeMicrosoftJwt(idToken);
|
|
434
|
-
const payload =
|
|
435
|
-
if (payload
|
|
731
|
+
const payload = this.parseJwtPayload(idToken);
|
|
732
|
+
if (getOptionalString(payload, "nonce") !== expectedNonce) {
|
|
436
733
|
throw new Error("Nonce mismatch - token may be replayed");
|
|
437
734
|
}
|
|
438
|
-
const accessToken = json
|
|
439
|
-
const refreshToken = json
|
|
440
|
-
const
|
|
735
|
+
const accessToken = getOptionalString(json, "access_token");
|
|
736
|
+
const refreshToken = getOptionalString(json, "refresh_token");
|
|
737
|
+
const expiresInSeconds = getOptionalNumber(json, "expires_in");
|
|
441
738
|
if (refreshToken) {
|
|
442
|
-
|
|
443
|
-
this._storageAdapter.save("microsoft_refresh_token", refreshToken);
|
|
444
|
-
} else {
|
|
445
|
-
localStorage.setItem("nitro_auth_microsoft_refresh_token", refreshToken);
|
|
446
|
-
}
|
|
739
|
+
this.saveRefreshToken(refreshToken);
|
|
447
740
|
}
|
|
448
741
|
this._grantedScopes = scopes;
|
|
449
|
-
|
|
450
|
-
this._storageAdapter.save(SCOPES_KEY, JSON.stringify(scopes));
|
|
451
|
-
} else {
|
|
452
|
-
localStorage.setItem(SCOPES_KEY, JSON.stringify(scopes));
|
|
453
|
-
}
|
|
742
|
+
this.saveValue(SCOPES_KEY, JSON.stringify(scopes));
|
|
454
743
|
const user = {
|
|
455
744
|
provider: "microsoft",
|
|
456
745
|
idToken,
|
|
457
746
|
accessToken: accessToken ?? undefined,
|
|
747
|
+
refreshToken: refreshToken ?? undefined,
|
|
458
748
|
scopes,
|
|
459
|
-
expirationTime:
|
|
749
|
+
expirationTime: typeof expiresInSeconds === "number" ? Date.now() + expiresInSeconds * 1000 : undefined,
|
|
460
750
|
...claims
|
|
461
751
|
};
|
|
462
752
|
this.updateUser(user);
|
|
@@ -473,51 +763,88 @@ class AuthWeb {
|
|
|
473
763
|
}
|
|
474
764
|
decodeMicrosoftJwt(token) {
|
|
475
765
|
try {
|
|
476
|
-
const
|
|
477
|
-
const decoded = JSON.parse(atob(payload));
|
|
766
|
+
const decoded = this.parseJwtPayload(token);
|
|
478
767
|
return {
|
|
479
|
-
email: decoded
|
|
480
|
-
name: decoded
|
|
768
|
+
email: getOptionalString(decoded, "preferred_username") ?? getOptionalString(decoded, "email"),
|
|
769
|
+
name: getOptionalString(decoded, "name")
|
|
481
770
|
};
|
|
482
|
-
} catch {
|
|
771
|
+
} catch (error) {
|
|
772
|
+
_logger.logger.warn("Failed to decode Microsoft ID token", {
|
|
773
|
+
error: String(error)
|
|
774
|
+
});
|
|
483
775
|
return {};
|
|
484
776
|
}
|
|
485
777
|
}
|
|
486
|
-
async
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
if (!clientId) {
|
|
490
|
-
throw new Error("Apple Web Client ID not configured. Add 'APPLE_WEB_CLIENT_ID' to your .env file.");
|
|
778
|
+
async ensureAppleSdkLoaded() {
|
|
779
|
+
if (window.AppleID) {
|
|
780
|
+
return;
|
|
491
781
|
}
|
|
492
|
-
|
|
782
|
+
if (this._appleSdkLoadPromise) {
|
|
783
|
+
return this._appleSdkLoadPromise;
|
|
784
|
+
}
|
|
785
|
+
this._appleSdkLoadPromise = new Promise((resolve, reject) => {
|
|
786
|
+
const scriptId = "nitro-auth-apple-sdk";
|
|
787
|
+
const existingScript = document.getElementById(scriptId);
|
|
788
|
+
if (existingScript) {
|
|
789
|
+
if (window.AppleID) {
|
|
790
|
+
resolve();
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
existingScript.addEventListener("load", () => {
|
|
794
|
+
resolve();
|
|
795
|
+
}, {
|
|
796
|
+
once: true
|
|
797
|
+
});
|
|
798
|
+
existingScript.addEventListener("error", () => {
|
|
799
|
+
this._appleSdkLoadPromise = undefined;
|
|
800
|
+
reject(new Error("Failed to load Apple SDK"));
|
|
801
|
+
}, {
|
|
802
|
+
once: true
|
|
803
|
+
});
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
493
806
|
const script = document.createElement("script");
|
|
807
|
+
script.id = scriptId;
|
|
494
808
|
script.src = "https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js";
|
|
495
809
|
script.async = true;
|
|
496
810
|
script.onload = () => {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
clientId,
|
|
503
|
-
scope: "name email",
|
|
504
|
-
redirectURI: window.location.origin,
|
|
505
|
-
usePopup: true
|
|
506
|
-
});
|
|
507
|
-
window.AppleID.auth.signIn().then(response => {
|
|
508
|
-
const user = {
|
|
509
|
-
provider: "apple",
|
|
510
|
-
idToken: response.authorization.id_token,
|
|
511
|
-
email: response.user?.email,
|
|
512
|
-
name: response.user?.name ? `${response.user.name.firstName} ${response.user.name.lastName}`.trim() : undefined
|
|
513
|
-
};
|
|
514
|
-
this.updateUser(user);
|
|
515
|
-
resolve();
|
|
516
|
-
}).catch(err => reject(this.mapError(err)));
|
|
811
|
+
resolve();
|
|
812
|
+
};
|
|
813
|
+
script.onerror = () => {
|
|
814
|
+
this._appleSdkLoadPromise = undefined;
|
|
815
|
+
reject(new Error("Failed to load Apple SDK"));
|
|
517
816
|
};
|
|
518
|
-
script.onerror = () => reject(new Error("Failed to load Apple SDK"));
|
|
519
817
|
document.head.appendChild(script);
|
|
520
818
|
});
|
|
819
|
+
return this._appleSdkLoadPromise;
|
|
820
|
+
}
|
|
821
|
+
async loginApple() {
|
|
822
|
+
const clientId = this._config.appleWebClientId;
|
|
823
|
+
if (!clientId) {
|
|
824
|
+
throw new Error("Apple Web Client ID not configured. Add 'APPLE_WEB_CLIENT_ID' to your .env file.");
|
|
825
|
+
}
|
|
826
|
+
await this.ensureAppleSdkLoaded();
|
|
827
|
+
if (!window.AppleID) {
|
|
828
|
+
throw new Error("Apple SDK not loaded");
|
|
829
|
+
}
|
|
830
|
+
window.AppleID.auth.init({
|
|
831
|
+
clientId,
|
|
832
|
+
scope: "name email",
|
|
833
|
+
redirectURI: window.location.origin,
|
|
834
|
+
usePopup: true
|
|
835
|
+
});
|
|
836
|
+
try {
|
|
837
|
+
const response = await window.AppleID.auth.signIn();
|
|
838
|
+
const user = {
|
|
839
|
+
provider: "apple",
|
|
840
|
+
idToken: response.authorization.id_token,
|
|
841
|
+
email: response.user?.email,
|
|
842
|
+
name: response.user?.name ? `${response.user.name.firstName} ${response.user.name.lastName}`.trim() : undefined
|
|
843
|
+
};
|
|
844
|
+
this.updateUser(user);
|
|
845
|
+
} catch (error) {
|
|
846
|
+
throw this.mapError(error);
|
|
847
|
+
}
|
|
521
848
|
}
|
|
522
849
|
async silentRestore() {
|
|
523
850
|
_logger.logger.log("Attempting silent restore...");
|
|
@@ -537,27 +864,22 @@ class AuthWeb {
|
|
|
537
864
|
this._grantedScopes = [];
|
|
538
865
|
this.removeFromCache(CACHE_KEY);
|
|
539
866
|
this.removeFromCache(SCOPES_KEY);
|
|
540
|
-
this.removeFromCache(
|
|
867
|
+
this.removeFromCache(MS_REFRESH_TOKEN_KEY);
|
|
541
868
|
this.notify();
|
|
542
869
|
}
|
|
543
870
|
updateUser(user) {
|
|
544
871
|
this._currentUser = user;
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
} else {
|
|
548
|
-
localStorage.setItem(CACHE_KEY, JSON.stringify(user));
|
|
549
|
-
}
|
|
872
|
+
const userToPersist = this.sanitizeUserForPersistence(user);
|
|
873
|
+
this.saveValue(CACHE_KEY, JSON.stringify(userToPersist));
|
|
550
874
|
this.notify();
|
|
551
875
|
}
|
|
552
876
|
setLoggingEnabled(enabled) {
|
|
553
877
|
_logger.logger.setEnabled(enabled);
|
|
554
878
|
}
|
|
555
|
-
|
|
556
|
-
this._storageAdapter = adapter;
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
this.notify();
|
|
560
|
-
}
|
|
879
|
+
setWebStorageAdapter(adapter) {
|
|
880
|
+
this._storageAdapter = adapter ? this.createWebStorageDriver(adapter) : undefined;
|
|
881
|
+
this.loadFromCache();
|
|
882
|
+
this.notify();
|
|
561
883
|
}
|
|
562
884
|
name = "Auth";
|
|
563
885
|
dispose() {}
|