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/src/Auth.web.ts
CHANGED
|
@@ -5,13 +5,31 @@ import type {
|
|
|
5
5
|
LoginOptions,
|
|
6
6
|
AuthTokens,
|
|
7
7
|
} from "./Auth.nitro";
|
|
8
|
-
import type {
|
|
8
|
+
import type { JSStorageAdapter } from "./js-storage-adapter";
|
|
9
9
|
import { logger } from "./utils/logger";
|
|
10
10
|
|
|
11
11
|
const CACHE_KEY = "nitro_auth_user";
|
|
12
12
|
const SCOPES_KEY = "nitro_auth_scopes";
|
|
13
|
+
const MS_REFRESH_TOKEN_KEY = "nitro_auth_microsoft_refresh_token";
|
|
13
14
|
const DEFAULT_SCOPES = ["openid", "email", "profile"];
|
|
14
15
|
const MS_DEFAULT_SCOPES = ["openid", "email", "profile", "User.Read"];
|
|
16
|
+
const STORAGE_MODE_SESSION = "session";
|
|
17
|
+
const STORAGE_MODE_LOCAL = "local";
|
|
18
|
+
const STORAGE_MODE_MEMORY = "memory";
|
|
19
|
+
const POPUP_POLL_INTERVAL_MS = 100;
|
|
20
|
+
const POPUP_TIMEOUT_MS = 120000;
|
|
21
|
+
const WEB_STORAGE_MODES = new Set([
|
|
22
|
+
STORAGE_MODE_SESSION,
|
|
23
|
+
STORAGE_MODE_LOCAL,
|
|
24
|
+
STORAGE_MODE_MEMORY,
|
|
25
|
+
] as const);
|
|
26
|
+
const inMemoryWebStorage = new Map<string, string>();
|
|
27
|
+
|
|
28
|
+
type WebStorageDriver = {
|
|
29
|
+
save(key: string, value: string): void;
|
|
30
|
+
load(key: string): string | undefined;
|
|
31
|
+
remove(key: string): void;
|
|
32
|
+
};
|
|
15
33
|
|
|
16
34
|
type AppleAuthResponse = {
|
|
17
35
|
authorization: {
|
|
@@ -26,11 +44,123 @@ type AppleAuthResponse = {
|
|
|
26
44
|
};
|
|
27
45
|
};
|
|
28
46
|
|
|
29
|
-
|
|
47
|
+
type AuthWebExtraConfig = {
|
|
48
|
+
googleWebClientId?: string;
|
|
49
|
+
microsoftClientId?: string;
|
|
50
|
+
microsoftTenant?: string;
|
|
51
|
+
microsoftB2cDomain?: string;
|
|
52
|
+
appleWebClientId?: string;
|
|
53
|
+
nitroAuthWebStorage?: "session" | "local" | "memory";
|
|
54
|
+
nitroAuthPersistTokensOnWeb?: boolean;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
type JsonObject = Record<string, unknown>;
|
|
58
|
+
|
|
59
|
+
class AuthWebError extends Error {
|
|
60
|
+
public readonly underlyingError?: string;
|
|
61
|
+
|
|
62
|
+
constructor(message: string, underlyingError?: string) {
|
|
63
|
+
super(message);
|
|
64
|
+
this.name = "AuthWebError";
|
|
65
|
+
this.underlyingError = underlyingError;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const isJsonObject = (value: unknown): value is JsonObject =>
|
|
70
|
+
typeof value === "object" && value !== null;
|
|
71
|
+
|
|
72
|
+
const isAuthProvider = (value: unknown): value is AuthProvider =>
|
|
73
|
+
value === "google" || value === "apple" || value === "microsoft";
|
|
74
|
+
|
|
75
|
+
const getOptionalString = (
|
|
76
|
+
source: JsonObject,
|
|
77
|
+
key: string,
|
|
78
|
+
): string | undefined => {
|
|
79
|
+
const candidate = source[key];
|
|
80
|
+
return typeof candidate === "string" ? candidate : undefined;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const getOptionalNumber = (
|
|
84
|
+
source: JsonObject,
|
|
85
|
+
key: string,
|
|
86
|
+
): number | undefined => {
|
|
87
|
+
const candidate = source[key];
|
|
88
|
+
return typeof candidate === "number" ? candidate : undefined;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const parseAuthUser = (value: unknown): AuthUser | undefined => {
|
|
92
|
+
if (!isJsonObject(value) || !isAuthProvider(value.provider)) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const scopesCandidate = value.scopes;
|
|
97
|
+
const scopes = Array.isArray(scopesCandidate)
|
|
98
|
+
? scopesCandidate.filter(
|
|
99
|
+
(scope): scope is string => typeof scope === "string",
|
|
100
|
+
)
|
|
101
|
+
: undefined;
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
provider: value.provider,
|
|
105
|
+
email: getOptionalString(value, "email"),
|
|
106
|
+
name: getOptionalString(value, "name"),
|
|
107
|
+
photo: getOptionalString(value, "photo"),
|
|
108
|
+
idToken: getOptionalString(value, "idToken"),
|
|
109
|
+
accessToken: getOptionalString(value, "accessToken"),
|
|
110
|
+
refreshToken: getOptionalString(value, "refreshToken"),
|
|
111
|
+
serverAuthCode: getOptionalString(value, "serverAuthCode"),
|
|
112
|
+
scopes,
|
|
113
|
+
expirationTime: getOptionalNumber(value, "expirationTime"),
|
|
114
|
+
underlyingError: getOptionalString(value, "underlyingError"),
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const parseScopes = (value: unknown): string[] | undefined => {
|
|
119
|
+
if (!Array.isArray(value)) {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return value.filter((scope): scope is string => typeof scope === "string");
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const parseAuthWebExtraConfig = (value: unknown): AuthWebExtraConfig => {
|
|
127
|
+
if (!isJsonObject(value)) {
|
|
128
|
+
return {};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const nitroAuthWebStorageCandidate = value.nitroAuthWebStorage;
|
|
132
|
+
const nitroAuthWebStorage =
|
|
133
|
+
nitroAuthWebStorageCandidate === STORAGE_MODE_SESSION ||
|
|
134
|
+
nitroAuthWebStorageCandidate === STORAGE_MODE_LOCAL ||
|
|
135
|
+
nitroAuthWebStorageCandidate === STORAGE_MODE_MEMORY
|
|
136
|
+
? nitroAuthWebStorageCandidate
|
|
137
|
+
: undefined;
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
googleWebClientId: getOptionalString(value, "googleWebClientId"),
|
|
141
|
+
microsoftClientId: getOptionalString(value, "microsoftClientId"),
|
|
142
|
+
microsoftTenant: getOptionalString(value, "microsoftTenant"),
|
|
143
|
+
microsoftB2cDomain: getOptionalString(value, "microsoftB2cDomain"),
|
|
144
|
+
appleWebClientId: getOptionalString(value, "appleWebClientId"),
|
|
145
|
+
nitroAuthWebStorage,
|
|
146
|
+
nitroAuthPersistTokensOnWeb:
|
|
147
|
+
typeof value.nitroAuthPersistTokensOnWeb === "boolean"
|
|
148
|
+
? value.nitroAuthPersistTokensOnWeb
|
|
149
|
+
: undefined,
|
|
150
|
+
};
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const getConfig = (): AuthWebExtraConfig => {
|
|
30
154
|
try {
|
|
31
155
|
const Constants = require("expo-constants").default;
|
|
32
|
-
return Constants.expoConfig?.extra
|
|
33
|
-
} catch {
|
|
156
|
+
return parseAuthWebExtraConfig(Constants.expoConfig?.extra);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
logger.debug(
|
|
159
|
+
"expo-constants unavailable on web, falling back to defaults",
|
|
160
|
+
{
|
|
161
|
+
error: String(error),
|
|
162
|
+
},
|
|
163
|
+
);
|
|
34
164
|
return {};
|
|
35
165
|
}
|
|
36
166
|
};
|
|
@@ -40,44 +170,223 @@ class AuthWeb implements Auth {
|
|
|
40
170
|
private _grantedScopes: string[] = [];
|
|
41
171
|
private _listeners: ((user: AuthUser | undefined) => void)[] = [];
|
|
42
172
|
private _tokenListeners: ((tokens: AuthTokens) => void)[] = [];
|
|
43
|
-
private _storageAdapter:
|
|
173
|
+
private _storageAdapter: WebStorageDriver | undefined;
|
|
44
174
|
|
|
45
175
|
constructor() {
|
|
46
176
|
this.loadFromCache();
|
|
47
177
|
}
|
|
48
178
|
|
|
179
|
+
private isPromiseLike(value: unknown): value is PromiseLike<unknown> {
|
|
180
|
+
if (!isJsonObject(value)) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
return typeof value.then === "function";
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private createWebStorageDriver(adapter: JSStorageAdapter): WebStorageDriver {
|
|
187
|
+
return {
|
|
188
|
+
save: (key, value) => {
|
|
189
|
+
const result = adapter.save(key, value);
|
|
190
|
+
if (this.isPromiseLike(result)) {
|
|
191
|
+
throw new Error("On web, JSStorageAdapter.save must be synchronous.");
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
load: (key) => {
|
|
195
|
+
const result = adapter.load(key);
|
|
196
|
+
if (this.isPromiseLike(result)) {
|
|
197
|
+
throw new Error("On web, JSStorageAdapter.load must be synchronous.");
|
|
198
|
+
}
|
|
199
|
+
return result;
|
|
200
|
+
},
|
|
201
|
+
remove: (key) => {
|
|
202
|
+
const result = adapter.remove(key);
|
|
203
|
+
if (this.isPromiseLike(result)) {
|
|
204
|
+
throw new Error(
|
|
205
|
+
"On web, JSStorageAdapter.remove must be synchronous.",
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private shouldPersistTokensInStorage(): boolean {
|
|
213
|
+
if (this._storageAdapter) {
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
return getConfig().nitroAuthPersistTokensOnWeb === true;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private getWebStorageMode(): "session" | "local" | "memory" {
|
|
220
|
+
const configuredMode = getConfig().nitroAuthWebStorage;
|
|
221
|
+
if (configuredMode && WEB_STORAGE_MODES.has(configuredMode)) {
|
|
222
|
+
return configuredMode;
|
|
223
|
+
}
|
|
224
|
+
return STORAGE_MODE_SESSION;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private getBrowserStorage(): Storage | undefined {
|
|
228
|
+
if (typeof window === "undefined") {
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const mode = this.getWebStorageMode();
|
|
233
|
+
if (mode === STORAGE_MODE_MEMORY) {
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const storage =
|
|
238
|
+
mode === STORAGE_MODE_LOCAL ? window.localStorage : window.sessionStorage;
|
|
239
|
+
try {
|
|
240
|
+
const testKey = "__nitro_auth_storage_probe__";
|
|
241
|
+
storage.setItem(testKey, "1");
|
|
242
|
+
storage.removeItem(testKey);
|
|
243
|
+
return storage;
|
|
244
|
+
} catch (error) {
|
|
245
|
+
logger.warn(
|
|
246
|
+
"Configured web storage is unavailable; using in-memory fallback",
|
|
247
|
+
{
|
|
248
|
+
mode,
|
|
249
|
+
error: String(error),
|
|
250
|
+
},
|
|
251
|
+
);
|
|
252
|
+
return undefined;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private saveValue(key: string, value: string): void {
|
|
257
|
+
if (this._storageAdapter) {
|
|
258
|
+
this._storageAdapter.save(key, value);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const storage = this.getBrowserStorage();
|
|
263
|
+
if (storage) {
|
|
264
|
+
storage.setItem(key, value);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
inMemoryWebStorage.set(key, value);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private loadValue(key: string): string | undefined {
|
|
271
|
+
if (this._storageAdapter) {
|
|
272
|
+
return this._storageAdapter.load(key);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const storage = this.getBrowserStorage();
|
|
276
|
+
if (storage) {
|
|
277
|
+
return storage.getItem(key) ?? undefined;
|
|
278
|
+
}
|
|
279
|
+
return inMemoryWebStorage.get(key);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private removeValue(key: string): void {
|
|
283
|
+
if (this._storageAdapter) {
|
|
284
|
+
this._storageAdapter.remove(key);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const storage = this.getBrowserStorage();
|
|
289
|
+
if (storage) {
|
|
290
|
+
storage.removeItem(key);
|
|
291
|
+
}
|
|
292
|
+
inMemoryWebStorage.delete(key);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private removePersistedBrowserValue(key: string): void {
|
|
296
|
+
if (typeof window === "undefined") {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
window.localStorage.removeItem(key);
|
|
302
|
+
window.sessionStorage.removeItem(key);
|
|
303
|
+
} catch (error) {
|
|
304
|
+
logger.debug("Failed to clear persisted browser value", {
|
|
305
|
+
key,
|
|
306
|
+
error: String(error),
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private sanitizeUserForPersistence(user: AuthUser): AuthUser {
|
|
312
|
+
if (this.shouldPersistTokensInStorage()) {
|
|
313
|
+
return user;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const safeUser = { ...user };
|
|
317
|
+
delete safeUser.accessToken;
|
|
318
|
+
delete safeUser.idToken;
|
|
319
|
+
delete safeUser.serverAuthCode;
|
|
320
|
+
return safeUser;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private saveRefreshToken(refreshToken: string): void {
|
|
324
|
+
if (this._storageAdapter || this.shouldPersistTokensInStorage()) {
|
|
325
|
+
this.saveValue(MS_REFRESH_TOKEN_KEY, refreshToken);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Security-first default: keep refresh tokens in-memory only on web.
|
|
330
|
+
inMemoryWebStorage.set(MS_REFRESH_TOKEN_KEY, refreshToken);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private loadRefreshToken(): string | undefined {
|
|
334
|
+
if (this._storageAdapter || this.shouldPersistTokensInStorage()) {
|
|
335
|
+
return this.loadValue(MS_REFRESH_TOKEN_KEY);
|
|
336
|
+
}
|
|
337
|
+
return inMemoryWebStorage.get(MS_REFRESH_TOKEN_KEY);
|
|
338
|
+
}
|
|
339
|
+
|
|
49
340
|
private loadFromCache() {
|
|
50
|
-
const cached = this.
|
|
51
|
-
? this._storageAdapter.load(CACHE_KEY)
|
|
52
|
-
: localStorage.getItem(CACHE_KEY);
|
|
341
|
+
const cached = this.loadValue(CACHE_KEY);
|
|
53
342
|
|
|
54
343
|
if (cached) {
|
|
55
344
|
try {
|
|
56
|
-
|
|
57
|
-
|
|
345
|
+
const parsedUser = parseAuthUser(JSON.parse(cached));
|
|
346
|
+
if (!parsedUser) {
|
|
347
|
+
throw new Error("Expected cached auth user to be a valid AuthUser");
|
|
348
|
+
}
|
|
349
|
+
if (this.shouldPersistTokensInStorage()) {
|
|
350
|
+
this._currentUser = parsedUser;
|
|
351
|
+
} else {
|
|
352
|
+
const safeUser = { ...parsedUser };
|
|
353
|
+
delete safeUser.accessToken;
|
|
354
|
+
delete safeUser.idToken;
|
|
355
|
+
delete safeUser.serverAuthCode;
|
|
356
|
+
this._currentUser = safeUser;
|
|
357
|
+
}
|
|
358
|
+
} catch (error) {
|
|
359
|
+
logger.warn("Failed to parse cached auth user; clearing cache", {
|
|
360
|
+
error: String(error),
|
|
361
|
+
});
|
|
58
362
|
this.removeFromCache(CACHE_KEY);
|
|
59
363
|
}
|
|
60
364
|
}
|
|
61
365
|
|
|
62
|
-
const scopes = this.
|
|
63
|
-
? this._storageAdapter.load(SCOPES_KEY)
|
|
64
|
-
: localStorage.getItem(SCOPES_KEY);
|
|
366
|
+
const scopes = this.loadValue(SCOPES_KEY);
|
|
65
367
|
|
|
66
368
|
if (scopes) {
|
|
67
369
|
try {
|
|
68
|
-
|
|
69
|
-
|
|
370
|
+
const parsedScopes = parseScopes(JSON.parse(scopes));
|
|
371
|
+
if (!parsedScopes) {
|
|
372
|
+
throw new Error("Expected cached scopes to be an array");
|
|
373
|
+
}
|
|
374
|
+
this._grantedScopes = parsedScopes;
|
|
375
|
+
} catch (error) {
|
|
376
|
+
logger.warn("Failed to parse cached scopes; clearing cache", {
|
|
377
|
+
error: String(error),
|
|
378
|
+
});
|
|
70
379
|
this.removeFromCache(SCOPES_KEY);
|
|
71
380
|
}
|
|
72
381
|
}
|
|
382
|
+
|
|
383
|
+
if (!this.shouldPersistTokensInStorage() && !this._storageAdapter) {
|
|
384
|
+
this.removePersistedBrowserValue(MS_REFRESH_TOKEN_KEY);
|
|
385
|
+
}
|
|
73
386
|
}
|
|
74
387
|
|
|
75
388
|
private removeFromCache(key: string) {
|
|
76
|
-
|
|
77
|
-
this._storageAdapter.remove(key);
|
|
78
|
-
} else {
|
|
79
|
-
localStorage.removeItem(key);
|
|
80
|
-
}
|
|
389
|
+
this.removeValue(key);
|
|
81
390
|
}
|
|
82
391
|
|
|
83
392
|
get currentUser(): AuthUser | undefined {
|
|
@@ -110,7 +419,9 @@ class AuthWeb implements Auth {
|
|
|
110
419
|
}
|
|
111
420
|
|
|
112
421
|
private notify() {
|
|
113
|
-
this._listeners.forEach((l) =>
|
|
422
|
+
this._listeners.forEach((l) => {
|
|
423
|
+
l(this._currentUser);
|
|
424
|
+
});
|
|
114
425
|
}
|
|
115
426
|
|
|
116
427
|
async login(provider: AuthProvider, options?: LoginOptions): Promise<void> {
|
|
@@ -169,14 +480,7 @@ class AuthWeb implements Auth {
|
|
|
169
480
|
this._grantedScopes = this._grantedScopes.filter(
|
|
170
481
|
(s) => !scopes.includes(s),
|
|
171
482
|
);
|
|
172
|
-
|
|
173
|
-
this._storageAdapter.save(
|
|
174
|
-
SCOPES_KEY,
|
|
175
|
-
JSON.stringify(this._grantedScopes),
|
|
176
|
-
);
|
|
177
|
-
} else {
|
|
178
|
-
localStorage.setItem(SCOPES_KEY, JSON.stringify(this._grantedScopes));
|
|
179
|
-
}
|
|
483
|
+
this.saveValue(SCOPES_KEY, JSON.stringify(this._grantedScopes));
|
|
180
484
|
if (this._currentUser) {
|
|
181
485
|
this._currentUser.scopes = this._grantedScopes;
|
|
182
486
|
this.updateUser(this._currentUser);
|
|
@@ -194,16 +498,14 @@ class AuthWeb implements Auth {
|
|
|
194
498
|
return this._currentUser?.accessToken;
|
|
195
499
|
}
|
|
196
500
|
|
|
197
|
-
async refreshToken(): Promise<
|
|
501
|
+
async refreshToken(): Promise<AuthTokens> {
|
|
198
502
|
if (!this._currentUser) {
|
|
199
503
|
throw new Error("No user logged in");
|
|
200
504
|
}
|
|
201
505
|
|
|
202
506
|
if (this._currentUser.provider === "microsoft") {
|
|
203
507
|
logger.log("Refreshing Microsoft tokens...");
|
|
204
|
-
const refreshToken = this.
|
|
205
|
-
? this._storageAdapter.load("microsoft_refresh_token")
|
|
206
|
-
: localStorage.getItem("nitro_auth_microsoft_refresh_token");
|
|
508
|
+
const refreshToken = this.loadRefreshToken();
|
|
207
509
|
|
|
208
510
|
if (!refreshToken) {
|
|
209
511
|
throw new Error("No refresh token available");
|
|
@@ -211,6 +513,11 @@ class AuthWeb implements Auth {
|
|
|
211
513
|
|
|
212
514
|
const config = getConfig();
|
|
213
515
|
const clientId = config.microsoftClientId;
|
|
516
|
+
if (!clientId) {
|
|
517
|
+
throw new Error(
|
|
518
|
+
"Microsoft Client ID not configured. Add 'microsoftClientId' to expo.extra in your app.config.js",
|
|
519
|
+
);
|
|
520
|
+
}
|
|
214
521
|
const tenant = config.microsoftTenant ?? "common";
|
|
215
522
|
const b2cDomain = config.microsoftB2cDomain;
|
|
216
523
|
|
|
@@ -230,43 +537,52 @@ class AuthWeb implements Auth {
|
|
|
230
537
|
body: body.toString(),
|
|
231
538
|
});
|
|
232
539
|
|
|
233
|
-
const json = await
|
|
540
|
+
const json = await this.parseResponseObject(response);
|
|
234
541
|
if (!response.ok) {
|
|
235
542
|
throw new Error(
|
|
236
|
-
json
|
|
543
|
+
getOptionalString(json, "error_description") ??
|
|
544
|
+
getOptionalString(json, "error") ??
|
|
545
|
+
"Token refresh failed",
|
|
237
546
|
);
|
|
238
547
|
}
|
|
239
548
|
|
|
240
|
-
const idToken = json
|
|
241
|
-
const accessToken = json
|
|
242
|
-
const newRefreshToken = json
|
|
243
|
-
const
|
|
549
|
+
const idToken = getOptionalString(json, "id_token");
|
|
550
|
+
const accessToken = getOptionalString(json, "access_token");
|
|
551
|
+
const newRefreshToken = getOptionalString(json, "refresh_token");
|
|
552
|
+
const expiresInSeconds = getOptionalNumber(json, "expires_in");
|
|
244
553
|
|
|
245
554
|
if (newRefreshToken) {
|
|
246
|
-
|
|
247
|
-
this._storageAdapter.save("microsoft_refresh_token", newRefreshToken);
|
|
248
|
-
} else {
|
|
249
|
-
localStorage.setItem(
|
|
250
|
-
"nitro_auth_microsoft_refresh_token",
|
|
251
|
-
newRefreshToken,
|
|
252
|
-
);
|
|
253
|
-
}
|
|
555
|
+
this.saveRefreshToken(newRefreshToken);
|
|
254
556
|
}
|
|
255
557
|
|
|
256
|
-
const
|
|
558
|
+
const expirationTime =
|
|
559
|
+
typeof expiresInSeconds === "number"
|
|
560
|
+
? Date.now() + expiresInSeconds * 1000
|
|
561
|
+
: undefined;
|
|
562
|
+
|
|
563
|
+
const effectiveIdToken = idToken ?? this._currentUser.idToken;
|
|
564
|
+
const claims = effectiveIdToken
|
|
565
|
+
? this.decodeMicrosoftJwt(effectiveIdToken)
|
|
566
|
+
: {};
|
|
257
567
|
const user: AuthUser = {
|
|
258
568
|
...this._currentUser,
|
|
259
|
-
idToken,
|
|
569
|
+
idToken: effectiveIdToken,
|
|
260
570
|
accessToken: accessToken ?? undefined,
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
: undefined,
|
|
571
|
+
refreshToken: newRefreshToken ?? this._currentUser.refreshToken,
|
|
572
|
+
expirationTime,
|
|
264
573
|
...claims,
|
|
265
574
|
};
|
|
266
575
|
this.updateUser(user);
|
|
267
576
|
|
|
268
|
-
const tokens = {
|
|
269
|
-
|
|
577
|
+
const tokens: AuthTokens = {
|
|
578
|
+
accessToken: accessToken ?? undefined,
|
|
579
|
+
idToken: effectiveIdToken,
|
|
580
|
+
refreshToken: newRefreshToken ?? undefined,
|
|
581
|
+
expirationTime,
|
|
582
|
+
};
|
|
583
|
+
this._tokenListeners.forEach((l) => {
|
|
584
|
+
l(tokens);
|
|
585
|
+
});
|
|
270
586
|
return tokens;
|
|
271
587
|
}
|
|
272
588
|
|
|
@@ -280,11 +596,15 @@ class AuthWeb implements Auth {
|
|
|
280
596
|
await this.loginGoogle(
|
|
281
597
|
this._grantedScopes.length > 0 ? this._grantedScopes : DEFAULT_SCOPES,
|
|
282
598
|
);
|
|
283
|
-
const tokens = {
|
|
599
|
+
const tokens: AuthTokens = {
|
|
284
600
|
accessToken: this._currentUser.accessToken,
|
|
285
601
|
idToken: this._currentUser.idToken,
|
|
602
|
+
refreshToken: this._currentUser.refreshToken,
|
|
603
|
+
expirationTime: this._currentUser.expirationTime,
|
|
286
604
|
};
|
|
287
|
-
this._tokenListeners.forEach((l) =>
|
|
605
|
+
this._tokenListeners.forEach((l) => {
|
|
606
|
+
l(tokens);
|
|
607
|
+
});
|
|
288
608
|
return tokens;
|
|
289
609
|
}
|
|
290
610
|
|
|
@@ -295,13 +615,100 @@ class AuthWeb implements Auth {
|
|
|
295
615
|
|
|
296
616
|
if (msg.includes("cancel") || msg.includes("popup_closed")) {
|
|
297
617
|
mappedMsg = "cancelled";
|
|
618
|
+
} else if (msg.includes("timeout")) {
|
|
619
|
+
mappedMsg = "timeout";
|
|
620
|
+
} else if (msg.includes("popup blocked")) {
|
|
621
|
+
mappedMsg = "popup_blocked";
|
|
298
622
|
} else if (msg.includes("network")) {
|
|
299
623
|
mappedMsg = "network_error";
|
|
300
624
|
} else if (msg.includes("client id") || msg.includes("config")) {
|
|
301
625
|
mappedMsg = "configuration_error";
|
|
302
626
|
}
|
|
303
627
|
|
|
304
|
-
return
|
|
628
|
+
return new AuthWebError(mappedMsg, rawMessage);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
private async parseResponseObject(response: Response): Promise<JsonObject> {
|
|
632
|
+
const parsed: unknown = await response.json();
|
|
633
|
+
if (!isJsonObject(parsed)) {
|
|
634
|
+
throw new Error("Expected JSON object response from auth provider");
|
|
635
|
+
}
|
|
636
|
+
return parsed;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
private parseJwtPayload(token: string): JsonObject {
|
|
640
|
+
const payload = token.split(".")[1];
|
|
641
|
+
if (!payload) {
|
|
642
|
+
throw new Error("Invalid JWT payload");
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const decoded: unknown = JSON.parse(atob(payload));
|
|
646
|
+
if (!isJsonObject(decoded)) {
|
|
647
|
+
throw new Error("Expected JWT payload to be an object");
|
|
648
|
+
}
|
|
649
|
+
return decoded;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
private waitForPopupRedirect(
|
|
653
|
+
popup: Window,
|
|
654
|
+
redirectUri: string,
|
|
655
|
+
provider: "Google" | "Microsoft",
|
|
656
|
+
onRedirect: (url: string) => Promise<void> | void,
|
|
657
|
+
): Promise<void> {
|
|
658
|
+
return new Promise((resolve, reject) => {
|
|
659
|
+
let crossOriginLogShown = false;
|
|
660
|
+
|
|
661
|
+
const cleanup = (
|
|
662
|
+
intervalId: number,
|
|
663
|
+
timeoutId: number,
|
|
664
|
+
shouldClosePopup: boolean,
|
|
665
|
+
) => {
|
|
666
|
+
window.clearInterval(intervalId);
|
|
667
|
+
window.clearTimeout(timeoutId);
|
|
668
|
+
if (shouldClosePopup && !popup.closed) {
|
|
669
|
+
popup.close();
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
const timeoutId = window.setTimeout(() => {
|
|
674
|
+
cleanup(intervalId, timeoutId, true);
|
|
675
|
+
reject(new Error(`${provider.toLowerCase()}_auth_timeout`));
|
|
676
|
+
}, POPUP_TIMEOUT_MS);
|
|
677
|
+
|
|
678
|
+
const intervalId = window.setInterval(() => {
|
|
679
|
+
if (popup.closed) {
|
|
680
|
+
cleanup(intervalId, timeoutId, false);
|
|
681
|
+
reject(new Error("cancelled"));
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
let url: string;
|
|
686
|
+
try {
|
|
687
|
+
url = popup.location.href;
|
|
688
|
+
} catch (error) {
|
|
689
|
+
if (!crossOriginLogShown) {
|
|
690
|
+
logger.debug(`Waiting for ${provider} auth redirect`, {
|
|
691
|
+
error: String(error),
|
|
692
|
+
});
|
|
693
|
+
crossOriginLogShown = true;
|
|
694
|
+
}
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (!url.startsWith(redirectUri)) {
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
cleanup(intervalId, timeoutId, true);
|
|
703
|
+
void Promise.resolve(onRedirect(url))
|
|
704
|
+
.then(() => {
|
|
705
|
+
resolve();
|
|
706
|
+
})
|
|
707
|
+
.catch((error: unknown) => {
|
|
708
|
+
reject(error);
|
|
709
|
+
});
|
|
710
|
+
}, POPUP_POLL_INTERVAL_MS);
|
|
711
|
+
});
|
|
305
712
|
}
|
|
306
713
|
|
|
307
714
|
private async loginGoogle(
|
|
@@ -324,7 +731,7 @@ class AuthWeb implements Auth {
|
|
|
324
731
|
authUrl.searchParams.set("redirect_uri", redirectUri);
|
|
325
732
|
authUrl.searchParams.set("response_type", "id_token token code");
|
|
326
733
|
authUrl.searchParams.set("scope", scopes.join(" "));
|
|
327
|
-
authUrl.searchParams.set("nonce",
|
|
734
|
+
authUrl.searchParams.set("nonce", crypto.randomUUID());
|
|
328
735
|
authUrl.searchParams.set("access_type", "offline");
|
|
329
736
|
authUrl.searchParams.set("prompt", "consent");
|
|
330
737
|
|
|
@@ -348,66 +755,53 @@ class AuthWeb implements Auth {
|
|
|
348
755
|
return;
|
|
349
756
|
}
|
|
350
757
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
758
|
+
void this.waitForPopupRedirect(popup, redirectUri, "Google", (url) => {
|
|
759
|
+
const hash = new URL(url).hash.slice(1);
|
|
760
|
+
const params = new URLSearchParams(hash);
|
|
761
|
+
const idToken = params.get("id_token");
|
|
762
|
+
const accessToken = params.get("access_token");
|
|
763
|
+
const expiresIn = params.get("expires_in");
|
|
764
|
+
const code = params.get("code");
|
|
358
765
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
expirationTime: expiresIn
|
|
386
|
-
? Date.now() + parseInt(expiresIn) * 1000
|
|
387
|
-
: undefined,
|
|
388
|
-
...this.decodeGoogleJwt(idToken),
|
|
389
|
-
};
|
|
390
|
-
this.updateUser(user);
|
|
391
|
-
resolve();
|
|
392
|
-
} else {
|
|
393
|
-
reject(new Error("No id_token in response"));
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
} catch {}
|
|
397
|
-
}, 100);
|
|
766
|
+
if (!idToken) {
|
|
767
|
+
throw new Error("No id_token in response");
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
this._grantedScopes = scopes;
|
|
771
|
+
this.saveValue(SCOPES_KEY, JSON.stringify(scopes));
|
|
772
|
+
|
|
773
|
+
const user: AuthUser = {
|
|
774
|
+
provider: "google",
|
|
775
|
+
idToken,
|
|
776
|
+
accessToken: accessToken ?? undefined,
|
|
777
|
+
serverAuthCode: code ?? undefined,
|
|
778
|
+
scopes,
|
|
779
|
+
expirationTime: expiresIn
|
|
780
|
+
? Date.now() + parseInt(expiresIn, 10) * 1000
|
|
781
|
+
: undefined,
|
|
782
|
+
...this.decodeGoogleJwt(idToken),
|
|
783
|
+
};
|
|
784
|
+
this.updateUser(user);
|
|
785
|
+
})
|
|
786
|
+
.then(() => {
|
|
787
|
+
resolve();
|
|
788
|
+
})
|
|
789
|
+
.catch((error: unknown) => {
|
|
790
|
+
reject(error);
|
|
791
|
+
});
|
|
398
792
|
});
|
|
399
793
|
}
|
|
400
794
|
|
|
401
795
|
private decodeGoogleJwt(token: string): Partial<AuthUser> {
|
|
402
796
|
try {
|
|
403
|
-
const
|
|
404
|
-
const decoded = JSON.parse(atob(payload));
|
|
797
|
+
const decoded = this.parseJwtPayload(token);
|
|
405
798
|
return {
|
|
406
|
-
email: decoded
|
|
407
|
-
name: decoded
|
|
408
|
-
photo: decoded
|
|
799
|
+
email: getOptionalString(decoded, "email"),
|
|
800
|
+
name: getOptionalString(decoded, "name"),
|
|
801
|
+
photo: getOptionalString(decoded, "picture"),
|
|
409
802
|
};
|
|
410
|
-
} catch {
|
|
803
|
+
} catch (error) {
|
|
804
|
+
logger.warn("Failed to decode Google ID token", { error: String(error) });
|
|
411
805
|
return {};
|
|
412
806
|
}
|
|
413
807
|
}
|
|
@@ -477,58 +871,46 @@ class AuthWeb implements Auth {
|
|
|
477
871
|
return;
|
|
478
872
|
}
|
|
479
873
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
874
|
+
void this.waitForPopupRedirect(
|
|
875
|
+
popup,
|
|
876
|
+
redirectUri,
|
|
877
|
+
"Microsoft",
|
|
878
|
+
async (url) => {
|
|
879
|
+
const urlObj = new URL(url);
|
|
880
|
+
const code = urlObj.searchParams.get("code");
|
|
881
|
+
const returnedState = urlObj.searchParams.get("state");
|
|
882
|
+
const error = urlObj.searchParams.get("error");
|
|
883
|
+
const errorDescription = urlObj.searchParams.get("error_description");
|
|
884
|
+
|
|
885
|
+
if (error) {
|
|
886
|
+
throw new Error(errorDescription ?? error);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
if (returnedState !== state) {
|
|
890
|
+
throw new Error("State mismatch - possible CSRF attack");
|
|
486
891
|
}
|
|
487
892
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
clearInterval(checkInterval);
|
|
491
|
-
popup.close();
|
|
492
|
-
|
|
493
|
-
const urlObj = new URL(url);
|
|
494
|
-
const code = urlObj.searchParams.get("code");
|
|
495
|
-
const returnedState = urlObj.searchParams.get("state");
|
|
496
|
-
const error = urlObj.searchParams.get("error");
|
|
497
|
-
const errorDescription =
|
|
498
|
-
urlObj.searchParams.get("error_description");
|
|
499
|
-
|
|
500
|
-
if (error) {
|
|
501
|
-
reject(new Error(errorDescription ?? error));
|
|
502
|
-
return;
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
if (returnedState !== state) {
|
|
506
|
-
reject(new Error("State mismatch - possible CSRF attack"));
|
|
507
|
-
return;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
if (!code) {
|
|
511
|
-
reject(new Error("No authorization code in response"));
|
|
512
|
-
return;
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
try {
|
|
516
|
-
await this.exchangeMicrosoftCodeForTokens(
|
|
517
|
-
code,
|
|
518
|
-
codeVerifier,
|
|
519
|
-
clientId,
|
|
520
|
-
redirectUri,
|
|
521
|
-
effectiveTenant,
|
|
522
|
-
nonce,
|
|
523
|
-
effectiveScopes,
|
|
524
|
-
);
|
|
525
|
-
resolve();
|
|
526
|
-
} catch (e) {
|
|
527
|
-
reject(e);
|
|
528
|
-
}
|
|
893
|
+
if (!code) {
|
|
894
|
+
throw new Error("No authorization code in response");
|
|
529
895
|
}
|
|
530
|
-
|
|
531
|
-
|
|
896
|
+
|
|
897
|
+
await this.exchangeMicrosoftCodeForTokens(
|
|
898
|
+
code,
|
|
899
|
+
codeVerifier,
|
|
900
|
+
clientId,
|
|
901
|
+
redirectUri,
|
|
902
|
+
effectiveTenant,
|
|
903
|
+
nonce,
|
|
904
|
+
effectiveScopes,
|
|
905
|
+
);
|
|
906
|
+
},
|
|
907
|
+
)
|
|
908
|
+
.then(() => {
|
|
909
|
+
resolve();
|
|
910
|
+
})
|
|
911
|
+
.catch((error: unknown) => {
|
|
912
|
+
reject(error);
|
|
913
|
+
});
|
|
532
914
|
});
|
|
533
915
|
}
|
|
534
916
|
|
|
@@ -582,55 +964,48 @@ class AuthWeb implements Auth {
|
|
|
582
964
|
body: body.toString(),
|
|
583
965
|
});
|
|
584
966
|
|
|
585
|
-
const json = await
|
|
967
|
+
const json = await this.parseResponseObject(response);
|
|
586
968
|
|
|
587
969
|
if (!response.ok) {
|
|
588
970
|
throw new Error(
|
|
589
|
-
json
|
|
971
|
+
getOptionalString(json, "error_description") ??
|
|
972
|
+
getOptionalString(json, "error") ??
|
|
973
|
+
"Token exchange failed",
|
|
590
974
|
);
|
|
591
975
|
}
|
|
592
976
|
|
|
593
|
-
const idToken = json
|
|
977
|
+
const idToken = getOptionalString(json, "id_token");
|
|
594
978
|
if (!idToken) {
|
|
595
979
|
throw new Error("No id_token in token response");
|
|
596
980
|
}
|
|
597
981
|
|
|
598
982
|
const claims = this.decodeMicrosoftJwt(idToken);
|
|
599
|
-
const payload =
|
|
600
|
-
if (payload
|
|
983
|
+
const payload = this.parseJwtPayload(idToken);
|
|
984
|
+
if (getOptionalString(payload, "nonce") !== expectedNonce) {
|
|
601
985
|
throw new Error("Nonce mismatch - token may be replayed");
|
|
602
986
|
}
|
|
603
987
|
|
|
604
|
-
const accessToken = json
|
|
605
|
-
const refreshToken = json
|
|
606
|
-
const
|
|
988
|
+
const accessToken = getOptionalString(json, "access_token");
|
|
989
|
+
const refreshToken = getOptionalString(json, "refresh_token");
|
|
990
|
+
const expiresInSeconds = getOptionalNumber(json, "expires_in");
|
|
607
991
|
|
|
608
992
|
if (refreshToken) {
|
|
609
|
-
|
|
610
|
-
this._storageAdapter.save("microsoft_refresh_token", refreshToken);
|
|
611
|
-
} else {
|
|
612
|
-
localStorage.setItem(
|
|
613
|
-
"nitro_auth_microsoft_refresh_token",
|
|
614
|
-
refreshToken,
|
|
615
|
-
);
|
|
616
|
-
}
|
|
993
|
+
this.saveRefreshToken(refreshToken);
|
|
617
994
|
}
|
|
618
995
|
|
|
619
996
|
this._grantedScopes = scopes;
|
|
620
|
-
|
|
621
|
-
this._storageAdapter.save(SCOPES_KEY, JSON.stringify(scopes));
|
|
622
|
-
} else {
|
|
623
|
-
localStorage.setItem(SCOPES_KEY, JSON.stringify(scopes));
|
|
624
|
-
}
|
|
997
|
+
this.saveValue(SCOPES_KEY, JSON.stringify(scopes));
|
|
625
998
|
|
|
626
999
|
const user: AuthUser = {
|
|
627
1000
|
provider: "microsoft",
|
|
628
1001
|
idToken,
|
|
629
1002
|
accessToken: accessToken ?? undefined,
|
|
1003
|
+
refreshToken: refreshToken ?? undefined,
|
|
630
1004
|
scopes,
|
|
631
|
-
expirationTime:
|
|
632
|
-
|
|
633
|
-
|
|
1005
|
+
expirationTime:
|
|
1006
|
+
typeof expiresInSeconds === "number"
|
|
1007
|
+
? Date.now() + expiresInSeconds * 1000
|
|
1008
|
+
: undefined,
|
|
634
1009
|
...claims,
|
|
635
1010
|
};
|
|
636
1011
|
this.updateUser(user);
|
|
@@ -650,13 +1025,17 @@ class AuthWeb implements Auth {
|
|
|
650
1025
|
|
|
651
1026
|
private decodeMicrosoftJwt(token: string): Partial<AuthUser> {
|
|
652
1027
|
try {
|
|
653
|
-
const
|
|
654
|
-
const decoded = JSON.parse(atob(payload));
|
|
1028
|
+
const decoded = this.parseJwtPayload(token);
|
|
655
1029
|
return {
|
|
656
|
-
email:
|
|
657
|
-
|
|
1030
|
+
email:
|
|
1031
|
+
getOptionalString(decoded, "preferred_username") ??
|
|
1032
|
+
getOptionalString(decoded, "email"),
|
|
1033
|
+
name: getOptionalString(decoded, "name"),
|
|
658
1034
|
};
|
|
659
|
-
} catch {
|
|
1035
|
+
} catch (error) {
|
|
1036
|
+
logger.warn("Failed to decode Microsoft ID token", {
|
|
1037
|
+
error: String(error),
|
|
1038
|
+
});
|
|
660
1039
|
return {};
|
|
661
1040
|
}
|
|
662
1041
|
}
|
|
@@ -701,9 +1080,13 @@ class AuthWeb implements Auth {
|
|
|
701
1080
|
this.updateUser(user);
|
|
702
1081
|
resolve();
|
|
703
1082
|
})
|
|
704
|
-
.catch((err: unknown) =>
|
|
1083
|
+
.catch((err: unknown) => {
|
|
1084
|
+
reject(this.mapError(err));
|
|
1085
|
+
});
|
|
1086
|
+
};
|
|
1087
|
+
script.onerror = () => {
|
|
1088
|
+
reject(new Error("Failed to load Apple SDK"));
|
|
705
1089
|
};
|
|
706
|
-
script.onerror = () => reject(new Error("Failed to load Apple SDK"));
|
|
707
1090
|
document.head.appendChild(script);
|
|
708
1091
|
});
|
|
709
1092
|
}
|
|
@@ -727,17 +1110,14 @@ class AuthWeb implements Auth {
|
|
|
727
1110
|
this._grantedScopes = [];
|
|
728
1111
|
this.removeFromCache(CACHE_KEY);
|
|
729
1112
|
this.removeFromCache(SCOPES_KEY);
|
|
730
|
-
this.removeFromCache(
|
|
1113
|
+
this.removeFromCache(MS_REFRESH_TOKEN_KEY);
|
|
731
1114
|
this.notify();
|
|
732
1115
|
}
|
|
733
1116
|
|
|
734
1117
|
private updateUser(user: AuthUser) {
|
|
735
1118
|
this._currentUser = user;
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
} else {
|
|
739
|
-
localStorage.setItem(CACHE_KEY, JSON.stringify(user));
|
|
740
|
-
}
|
|
1119
|
+
const userToPersist = this.sanitizeUserForPersistence(user);
|
|
1120
|
+
this.saveValue(CACHE_KEY, JSON.stringify(userToPersist));
|
|
741
1121
|
this.notify();
|
|
742
1122
|
}
|
|
743
1123
|
|
|
@@ -745,12 +1125,12 @@ class AuthWeb implements Auth {
|
|
|
745
1125
|
logger.setEnabled(enabled);
|
|
746
1126
|
}
|
|
747
1127
|
|
|
748
|
-
|
|
749
|
-
this._storageAdapter = adapter
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
1128
|
+
setWebStorageAdapter(adapter: JSStorageAdapter | undefined): void {
|
|
1129
|
+
this._storageAdapter = adapter
|
|
1130
|
+
? this.createWebStorageDriver(adapter)
|
|
1131
|
+
: undefined;
|
|
1132
|
+
this.loadFromCache();
|
|
1133
|
+
this.notify();
|
|
754
1134
|
}
|
|
755
1135
|
|
|
756
1136
|
name = "Auth";
|