@chipmobilesdk/rn-auth 0.2.1 → 0.3.1
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 +126 -71
- package/README.md +143 -130
- package/package.json +51 -54
- package/skills/sdk-rn-auth/SKILL.md +1 -2
- package/src/access/accessTier.ts +88 -88
- package/src/config/types.ts +154 -143
- package/src/config/validateConfig.ts +114 -108
- package/src/core/authMachine.ts +364 -249
- package/src/core/profile.ts +57 -60
- package/src/core/session.ts +105 -96
- package/src/diagnostics/errors.ts +87 -85
- package/src/diagnostics/messageKeys.ts +59 -61
- package/src/index.ts +71 -82
- package/src/oidc/authClient.ts +195 -224
- package/src/oidc/classifyError.ts +76 -0
- package/src/oidc/socialRouting.ts +66 -73
- package/src/react/AuthProvider.tsx +41 -47
- package/src/react/hooks.ts +86 -90
- package/src/tokens/accessToken.ts +159 -105
- package/src/tokens/secureStore.ts +51 -52
package/src/index.ts
CHANGED
|
@@ -1,82 +1,71 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @chipmobilesdk/rn-auth — reusable Microsoft Entra External ID authentication
|
|
3
|
-
* for ChipMobileSdk-family React Native apps (public OIDC/PKCE client, no
|
|
4
|
-
* backend). This is the single public entrypoint (LIB-003); modules under
|
|
5
|
-
* `core/`, `tokens/`, `oidc/`, `access/`, and `diagnostics/` are private.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { createAuthMachine } from './core/authMachine';
|
|
9
|
-
import type { Auth, AuthConfig, CreateAuthOptions } from './config/types';
|
|
10
|
-
|
|
11
|
-
/** Create the auth controller. Throws AuthConfigError on invalid config (FR-005). */
|
|
12
|
-
export function createAuth(config: AuthConfig, options?: CreateAuthOptions): Auth {
|
|
13
|
-
return createAuthMachine(config, options);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// Public types
|
|
17
|
-
export type {
|
|
18
|
-
Auth,
|
|
19
|
-
AuthConfig,
|
|
20
|
-
CreateAuthOptions,
|
|
21
|
-
AuthState,
|
|
22
|
-
AuthStatus,
|
|
23
|
-
RestoreStatus,
|
|
24
|
-
Profile,
|
|
25
|
-
ProviderIdentity,
|
|
26
|
-
ProviderConfig,
|
|
27
|
-
ProviderId,
|
|
28
|
-
DevicePlatform,
|
|
29
|
-
ResourceConfig,
|
|
30
|
-
ClaimMatcher,
|
|
31
|
-
AccessTierMapping,
|
|
32
|
-
AccessTier,
|
|
33
|
-
AccessTokenResult,
|
|
34
|
-
AccessTokenStatus,
|
|
35
|
-
PremiumGateResult,
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
export {
|
|
41
|
-
export
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
export {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
useProfile,
|
|
73
|
-
useAccessTier,
|
|
74
|
-
useAccessToken,
|
|
75
|
-
useSignInProvider,
|
|
76
|
-
useSignInProviders,
|
|
77
|
-
} from './react/hooks';
|
|
78
|
-
export type {
|
|
79
|
-
UseAuthResult,
|
|
80
|
-
UseAccessTokenResult,
|
|
81
|
-
UseAccessTokenStatus,
|
|
82
|
-
} from './react/hooks';
|
|
1
|
+
/**
|
|
2
|
+
* @chipmobilesdk/rn-auth — reusable Microsoft Entra External ID authentication
|
|
3
|
+
* for ChipMobileSdk-family React Native apps (public OIDC/PKCE client, no
|
|
4
|
+
* backend). This is the single public entrypoint (LIB-003); modules under
|
|
5
|
+
* `core/`, `tokens/`, `oidc/`, `access/`, and `diagnostics/` are private.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createAuthMachine } from './core/authMachine';
|
|
9
|
+
import type { Auth, AuthConfig, CreateAuthOptions } from './config/types';
|
|
10
|
+
|
|
11
|
+
/** Create the auth controller. Throws AuthConfigError on invalid config (FR-005). */
|
|
12
|
+
export function createAuth(config: AuthConfig, options?: CreateAuthOptions): Auth {
|
|
13
|
+
return createAuthMachine(config, options);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Public types
|
|
17
|
+
export type {
|
|
18
|
+
Auth,
|
|
19
|
+
AuthConfig,
|
|
20
|
+
CreateAuthOptions,
|
|
21
|
+
AuthState,
|
|
22
|
+
AuthStatus,
|
|
23
|
+
RestoreStatus,
|
|
24
|
+
Profile,
|
|
25
|
+
ProviderIdentity,
|
|
26
|
+
ProviderConfig,
|
|
27
|
+
ProviderId,
|
|
28
|
+
DevicePlatform,
|
|
29
|
+
ResourceConfig,
|
|
30
|
+
ClaimMatcher,
|
|
31
|
+
AccessTierMapping,
|
|
32
|
+
AccessTier,
|
|
33
|
+
AccessTokenResult,
|
|
34
|
+
AccessTokenStatus,
|
|
35
|
+
PremiumGateResult,
|
|
36
|
+
GlobalSignOutResult,
|
|
37
|
+
} from './config/types';
|
|
38
|
+
|
|
39
|
+
// Ports + default adapters
|
|
40
|
+
export type { SecureTokenStore } from './tokens/secureStore';
|
|
41
|
+
export { createKeychainSecureStore, AUTH_STORAGE_KEY } from './tokens/secureStore';
|
|
42
|
+
export type {
|
|
43
|
+
OidcAuthClient,
|
|
44
|
+
OidcTokens,
|
|
45
|
+
AuthorizeParams,
|
|
46
|
+
RefreshParams,
|
|
47
|
+
EndSessionParams,
|
|
48
|
+
} from './oidc/authClient';
|
|
49
|
+
export { createAppAuthClient } from './oidc/authClient';
|
|
50
|
+
|
|
51
|
+
// Access-tier evaluation (also exported for app-side reuse/testing)
|
|
52
|
+
export { evaluateAccessTier, failClosedTier } from './access/accessTier';
|
|
53
|
+
|
|
54
|
+
// Diagnostics
|
|
55
|
+
export { AuthError, AuthConfigError, isAuthError, redact } from './diagnostics/errors';
|
|
56
|
+
export type { AuthErrorCode, AuthErrorAudience } from './diagnostics/errors';
|
|
57
|
+
export { DEFAULT_MESSAGES, resolveMessages } from './diagnostics/messageKeys';
|
|
58
|
+
export type { AuthMessageMap, AuthMessageKey } from './diagnostics/messageKeys';
|
|
59
|
+
|
|
60
|
+
// React bindings
|
|
61
|
+
export { AuthProvider } from './react/AuthProvider';
|
|
62
|
+
export type { AuthProviderProps } from './react/AuthProvider';
|
|
63
|
+
export {
|
|
64
|
+
useAuth,
|
|
65
|
+
useProfile,
|
|
66
|
+
useAccessTier,
|
|
67
|
+
useAccessToken,
|
|
68
|
+
useSignInProvider,
|
|
69
|
+
useSignInProviders,
|
|
70
|
+
} from './react/hooks';
|
|
71
|
+
export type { UseAuthResult, UseAccessTokenResult, UseAccessTokenStatus } from './react/hooks';
|
package/src/oidc/authClient.ts
CHANGED
|
@@ -1,224 +1,195 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OidcAuthClient port + default react-native-app-auth adapter (R1).
|
|
3
|
-
*
|
|
4
|
-
* The port abstracts the Authorization Code + PKCE public-client flow so the
|
|
5
|
-
* auth engine is testable without native code. The default adapter lazily
|
|
6
|
-
* requires react-native-app-auth and is validated on device.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
export interface OidcTokens {
|
|
12
|
-
/** Resource/audience access token (present for authorize and resource refresh). */
|
|
13
|
-
accessToken?: string;
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
let
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
throw classifyAppAuthError(error);
|
|
197
|
-
}
|
|
198
|
-
},
|
|
199
|
-
async refresh(params) {
|
|
200
|
-
try {
|
|
201
|
-
const result = await AppAuth.refresh(
|
|
202
|
-
baseConfig(params.authority, params.clientId, params.redirectUrl, params.scopes),
|
|
203
|
-
{ refreshToken: params.refreshToken },
|
|
204
|
-
);
|
|
205
|
-
return toTokens(result);
|
|
206
|
-
} catch (error) {
|
|
207
|
-
throw classifyAppAuthError(error);
|
|
208
|
-
}
|
|
209
|
-
},
|
|
210
|
-
async endSession(params) {
|
|
211
|
-
try {
|
|
212
|
-
await AppAuth.logout(
|
|
213
|
-
baseConfig(params.authority, params.clientId, params.redirectUrl, []),
|
|
214
|
-
{
|
|
215
|
-
idToken: params.idToken ?? '',
|
|
216
|
-
postLogoutRedirectUrl: params.redirectUrl,
|
|
217
|
-
},
|
|
218
|
-
);
|
|
219
|
-
} catch (error) {
|
|
220
|
-
throw classifyAppAuthError(error);
|
|
221
|
-
}
|
|
222
|
-
},
|
|
223
|
-
};
|
|
224
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* OidcAuthClient port + default react-native-app-auth adapter (R1).
|
|
3
|
+
*
|
|
4
|
+
* The port abstracts the Authorization Code + PKCE public-client flow so the
|
|
5
|
+
* auth engine is testable without native code. The default adapter lazily
|
|
6
|
+
* requires react-native-app-auth and is validated on device.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { classifyAppAuthError } from './classifyError';
|
|
10
|
+
|
|
11
|
+
export interface OidcTokens {
|
|
12
|
+
/** Resource/audience access token (present for authorize and resource refresh). */
|
|
13
|
+
accessToken?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Refresh token used for silent acquisition and restore. Absent when the
|
|
16
|
+
* provider issued none — a refresh that does not rotate, or an authorize the
|
|
17
|
+
* tenant answered without `offline_access`.
|
|
18
|
+
*/
|
|
19
|
+
refreshToken?: string;
|
|
20
|
+
/** Raw id token (JWT). Absent from a refresh requested without `openid`. */
|
|
21
|
+
idToken?: string;
|
|
22
|
+
/** Decoded id-token (or access-token) claims used for profile + access tier. */
|
|
23
|
+
claims: Record<string, unknown>;
|
|
24
|
+
/** Access-token expiry in epoch milliseconds, when available. */
|
|
25
|
+
accessTokenExpiresAt?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface AuthorizeParams {
|
|
29
|
+
authority: string;
|
|
30
|
+
clientId: string;
|
|
31
|
+
redirectUrl: string;
|
|
32
|
+
scopes: string[];
|
|
33
|
+
/** Additional OIDC parameters, e.g. `domain_hint` for social routing. */
|
|
34
|
+
additionalParameters?: Record<string, string>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface RefreshParams {
|
|
38
|
+
authority: string;
|
|
39
|
+
clientId: string;
|
|
40
|
+
redirectUrl: string;
|
|
41
|
+
refreshToken: string;
|
|
42
|
+
scopes: string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface EndSessionParams {
|
|
46
|
+
authority: string;
|
|
47
|
+
clientId: string;
|
|
48
|
+
redirectUrl: string;
|
|
49
|
+
idToken?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface OidcAuthClient {
|
|
53
|
+
authorize(params: AuthorizeParams): Promise<OidcTokens>;
|
|
54
|
+
refresh(params: RefreshParams): Promise<OidcTokens>;
|
|
55
|
+
endSession(params: EndSessionParams): Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Decode the payload segment of a JWT without any dependency (device-safe). */
|
|
59
|
+
export function decodeJwtClaims(jwt: string | null | undefined): Record<string, unknown> {
|
|
60
|
+
if (typeof jwt !== 'string') {
|
|
61
|
+
return {};
|
|
62
|
+
}
|
|
63
|
+
const parts = jwt.split('.');
|
|
64
|
+
if (parts.length < 2) {
|
|
65
|
+
return {};
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const json = base64UrlDecode(parts[1]);
|
|
69
|
+
return JSON.parse(json) as Record<string, unknown>;
|
|
70
|
+
} catch {
|
|
71
|
+
return {};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const BASE64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
76
|
+
|
|
77
|
+
function base64UrlDecode(input: string): string {
|
|
78
|
+
let base64 = input.replace(/-/g, '+').replace(/_/g, '/');
|
|
79
|
+
while (base64.length % 4 !== 0) {
|
|
80
|
+
base64 += '=';
|
|
81
|
+
}
|
|
82
|
+
let bytes = '';
|
|
83
|
+
let buffer = 0;
|
|
84
|
+
let bits = 0;
|
|
85
|
+
for (const char of base64) {
|
|
86
|
+
if (char === '=') {
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
const index = BASE64_CHARS.indexOf(char);
|
|
90
|
+
if (index === -1) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
// Standard base64 bit assembly; bitwise ops are intentional here.
|
|
94
|
+
// eslint-disable-next-line no-bitwise
|
|
95
|
+
buffer = (buffer << 6) | index;
|
|
96
|
+
bits += 6;
|
|
97
|
+
if (bits >= 8) {
|
|
98
|
+
bits -= 8;
|
|
99
|
+
// eslint-disable-next-line no-bitwise
|
|
100
|
+
bytes += String.fromCharCode((buffer >> bits) & 0xff);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Interpret the byte string (each char is a 0–255 byte) as UTF-8 without
|
|
104
|
+
// relying on the deprecated global `escape`.
|
|
105
|
+
try {
|
|
106
|
+
const percentEncoded = Array.from(bytes)
|
|
107
|
+
.map(char => '%' + char.charCodeAt(0).toString(16).padStart(2, '0'))
|
|
108
|
+
.join('');
|
|
109
|
+
return decodeURIComponent(percentEncoded);
|
|
110
|
+
} catch {
|
|
111
|
+
return bytes;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface AppAuthClientOptions {
|
|
116
|
+
/** When true, the OIDC discovery document is fetched from `${authority}`. */
|
|
117
|
+
useDiscovery?: boolean;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Default OidcAuthClient adapter wrapping react-native-app-auth (PKCE public
|
|
122
|
+
* client). Lazily requires the native module. Validated on device (R8).
|
|
123
|
+
*/
|
|
124
|
+
export function createAppAuthClient(options: AppAuthClientOptions = {}): OidcAuthClient {
|
|
125
|
+
// Lazy require so injecting a fake client never loads native code.
|
|
126
|
+
const AppAuth = require('react-native-app-auth');
|
|
127
|
+
const useDiscovery = options.useDiscovery ?? true;
|
|
128
|
+
|
|
129
|
+
const baseConfig = (
|
|
130
|
+
authority: string,
|
|
131
|
+
clientId: string,
|
|
132
|
+
redirectUrl: string,
|
|
133
|
+
scopes: string[],
|
|
134
|
+
) => ({
|
|
135
|
+
issuer: useDiscovery ? authority : undefined,
|
|
136
|
+
clientId,
|
|
137
|
+
redirectUrl,
|
|
138
|
+
scopes,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const toTokens = (result: {
|
|
142
|
+
accessToken?: string;
|
|
143
|
+
refreshToken?: string | null;
|
|
144
|
+
idToken?: string | null;
|
|
145
|
+
accessTokenExpirationDate?: string;
|
|
146
|
+
}): OidcTokens => ({
|
|
147
|
+
accessToken: result.accessToken,
|
|
148
|
+
// Absent stays absent: an empty string was indistinguishable from a real
|
|
149
|
+
// token by type, and silently disabled session persistence.
|
|
150
|
+
refreshToken: result.refreshToken || undefined,
|
|
151
|
+
idToken: result.idToken || undefined,
|
|
152
|
+
claims: decodeJwtClaims(result.idToken),
|
|
153
|
+
accessTokenExpiresAt: result.accessTokenExpirationDate
|
|
154
|
+
? new Date(result.accessTokenExpirationDate).getTime()
|
|
155
|
+
: undefined,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
async authorize(params) {
|
|
160
|
+
try {
|
|
161
|
+
const result = await AppAuth.authorize({
|
|
162
|
+
...baseConfig(params.authority, params.clientId, params.redirectUrl, params.scopes),
|
|
163
|
+
additionalParameters: params.additionalParameters,
|
|
164
|
+
});
|
|
165
|
+
return toTokens(result);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
throw classifyAppAuthError(error);
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
async refresh(params) {
|
|
171
|
+
try {
|
|
172
|
+
const result = await AppAuth.refresh(
|
|
173
|
+
baseConfig(params.authority, params.clientId, params.redirectUrl, params.scopes),
|
|
174
|
+
{ refreshToken: params.refreshToken },
|
|
175
|
+
);
|
|
176
|
+
return toTokens(result);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
throw classifyAppAuthError(error);
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
async endSession(params) {
|
|
182
|
+
try {
|
|
183
|
+
await AppAuth.logout(
|
|
184
|
+
baseConfig(params.authority, params.clientId, params.redirectUrl, []),
|
|
185
|
+
{
|
|
186
|
+
idToken: params.idToken ?? '',
|
|
187
|
+
postLogoutRedirectUrl: params.redirectUrl,
|
|
188
|
+
},
|
|
189
|
+
);
|
|
190
|
+
} catch (error) {
|
|
191
|
+
throw classifyAppAuthError(error);
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classifies a native OIDC error into an `AuthErrorCode`.
|
|
3
|
+
*
|
|
4
|
+
* The rules are an ordered table rather than a chain of substring checks, so a
|
|
5
|
+
* platform adapter (the iOS one, later) adds its own rows without touching the
|
|
6
|
+
* engine. Order matters: the first matching rule wins.
|
|
7
|
+
*
|
|
8
|
+
* The default is deliberately not "session expired". Restore clears the stored
|
|
9
|
+
* session only on `session-expired`, so anything this table cannot name — a
|
|
10
|
+
* timeout, a 5xx, a discovery failure, a localized message — must keep the
|
|
11
|
+
* session rather than sign the user out.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { AuthError, type AuthErrorCode } from '../diagnostics/errors';
|
|
15
|
+
|
|
16
|
+
export interface ClassificationRule {
|
|
17
|
+
readonly code: AuthErrorCode;
|
|
18
|
+
readonly messageKey: string;
|
|
19
|
+
/** Matched against the native error's `code` and `message`, case-insensitively. */
|
|
20
|
+
readonly pattern: RegExp;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* OAuth error codes (RFC 6749 §5.2, OIDC Core §3.1.2.6) and the Entra ID AADSTS
|
|
25
|
+
* codes that mean the same thing, plus the transport failures app-auth reports.
|
|
26
|
+
*/
|
|
27
|
+
export const DEFAULT_CLASSIFICATION_RULES: readonly ClassificationRule[] = [
|
|
28
|
+
{
|
|
29
|
+
code: 'provider-cancelled',
|
|
30
|
+
messageKey: 'auth.error.cancelled',
|
|
31
|
+
pattern: /cancel|user_cancel|authorization flow (was )?dismissed/i,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
code: 'session-expired',
|
|
35
|
+
messageKey: 'auth.error.sessionExpired',
|
|
36
|
+
// invalid_grant is the refresh-token verdict; the AADSTS codes are Entra's
|
|
37
|
+
// expired, revoked, or inactive refresh token and password-changed cases.
|
|
38
|
+
pattern:
|
|
39
|
+
/\binvalid_grant\b|\binteraction_required\b|\blogin_required\b|AADSTS(700082|700084|50173|70043)\b/i,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
code: 'redirect-mismatch',
|
|
43
|
+
messageKey: 'auth.error.redirectMismatch',
|
|
44
|
+
pattern: /redirect_uri|redirect uri|redirect_mismatch|AADSTS50011\b/i,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
code: 'consent-denied',
|
|
48
|
+
messageKey: 'auth.error.consentDenied',
|
|
49
|
+
pattern: /\baccess_denied\b|\bconsent_required\b|AADSTS65001\b/i,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
code: 'network-unavailable',
|
|
53
|
+
messageKey: 'auth.error.network',
|
|
54
|
+
pattern:
|
|
55
|
+
/network|connection|offline|timed? ?out|unable to resolve host|unreachable|\bserver_error\b|\btemporarily_unavailable\b|\b5\d\d\b/i,
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
/** Maps a native app-auth error to a classified AuthError without leaking tokens. */
|
|
60
|
+
export function classifyAppAuthError(
|
|
61
|
+
error: unknown,
|
|
62
|
+
rules: readonly ClassificationRule[] = DEFAULT_CLASSIFICATION_RULES,
|
|
63
|
+
): AuthError {
|
|
64
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
65
|
+
const nativeCode = (error as { code?: unknown } | undefined)?.code;
|
|
66
|
+
const haystack = `${typeof nativeCode === 'string' ? nativeCode : ''} ${message}`;
|
|
67
|
+
const rule = rules.find(candidate => candidate.pattern.test(haystack));
|
|
68
|
+
const code = rule?.code ?? 'token-acquisition-failed';
|
|
69
|
+
return new AuthError({
|
|
70
|
+
code,
|
|
71
|
+
messageKey: rule?.messageKey ?? 'auth.error.token',
|
|
72
|
+
audience: code === 'redirect-mismatch' ? 'developer' : 'user',
|
|
73
|
+
// AuthError redacts `detail`, so token-shaped values never survive.
|
|
74
|
+
detail: message,
|
|
75
|
+
});
|
|
76
|
+
}
|