react-native-nitro-auth 0.5.8 → 0.5.10
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/CHANGELOG.md +48 -0
- package/README.md +69 -50
- package/cpp/HybridAuth.cpp +135 -50
- package/cpp/HybridAuth.hpp +1 -0
- package/ios/AuthAdapter.swift +28 -9
- package/lib/commonjs/Auth.web.js +141 -73
- package/lib/commonjs/Auth.web.js.map +1 -1
- package/lib/commonjs/create-auth-service.js +71 -0
- package/lib/commonjs/create-auth-service.js.map +1 -0
- package/lib/commonjs/service.js +2 -79
- package/lib/commonjs/service.js.map +1 -1
- package/lib/commonjs/service.web.js +2 -79
- package/lib/commonjs/service.web.js.map +1 -1
- package/lib/commonjs/use-auth.js +6 -3
- package/lib/commonjs/use-auth.js.map +1 -1
- package/lib/module/Auth.web.js +141 -73
- package/lib/module/Auth.web.js.map +1 -1
- package/lib/module/create-auth-service.js +67 -0
- package/lib/module/create-auth-service.js.map +1 -0
- package/lib/module/service.js +2 -79
- package/lib/module/service.js.map +1 -1
- package/lib/module/service.web.js +2 -79
- package/lib/module/service.web.js.map +1 -1
- package/lib/module/use-auth.js +6 -3
- package/lib/module/use-auth.js.map +1 -1
- package/lib/typescript/commonjs/Auth.web.d.ts +4 -2
- package/lib/typescript/commonjs/Auth.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/create-auth-service.d.ts +5 -0
- package/lib/typescript/commonjs/create-auth-service.d.ts.map +1 -0
- package/lib/typescript/commonjs/service.d.ts.map +1 -1
- package/lib/typescript/commonjs/service.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/use-auth.d.ts.map +1 -1
- package/lib/typescript/module/Auth.web.d.ts +4 -2
- package/lib/typescript/module/Auth.web.d.ts.map +1 -1
- package/lib/typescript/module/create-auth-service.d.ts +5 -0
- package/lib/typescript/module/create-auth-service.d.ts.map +1 -0
- package/lib/typescript/module/service.d.ts.map +1 -1
- package/lib/typescript/module/service.web.d.ts.map +1 -1
- package/lib/typescript/module/use-auth.d.ts.map +1 -1
- package/package.json +7 -5
- package/react-native-nitro-auth.podspec +1 -0
- package/src/Auth.web.ts +261 -102
- package/src/create-auth-service.ts +97 -0
- package/src/service.ts +3 -101
- package/src/service.web.ts +3 -101
- package/src/use-auth.ts +7 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Auth.web.d.ts","sourceRoot":"","sources":["../../../src/Auth.web.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,IAAI,EACJ,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,UAAU,
|
|
1
|
+
{"version":3,"file":"Auth.web.d.ts","sourceRoot":"","sources":["../../../src/Auth.web.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-auth-service.d.ts","sourceRoot":"","sources":["../../../src/create-auth-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,IAAI,EAKL,MAAM,cAAc,CAAC;AAGtB,KAAK,UAAU,GAAG,MAAM,IAAI,CAAC;AAiB7B,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI,CAsE3D"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../../src/service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../../src/service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAUzC,eAAO,MAAM,WAAW,EAAE,IAAsC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.web.d.ts","sourceRoot":"","sources":["../../../src/service.web.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"service.web.d.ts","sourceRoot":"","sources":["../../../src/service.web.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAIzC,eAAO,MAAM,WAAW,EAAE,IAA0C,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-auth.d.ts","sourceRoot":"","sources":["../../../src/use-auth.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,UAAU,EACX,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"use-auth.d.ts","sourceRoot":"","sources":["../../../src/use-auth.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,UAAU,EACX,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAQ/C,KAAK,SAAS,GAAG;IACf,IAAI,EAAE,QAAQ,GAAG,SAAS,CAAC;IAC3B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,SAAS,GAAG,SAAS,CAAC;CAC9B,CAAC;AAwBF,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG;IACtC,eAAe,EAAE,OAAO,CAAC;IACzB,KAAK,EAAE,CAAC,QAAQ,EAAE,YAAY,EAAE,OAAO,CAAC,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACzE,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,aAAa,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAClD,cAAc,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAClD,YAAY,EAAE,MAAM,OAAO,CAAC,UAAU,CAAC,CAAC;IACxC,aAAa,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC,CAAC;AAEF,wBAAgB,OAAO,IAAI,aAAa,CA+KvC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-nitro-auth",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.10",
|
|
4
4
|
"description": "High-performance authentication library for React Native with Google Sign-In, Apple Sign-In, and Microsoft Sign-In support, powered by Nitro Modules (JSI)",
|
|
5
5
|
"main": "lib/commonjs/index.js",
|
|
6
6
|
"module": "lib/module/index.js",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"ios",
|
|
18
18
|
"nitrogen",
|
|
19
19
|
"nitro.json",
|
|
20
|
+
"CHANGELOG.md",
|
|
20
21
|
"*.podspec",
|
|
21
22
|
"app.plugin.js",
|
|
22
23
|
"!**/__tests__",
|
|
@@ -39,6 +40,7 @@
|
|
|
39
40
|
"test": "jest",
|
|
40
41
|
"test:coverage": "jest --coverage",
|
|
41
42
|
"test:cpp": "node scripts/test-cpp.js",
|
|
43
|
+
"test:cpp:coverage": "node scripts/test-cpp.js --coverage",
|
|
42
44
|
"prepublishOnly": "bun run clean && bun run codegen && bun run build && bun run typecheck && bun run lint && bun run test && bun run test:cpp",
|
|
43
45
|
"prepack": "bun ../../scripts/sync-package-docs.ts",
|
|
44
46
|
"pack:dry-run": "bun pm pack --dry-run",
|
|
@@ -82,13 +84,13 @@
|
|
|
82
84
|
},
|
|
83
85
|
"devDependencies": {
|
|
84
86
|
"@expo/config-plugins": "^55.0.8",
|
|
85
|
-
"@react-native/babel-preset": "^0.83.
|
|
87
|
+
"@react-native/babel-preset": "^0.83.6",
|
|
86
88
|
"@testing-library/react": "^16.3.2",
|
|
87
|
-
"@types/node": "^22.19.
|
|
89
|
+
"@types/node": "^22.19.17",
|
|
88
90
|
"jest-environment-jsdom": "^29.7.0",
|
|
89
91
|
"react": "19.2.0",
|
|
90
|
-
"react-native": "0.83.
|
|
91
|
-
"react-native-nitro-modules": "^0.35.
|
|
92
|
+
"react-native": "0.83.6",
|
|
93
|
+
"react-native-nitro-modules": "^0.35.5",
|
|
92
94
|
"react-native-web": "^0.21.2",
|
|
93
95
|
"typescript": "^5.9.3"
|
|
94
96
|
},
|
package/src/Auth.web.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
AuthProvider,
|
|
5
5
|
LoginOptions,
|
|
6
6
|
AuthTokens,
|
|
7
|
+
AuthErrorCode,
|
|
7
8
|
} from "./Auth.nitro";
|
|
8
9
|
import type { JSStorageAdapter } from "./js-storage-adapter";
|
|
9
10
|
import { logger } from "./utils/logger";
|
|
@@ -23,6 +24,24 @@ const WEB_STORAGE_MODES = new Set([
|
|
|
23
24
|
STORAGE_MODE_LOCAL,
|
|
24
25
|
STORAGE_MODE_MEMORY,
|
|
25
26
|
] as const);
|
|
27
|
+
const WEB_AUTH_ERROR_CODES: ReadonlySet<string> = new Set<AuthErrorCode>([
|
|
28
|
+
"cancelled",
|
|
29
|
+
"timeout",
|
|
30
|
+
"popup_blocked",
|
|
31
|
+
"network_error",
|
|
32
|
+
"configuration_error",
|
|
33
|
+
"not_signed_in",
|
|
34
|
+
"operation_in_progress",
|
|
35
|
+
"unsupported_provider",
|
|
36
|
+
"invalid_state",
|
|
37
|
+
"invalid_nonce",
|
|
38
|
+
"token_error",
|
|
39
|
+
"no_id_token",
|
|
40
|
+
"parse_error",
|
|
41
|
+
"refresh_failed",
|
|
42
|
+
"unknown",
|
|
43
|
+
]);
|
|
44
|
+
const JWT_BASE64_URL_RE = /^[A-Za-z0-9_-]+$/;
|
|
26
45
|
const inMemoryWebStorage = new Map<string, string>();
|
|
27
46
|
let _appleSdkLoadPromise: Promise<void> | undefined;
|
|
28
47
|
|
|
@@ -60,7 +79,7 @@ type JsonObject = Record<string, unknown>;
|
|
|
60
79
|
class AuthWebError extends Error {
|
|
61
80
|
public readonly underlyingError?: string;
|
|
62
81
|
|
|
63
|
-
constructor(message:
|
|
82
|
+
constructor(message: AuthErrorCode, underlyingError?: string) {
|
|
64
83
|
super(message);
|
|
65
84
|
this.name = "AuthWebError";
|
|
66
85
|
this.underlyingError = underlyingError;
|
|
@@ -86,7 +105,28 @@ const getOptionalNumber = (
|
|
|
86
105
|
key: string,
|
|
87
106
|
): number | undefined => {
|
|
88
107
|
const candidate = source[key];
|
|
89
|
-
return typeof candidate === "number"
|
|
108
|
+
return typeof candidate === "number" && Number.isFinite(candidate)
|
|
109
|
+
? candidate
|
|
110
|
+
: undefined;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const parseExpiresInSeconds = (value: unknown): number | undefined => {
|
|
114
|
+
const candidate =
|
|
115
|
+
typeof value === "number"
|
|
116
|
+
? value
|
|
117
|
+
: typeof value === "string" && value.trim().length > 0
|
|
118
|
+
? Number(value)
|
|
119
|
+
: undefined;
|
|
120
|
+
|
|
121
|
+
if (
|
|
122
|
+
typeof candidate !== "number" ||
|
|
123
|
+
!Number.isFinite(candidate) ||
|
|
124
|
+
candidate < 0
|
|
125
|
+
) {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return candidate;
|
|
90
130
|
};
|
|
91
131
|
|
|
92
132
|
const parseAuthUser = (value: unknown): AuthUser | undefined => {
|
|
@@ -124,6 +164,9 @@ const parseScopes = (value: unknown): string[] | undefined => {
|
|
|
124
164
|
return value.filter((scope): scope is string => typeof scope === "string");
|
|
125
165
|
};
|
|
126
166
|
|
|
167
|
+
const isAuthErrorCode = (value: string): value is AuthErrorCode =>
|
|
168
|
+
WEB_AUTH_ERROR_CODES.has(value);
|
|
169
|
+
|
|
127
170
|
const parseAuthWebExtraConfig = (value: unknown): AuthWebExtraConfig => {
|
|
128
171
|
if (!isJsonObject(value)) {
|
|
129
172
|
return {};
|
|
@@ -438,29 +481,67 @@ class AuthWeb implements Auth {
|
|
|
438
481
|
}
|
|
439
482
|
|
|
440
483
|
private notify() {
|
|
441
|
-
this._listeners
|
|
442
|
-
|
|
443
|
-
}
|
|
484
|
+
for (const listener of [...this._listeners]) {
|
|
485
|
+
listener(this._currentUser);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private notifyTokenListeners(tokens: AuthTokens): void {
|
|
490
|
+
for (const listener of [...this._tokenListeners]) {
|
|
491
|
+
listener(tokens);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
private async runLoginOperation(
|
|
496
|
+
operation: () => Promise<void>,
|
|
497
|
+
): Promise<void> {
|
|
498
|
+
if (this._loginInFlight) {
|
|
499
|
+
throw new AuthWebError(
|
|
500
|
+
"operation_in_progress",
|
|
501
|
+
"Another login is already in progress",
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
this._loginInFlight = true;
|
|
506
|
+
try {
|
|
507
|
+
await operation();
|
|
508
|
+
} finally {
|
|
509
|
+
this._loginInFlight = false;
|
|
510
|
+
}
|
|
444
511
|
}
|
|
445
512
|
|
|
446
513
|
async login(provider: AuthProvider, options?: LoginOptions): Promise<void> {
|
|
447
514
|
const loginHint = options?.loginHint;
|
|
448
515
|
logger.log(`Starting login with ${provider}`, { scopes: options?.scopes });
|
|
449
516
|
try {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
options?.
|
|
459
|
-
|
|
517
|
+
await this.runLoginOperation(async () => {
|
|
518
|
+
if (provider === "google") {
|
|
519
|
+
const scopes = options?.scopes ?? DEFAULT_SCOPES;
|
|
520
|
+
await this.loginGoogle(scopes, loginHint);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (provider === "microsoft") {
|
|
525
|
+
const scopes = options?.scopes ?? MS_DEFAULT_SCOPES;
|
|
526
|
+
await this.loginMicrosoft(
|
|
527
|
+
scopes,
|
|
528
|
+
loginHint,
|
|
529
|
+
options?.tenant,
|
|
530
|
+
options?.prompt,
|
|
531
|
+
);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (provider === "apple") {
|
|
536
|
+
await this.loginApple();
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
throw new AuthWebError(
|
|
541
|
+
"unsupported_provider",
|
|
542
|
+
`Unsupported auth provider: ${provider}`,
|
|
460
543
|
);
|
|
461
|
-
}
|
|
462
|
-
await this.loginApple();
|
|
463
|
-
}
|
|
544
|
+
});
|
|
464
545
|
logger.log(`Login successful with ${provider}`);
|
|
465
546
|
} catch (e: unknown) {
|
|
466
547
|
const error = this.mapError(e);
|
|
@@ -482,11 +563,14 @@ class AuthWeb implements Auth {
|
|
|
482
563
|
logger.log("Requesting additional scopes:", scopes);
|
|
483
564
|
const newScopes = [...new Set([...this._grantedScopes, ...scopes])];
|
|
484
565
|
try {
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
566
|
+
await this.runLoginOperation(async () => {
|
|
567
|
+
if (provider === "google") {
|
|
568
|
+
await this.loginGoogle(newScopes);
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
|
|
488
572
|
await this.loginMicrosoft(newScopes);
|
|
489
|
-
}
|
|
573
|
+
});
|
|
490
574
|
} catch (e) {
|
|
491
575
|
const error = this.mapError(e);
|
|
492
576
|
logger.error("Requesting scopes failed:", error.message);
|
|
@@ -573,7 +657,8 @@ class AuthWeb implements Auth {
|
|
|
573
657
|
|
|
574
658
|
const json = await this.parseResponseObject(response);
|
|
575
659
|
if (!response.ok) {
|
|
576
|
-
throw new
|
|
660
|
+
throw new AuthWebError(
|
|
661
|
+
"refresh_failed",
|
|
577
662
|
getOptionalString(json, "error_description") ??
|
|
578
663
|
getOptionalString(json, "error") ??
|
|
579
664
|
"Token refresh failed",
|
|
@@ -583,16 +668,12 @@ class AuthWeb implements Auth {
|
|
|
583
668
|
const idToken = getOptionalString(json, "id_token");
|
|
584
669
|
const accessToken = getOptionalString(json, "access_token");
|
|
585
670
|
const newRefreshToken = getOptionalString(json, "refresh_token");
|
|
586
|
-
const expiresInSeconds = getOptionalNumber(json, "expires_in");
|
|
587
671
|
|
|
588
672
|
if (newRefreshToken) {
|
|
589
673
|
this.saveRefreshToken(newRefreshToken);
|
|
590
674
|
}
|
|
591
675
|
|
|
592
|
-
const expirationTime =
|
|
593
|
-
typeof expiresInSeconds === "number"
|
|
594
|
-
? Date.now() + expiresInSeconds * 1000
|
|
595
|
-
: undefined;
|
|
676
|
+
const expirationTime = this.getExpirationTime(json["expires_in"]);
|
|
596
677
|
|
|
597
678
|
const effectiveIdToken = idToken ?? this._currentUser.idToken;
|
|
598
679
|
const claims = effectiveIdToken
|
|
@@ -614,9 +695,7 @@ class AuthWeb implements Auth {
|
|
|
614
695
|
refreshToken: newRefreshToken ?? undefined,
|
|
615
696
|
expirationTime,
|
|
616
697
|
};
|
|
617
|
-
this.
|
|
618
|
-
l(tokens);
|
|
619
|
-
});
|
|
698
|
+
this.notifyTokenListeners(tokens);
|
|
620
699
|
return tokens;
|
|
621
700
|
}
|
|
622
701
|
|
|
@@ -636,18 +715,22 @@ class AuthWeb implements Auth {
|
|
|
636
715
|
refreshToken: this._currentUser.refreshToken,
|
|
637
716
|
expirationTime: this._currentUser.expirationTime,
|
|
638
717
|
};
|
|
639
|
-
this.
|
|
640
|
-
l(tokens);
|
|
641
|
-
});
|
|
718
|
+
this.notifyTokenListeners(tokens);
|
|
642
719
|
return tokens;
|
|
643
720
|
}
|
|
644
721
|
|
|
645
722
|
private mapError(error: unknown): Error {
|
|
723
|
+
if (error instanceof AuthWebError) {
|
|
724
|
+
return error;
|
|
725
|
+
}
|
|
726
|
+
|
|
646
727
|
const rawMessage = error instanceof Error ? error.message : String(error);
|
|
647
728
|
const msg = rawMessage.toLowerCase();
|
|
648
|
-
let mappedMsg =
|
|
729
|
+
let mappedMsg: AuthErrorCode = "unknown";
|
|
649
730
|
|
|
650
|
-
if (
|
|
731
|
+
if (isAuthErrorCode(rawMessage)) {
|
|
732
|
+
mappedMsg = rawMessage;
|
|
733
|
+
} else if (msg.includes("cancel") || msg.includes("popup_closed")) {
|
|
651
734
|
mappedMsg = "cancelled";
|
|
652
735
|
} else if (msg.includes("access_denied")) {
|
|
653
736
|
mappedMsg = "cancelled";
|
|
@@ -655,6 +738,21 @@ class AuthWeb implements Auth {
|
|
|
655
738
|
mappedMsg = "timeout";
|
|
656
739
|
} else if (msg.includes("popup blocked")) {
|
|
657
740
|
mappedMsg = "popup_blocked";
|
|
741
|
+
} else if (msg.includes("login is already in progress")) {
|
|
742
|
+
mappedMsg = "operation_in_progress";
|
|
743
|
+
} else if (msg.includes("state mismatch")) {
|
|
744
|
+
mappedMsg = "invalid_state";
|
|
745
|
+
} else if (msg.includes("nonce mismatch")) {
|
|
746
|
+
mappedMsg = "invalid_nonce";
|
|
747
|
+
} else if (msg.includes("no id_token") || msg.includes("no_id_token")) {
|
|
748
|
+
mappedMsg = "no_id_token";
|
|
749
|
+
} else if (msg.includes("invalid jwt") || msg.includes("json")) {
|
|
750
|
+
mappedMsg = "parse_error";
|
|
751
|
+
} else if (
|
|
752
|
+
msg.includes("no user logged in") ||
|
|
753
|
+
msg.includes("not signed in")
|
|
754
|
+
) {
|
|
755
|
+
mappedMsg = "not_signed_in";
|
|
658
756
|
} else if (
|
|
659
757
|
msg.includes("network") ||
|
|
660
758
|
msg.includes("server_error") ||
|
|
@@ -677,26 +775,105 @@ class AuthWeb implements Auth {
|
|
|
677
775
|
}
|
|
678
776
|
|
|
679
777
|
private async parseResponseObject(response: Response): Promise<JsonObject> {
|
|
680
|
-
|
|
778
|
+
let parsed: unknown;
|
|
779
|
+
try {
|
|
780
|
+
parsed = await response.json();
|
|
781
|
+
} catch (error) {
|
|
782
|
+
throw new AuthWebError("parse_error", String(error));
|
|
783
|
+
}
|
|
784
|
+
|
|
681
785
|
if (!isJsonObject(parsed)) {
|
|
682
|
-
throw new
|
|
786
|
+
throw new AuthWebError(
|
|
787
|
+
"parse_error",
|
|
788
|
+
"Expected JSON object response from auth provider",
|
|
789
|
+
);
|
|
683
790
|
}
|
|
684
791
|
return parsed;
|
|
685
792
|
}
|
|
686
793
|
|
|
687
794
|
private parseJwtPayload(token: string): JsonObject {
|
|
688
|
-
const
|
|
689
|
-
|
|
690
|
-
|
|
795
|
+
const parts = token.split(".");
|
|
796
|
+
const payload = parts[1];
|
|
797
|
+
if (
|
|
798
|
+
parts.length < 2 ||
|
|
799
|
+
!payload ||
|
|
800
|
+
payload.length % 4 === 1 ||
|
|
801
|
+
!JWT_BASE64_URL_RE.test(payload)
|
|
802
|
+
) {
|
|
803
|
+
throw new AuthWebError("parse_error", "Invalid JWT payload");
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
try {
|
|
807
|
+
const normalizedPayload = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
808
|
+
const padding = "=".repeat((4 - (normalizedPayload.length % 4)) % 4);
|
|
809
|
+
const binary = atob(`${normalizedPayload}${padding}`);
|
|
810
|
+
const decodedText =
|
|
811
|
+
typeof TextDecoder === "function"
|
|
812
|
+
? new TextDecoder().decode(
|
|
813
|
+
Uint8Array.from(binary, (char) => char.charCodeAt(0)),
|
|
814
|
+
)
|
|
815
|
+
: decodeURIComponent(
|
|
816
|
+
Array.from(
|
|
817
|
+
binary,
|
|
818
|
+
(char) =>
|
|
819
|
+
`%${char.charCodeAt(0).toString(16).padStart(2, "0")}`,
|
|
820
|
+
).join(""),
|
|
821
|
+
);
|
|
822
|
+
const decoded: unknown = JSON.parse(decodedText);
|
|
823
|
+
if (!isJsonObject(decoded)) {
|
|
824
|
+
throw new Error("Expected JWT payload to be an object");
|
|
825
|
+
}
|
|
826
|
+
return decoded;
|
|
827
|
+
} catch (error) {
|
|
828
|
+
if (error instanceof AuthWebError) {
|
|
829
|
+
throw error;
|
|
830
|
+
}
|
|
831
|
+
throw new AuthWebError("parse_error", String(error));
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
private mapOAuthErrorCode(error: string): AuthErrorCode {
|
|
836
|
+
const normalizedError = error.trim().toLowerCase();
|
|
837
|
+
if (
|
|
838
|
+
normalizedError === "access_denied" ||
|
|
839
|
+
normalizedError === "popup_closed_by_user" ||
|
|
840
|
+
normalizedError === "user_cancelled"
|
|
841
|
+
) {
|
|
842
|
+
return "cancelled";
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
if (
|
|
846
|
+
normalizedError === "server_error" ||
|
|
847
|
+
normalizedError === "temporarily_unavailable"
|
|
848
|
+
) {
|
|
849
|
+
return "network_error";
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
if (
|
|
853
|
+
normalizedError === "invalid_client" ||
|
|
854
|
+
normalizedError === "invalid_scope" ||
|
|
855
|
+
normalizedError === "unauthorized_client"
|
|
856
|
+
) {
|
|
857
|
+
return "configuration_error";
|
|
691
858
|
}
|
|
692
859
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
860
|
+
if (
|
|
861
|
+
normalizedError === "invalid_grant" ||
|
|
862
|
+
normalizedError === "invalid_token"
|
|
863
|
+
) {
|
|
864
|
+
return "token_error";
|
|
698
865
|
}
|
|
699
|
-
|
|
866
|
+
|
|
867
|
+
return "unknown";
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
private getExpirationTime(expiresIn: unknown): number | undefined {
|
|
871
|
+
const expiresInSeconds = parseExpiresInSeconds(expiresIn);
|
|
872
|
+
if (expiresInSeconds === undefined) {
|
|
873
|
+
return undefined;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
return Date.now() + expiresInSeconds * 1000;
|
|
700
877
|
}
|
|
701
878
|
|
|
702
879
|
private waitForPopupRedirect(
|
|
@@ -750,7 +927,8 @@ class AuthWeb implements Auth {
|
|
|
750
927
|
}
|
|
751
928
|
|
|
752
929
|
cleanup(intervalId, timeoutId, true);
|
|
753
|
-
void Promise.resolve(
|
|
930
|
+
void Promise.resolve()
|
|
931
|
+
.then(() => onRedirect(url))
|
|
754
932
|
.then(() => {
|
|
755
933
|
resolve();
|
|
756
934
|
})
|
|
@@ -764,24 +942,6 @@ class AuthWeb implements Auth {
|
|
|
764
942
|
private async loginGoogle(
|
|
765
943
|
scopes: string[],
|
|
766
944
|
loginHint?: string,
|
|
767
|
-
): Promise<void> {
|
|
768
|
-
if (this._loginInFlight) {
|
|
769
|
-
throw new AuthWebError(
|
|
770
|
-
"cancelled",
|
|
771
|
-
"Another login is already in progress",
|
|
772
|
-
);
|
|
773
|
-
}
|
|
774
|
-
this._loginInFlight = true;
|
|
775
|
-
try {
|
|
776
|
-
await this._loginGoogleInner(scopes, loginHint);
|
|
777
|
-
} finally {
|
|
778
|
-
this._loginInFlight = false;
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
private async _loginGoogleInner(
|
|
783
|
-
scopes: string[],
|
|
784
|
-
loginHint?: string,
|
|
785
945
|
): Promise<void> {
|
|
786
946
|
const clientId = this._config.googleWebClientId;
|
|
787
947
|
|
|
@@ -831,15 +991,27 @@ class AuthWeb implements Auth {
|
|
|
831
991
|
const accessToken = params.get("access_token");
|
|
832
992
|
const expiresIn = params.get("expires_in");
|
|
833
993
|
const code = params.get("code");
|
|
994
|
+
const error = params.get("error");
|
|
995
|
+
const errorDescription = params.get("error_description");
|
|
996
|
+
|
|
997
|
+
if (error) {
|
|
998
|
+
throw new AuthWebError(
|
|
999
|
+
this.mapOAuthErrorCode(error),
|
|
1000
|
+
errorDescription ?? error,
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
834
1003
|
|
|
835
1004
|
if (!idToken) {
|
|
836
|
-
throw new
|
|
1005
|
+
throw new AuthWebError("no_id_token", "No id_token in response");
|
|
837
1006
|
}
|
|
838
1007
|
|
|
839
1008
|
const decoded = this.parseJwtPayload(idToken);
|
|
840
1009
|
if (decoded["nonce"] !== this._pendingGoogleNonce) {
|
|
841
1010
|
this._pendingGoogleNonce = undefined;
|
|
842
|
-
throw new
|
|
1011
|
+
throw new AuthWebError(
|
|
1012
|
+
"invalid_nonce",
|
|
1013
|
+
"Nonce mismatch - possible replay attack",
|
|
1014
|
+
);
|
|
843
1015
|
}
|
|
844
1016
|
this._pendingGoogleNonce = undefined;
|
|
845
1017
|
|
|
@@ -852,9 +1024,7 @@ class AuthWeb implements Auth {
|
|
|
852
1024
|
accessToken: accessToken ?? undefined,
|
|
853
1025
|
serverAuthCode: code ?? undefined,
|
|
854
1026
|
scopes,
|
|
855
|
-
expirationTime: expiresIn
|
|
856
|
-
? Date.now() + parseInt(expiresIn, 10) * 1000
|
|
857
|
-
: undefined,
|
|
1027
|
+
expirationTime: this.getExpirationTime(expiresIn),
|
|
858
1028
|
...this.decodeGoogleJwt(idToken),
|
|
859
1029
|
};
|
|
860
1030
|
this.updateUser(user);
|
|
@@ -887,26 +1057,6 @@ class AuthWeb implements Auth {
|
|
|
887
1057
|
loginHint?: string,
|
|
888
1058
|
tenant?: string,
|
|
889
1059
|
prompt?: string,
|
|
890
|
-
): Promise<void> {
|
|
891
|
-
if (this._loginInFlight) {
|
|
892
|
-
throw new AuthWebError(
|
|
893
|
-
"cancelled",
|
|
894
|
-
"Another login is already in progress",
|
|
895
|
-
);
|
|
896
|
-
}
|
|
897
|
-
this._loginInFlight = true;
|
|
898
|
-
try {
|
|
899
|
-
await this._loginMicrosoftInner(scopes, loginHint, tenant, prompt);
|
|
900
|
-
} finally {
|
|
901
|
-
this._loginInFlight = false;
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
private async _loginMicrosoftInner(
|
|
906
|
-
scopes: string[],
|
|
907
|
-
loginHint?: string,
|
|
908
|
-
tenant?: string,
|
|
909
|
-
prompt?: string,
|
|
910
1060
|
): Promise<void> {
|
|
911
1061
|
const clientId = this._config.microsoftClientId;
|
|
912
1062
|
|
|
@@ -978,15 +1128,24 @@ class AuthWeb implements Auth {
|
|
|
978
1128
|
const errorDescription = urlObj.searchParams.get("error_description");
|
|
979
1129
|
|
|
980
1130
|
if (error) {
|
|
981
|
-
throw new
|
|
1131
|
+
throw new AuthWebError(
|
|
1132
|
+
this.mapOAuthErrorCode(error),
|
|
1133
|
+
errorDescription ?? error,
|
|
1134
|
+
);
|
|
982
1135
|
}
|
|
983
1136
|
|
|
984
1137
|
if (returnedState !== state) {
|
|
985
|
-
throw new
|
|
1138
|
+
throw new AuthWebError(
|
|
1139
|
+
"invalid_state",
|
|
1140
|
+
"State mismatch - possible CSRF attack",
|
|
1141
|
+
);
|
|
986
1142
|
}
|
|
987
1143
|
|
|
988
1144
|
if (!code) {
|
|
989
|
-
throw new
|
|
1145
|
+
throw new AuthWebError(
|
|
1146
|
+
"token_error",
|
|
1147
|
+
"No authorization code in response",
|
|
1148
|
+
);
|
|
990
1149
|
}
|
|
991
1150
|
|
|
992
1151
|
await this.exchangeMicrosoftCodeForTokens(
|
|
@@ -1061,7 +1220,8 @@ class AuthWeb implements Auth {
|
|
|
1061
1220
|
const json = await this.parseResponseObject(response);
|
|
1062
1221
|
|
|
1063
1222
|
if (!response.ok) {
|
|
1064
|
-
throw new
|
|
1223
|
+
throw new AuthWebError(
|
|
1224
|
+
"token_error",
|
|
1065
1225
|
getOptionalString(json, "error_description") ??
|
|
1066
1226
|
getOptionalString(json, "error") ??
|
|
1067
1227
|
"Token exchange failed",
|
|
@@ -1070,18 +1230,20 @@ class AuthWeb implements Auth {
|
|
|
1070
1230
|
|
|
1071
1231
|
const idToken = getOptionalString(json, "id_token");
|
|
1072
1232
|
if (!idToken) {
|
|
1073
|
-
throw new
|
|
1233
|
+
throw new AuthWebError("no_id_token", "No id_token in token response");
|
|
1074
1234
|
}
|
|
1075
1235
|
|
|
1076
1236
|
const claims = this.decodeMicrosoftJwt(idToken);
|
|
1077
1237
|
const payload = this.parseJwtPayload(idToken);
|
|
1078
1238
|
if (getOptionalString(payload, "nonce") !== expectedNonce) {
|
|
1079
|
-
throw new
|
|
1239
|
+
throw new AuthWebError(
|
|
1240
|
+
"invalid_nonce",
|
|
1241
|
+
"Nonce mismatch - token may be replayed",
|
|
1242
|
+
);
|
|
1080
1243
|
}
|
|
1081
1244
|
|
|
1082
1245
|
const accessToken = getOptionalString(json, "access_token");
|
|
1083
1246
|
const refreshToken = getOptionalString(json, "refresh_token");
|
|
1084
|
-
const expiresInSeconds = getOptionalNumber(json, "expires_in");
|
|
1085
1247
|
|
|
1086
1248
|
if (refreshToken) {
|
|
1087
1249
|
this.saveRefreshToken(refreshToken);
|
|
@@ -1096,10 +1258,7 @@ class AuthWeb implements Auth {
|
|
|
1096
1258
|
accessToken: accessToken ?? undefined,
|
|
1097
1259
|
refreshToken: refreshToken ?? undefined,
|
|
1098
1260
|
scopes,
|
|
1099
|
-
expirationTime:
|
|
1100
|
-
typeof expiresInSeconds === "number"
|
|
1101
|
-
? Date.now() + expiresInSeconds * 1000
|
|
1102
|
-
: undefined,
|
|
1261
|
+
expirationTime: this.getExpirationTime(json["expires_in"]),
|
|
1103
1262
|
...claims,
|
|
1104
1263
|
};
|
|
1105
1264
|
this.updateUser(user);
|