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