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