react-native-nitro-auth 0.5.0 → 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.
- package/README.md +374 -189
- package/android/src/main/cpp/PlatformAuth+Android.cpp +4 -1
- package/android/src/main/java/com/auth/AuthAdapter.kt +99 -182
- package/android/src/main/java/com/auth/GoogleSignInActivity.kt +2 -1
- package/android/src/main/java/com/auth/NitroAuthPackage.kt +2 -0
- package/app.plugin.js +2 -9
- package/cpp/AuthCache.cpp +12 -102
- package/cpp/HybridAuth.cpp +37 -61
- package/cpp/HybridAuth.hpp +2 -4
- package/ios/AuthAdapter.swift +21 -25
- package/lib/commonjs/Auth.web.js +433 -164
- 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 +7 -84
- 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 +8 -2
- 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 +433 -164
- 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 +7 -84
- 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 +8 -2
- 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 +5 -3
- package/lib/typescript/commonjs/Auth.nitro.d.ts.map +1 -1
- package/lib/typescript/commonjs/Auth.web.d.ts +18 -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 +5 -3
- package/lib/typescript/module/Auth.nitro.d.ts.map +1 -1
- package/lib/typescript/module/Auth.web.d.ts +18 -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/nitrogen/generated/shared/c++/LoginOptions.hpp +5 -1
- package/package.json +13 -10
- package/src/Auth.nitro.ts +6 -3
- package/src/Auth.web.ts +582 -202
- 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 +11 -104
- 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 +18 -6
- package/src/utils/logger.ts +12 -4
- 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
|
};
|
|
@@ -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.
|
|
236
|
+
const cached = this.loadValue(CACHE_KEY);
|
|
29
237
|
if (cached) {
|
|
30
238
|
try {
|
|
31
|
-
|
|
32
|
-
|
|
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.
|
|
261
|
+
const scopes = this.loadValue(SCOPES_KEY);
|
|
37
262
|
if (scopes) {
|
|
38
263
|
try {
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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 =>
|
|
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
|
-
|
|
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.
|
|
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
|
|
404
|
+
const json = await this.parseResponseObject(response);
|
|
173
405
|
if (!response.ok) {
|
|
174
|
-
throw new Error(json
|
|
406
|
+
throw new Error(getOptionalString(json, "error_description") ?? getOptionalString(json, "error") ?? "Token refresh failed");
|
|
175
407
|
}
|
|
176
|
-
const idToken = json
|
|
177
|
-
const accessToken = json
|
|
178
|
-
const newRefreshToken = json
|
|
179
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 =>
|
|
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 =>
|
|
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
|
|
227
|
-
|
|
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",
|
|
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
|
-
|
|
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);
|
|
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
|
|
304
|
-
const decoded = JSON.parse(atob(payload));
|
|
592
|
+
const decoded = this.parseJwtPayload(token);
|
|
305
593
|
return {
|
|
306
|
-
email: decoded
|
|
307
|
-
name: decoded
|
|
308
|
-
photo: decoded
|
|
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
|
-
|
|
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);
|
|
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
|
|
700
|
+
const json = await this.parseResponseObject(response);
|
|
426
701
|
if (!response.ok) {
|
|
427
|
-
throw new Error(json
|
|
702
|
+
throw new Error(getOptionalString(json, "error_description") ?? getOptionalString(json, "error") ?? "Token exchange failed");
|
|
428
703
|
}
|
|
429
|
-
const idToken = json
|
|
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 =
|
|
435
|
-
if (payload
|
|
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
|
|
439
|
-
const refreshToken = json
|
|
440
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
477
|
-
const decoded = JSON.parse(atob(payload));
|
|
744
|
+
const decoded = this.parseJwtPayload(token);
|
|
478
745
|
return {
|
|
479
|
-
email: decoded
|
|
480
|
-
name: decoded
|
|
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 =>
|
|
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(
|
|
814
|
+
this.removeFromCache(MS_REFRESH_TOKEN_KEY);
|
|
541
815
|
this.notify();
|
|
542
816
|
}
|
|
543
817
|
updateUser(user) {
|
|
544
818
|
this._currentUser = user;
|
|
545
|
-
|
|
546
|
-
|
|
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
|
-
|
|
556
|
-
this._storageAdapter = adapter;
|
|
557
|
-
|
|
558
|
-
|
|
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() {}
|