@authhero/react-admin 0.10.0 → 0.12.0
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 +12 -0
- package/package.json +1 -1
- package/src/AuthCallback.tsx +41 -7
- package/src/authProvider.ts +415 -31
- package/src/components/TenantAppBar.tsx +22 -7
- package/src/components/clients/edit.tsx +43 -2
- package/src/components/organizations/edit.tsx +238 -2
- package/src/components/resource-servers/edit.tsx +130 -97
- package/src/dataProvider.ts +15 -6
- package/src/utils/tokenUtils.ts +142 -18
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
package/src/AuthCallback.tsx
CHANGED
|
@@ -2,7 +2,7 @@ import { useEffect, useState, useRef } from "react";
|
|
|
2
2
|
import { useNavigate, useLocation } from "react-router-dom";
|
|
3
3
|
import { CircularProgress, Box, Typography, Alert } from "@mui/material";
|
|
4
4
|
import { getSelectedDomainFromStorage } from "./utils/domainUtils";
|
|
5
|
-
import { createAuth0Client } from "./authProvider";
|
|
5
|
+
import { createAuth0Client, createAuth0ClientForOrg } from "./authProvider";
|
|
6
6
|
|
|
7
7
|
interface AuthCallbackProps {
|
|
8
8
|
onAuthComplete?: () => void;
|
|
@@ -55,19 +55,53 @@ export function AuthCallback({ onAuthComplete }: AuthCallbackProps) {
|
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
// Get the auth0 client for the selected domain
|
|
59
|
-
const auth0 = createAuth0Client(selectedDomain);
|
|
60
|
-
|
|
61
58
|
// Mark that we've processed this callback to prevent duplicate processing
|
|
62
59
|
callbackProcessedRef.current = true;
|
|
63
60
|
|
|
61
|
+
// First, try with the non-org client to handle the callback
|
|
62
|
+
// The Auth0 SDK will extract the token, and we can then check if it has org_id
|
|
63
|
+
const auth0 = createAuth0Client(selectedDomain);
|
|
64
|
+
|
|
64
65
|
// Process the redirect callback
|
|
65
66
|
// This is critical - it extracts the auth info from the URL and stores it
|
|
66
|
-
await auth0.handleRedirectCallback();
|
|
67
|
+
const result = await auth0.handleRedirectCallback();
|
|
68
|
+
|
|
69
|
+
// Get the return URL from appState, defaulting to /tenants
|
|
70
|
+
const returnTo = result?.appState?.returnTo || "/tenants";
|
|
71
|
+
|
|
72
|
+
// Check if the token has an organization by decoding the ID token
|
|
73
|
+
// If returnTo is a tenant path (not /tenants), we need to also cache for that org
|
|
74
|
+
const pathSegments = returnTo.split("/").filter(Boolean);
|
|
75
|
+
const possibleOrgId = pathSegments[0];
|
|
76
|
+
|
|
77
|
+
// If it looks like a tenant path (not 'tenants' itself), create an org client
|
|
78
|
+
// to ensure the token is also cached in the org-specific cache
|
|
79
|
+
if (possibleOrgId && possibleOrgId !== "tenants") {
|
|
80
|
+
try {
|
|
81
|
+
// Get the token from the non-org client and check if it has org_id
|
|
82
|
+
const token = await auth0.getIdTokenClaims();
|
|
83
|
+
if (token?.org_id === possibleOrgId) {
|
|
84
|
+
// Create the org client - this will also cache the token in the org-specific cache
|
|
85
|
+
const orgAuth0 = createAuth0ClientForOrg(
|
|
86
|
+
selectedDomain,
|
|
87
|
+
possibleOrgId,
|
|
88
|
+
);
|
|
89
|
+
// Try to get a token to populate the org cache
|
|
90
|
+
await orgAuth0.getTokenSilently().catch(() => {
|
|
91
|
+
// It's ok if this fails - the main callback was processed
|
|
92
|
+
console.log(
|
|
93
|
+
`[AuthCallback] Org token caching for ${possibleOrgId} deferred`,
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
// Non-critical - continue with navigation
|
|
99
|
+
}
|
|
100
|
+
}
|
|
67
101
|
|
|
68
102
|
// We're being redirected back from the authentication provider
|
|
69
|
-
//
|
|
70
|
-
forceNavigate(
|
|
103
|
+
// Navigate to the original URL or /tenants
|
|
104
|
+
forceNavigate(returnTo);
|
|
71
105
|
} catch (err) {
|
|
72
106
|
// Only set error for real errors, not for "already processed" errors
|
|
73
107
|
if (err instanceof Error) {
|
package/src/authProvider.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Auth0AuthProvider
|
|
1
|
+
import { Auth0AuthProvider } from "ra-auth-auth0";
|
|
2
2
|
import { Auth0Client } from "@auth0/auth0-spa-js";
|
|
3
3
|
import { ManagementClient } from "auth0";
|
|
4
4
|
import {
|
|
@@ -6,8 +6,13 @@ import {
|
|
|
6
6
|
getClientIdFromStorage,
|
|
7
7
|
getDomainFromStorage,
|
|
8
8
|
buildUrlWithProtocol,
|
|
9
|
+
formatDomain,
|
|
9
10
|
} from "./utils/domainUtils";
|
|
10
|
-
import getToken
|
|
11
|
+
import getToken, {
|
|
12
|
+
clearOrganizationTokenCache,
|
|
13
|
+
getOrganizationToken,
|
|
14
|
+
OrgCache,
|
|
15
|
+
} from "./utils/tokenUtils";
|
|
11
16
|
|
|
12
17
|
// Track auth requests globally
|
|
13
18
|
let authRequestInProgress = false;
|
|
@@ -17,13 +22,16 @@ const AUTH_REQUEST_DEBOUNCE_MS = 1000; // Debounce time between auth requests
|
|
|
17
22
|
// Store active sessions by domain
|
|
18
23
|
const activeSessions = new Map<string, boolean>();
|
|
19
24
|
|
|
20
|
-
// Cache for auth0 clients
|
|
25
|
+
// Cache for auth0 clients (domain only, no org)
|
|
21
26
|
const auth0ClientCache = new Map<string, Auth0Client>();
|
|
22
27
|
|
|
28
|
+
// Cache for organization-specific auth0 clients (domain:orgId)
|
|
29
|
+
const auth0OrgClientCache = new Map<string, Auth0Client>();
|
|
30
|
+
|
|
23
31
|
// Cache for management clients
|
|
24
32
|
const managementClientCache = new Map<string, ManagementClient>();
|
|
25
33
|
|
|
26
|
-
// Create a function to get Auth0Client with the specified domain
|
|
34
|
+
// Create a function to get Auth0Client with the specified domain (no organization)
|
|
27
35
|
export const createAuth0Client = (domain: string) => {
|
|
28
36
|
// Check cache first to avoid creating multiple clients for the same domain
|
|
29
37
|
if (auth0ClientCache.has(domain)) {
|
|
@@ -37,13 +45,18 @@ export const createAuth0Client = (domain: string) => {
|
|
|
37
45
|
const currentUrl = new URL(window.location.href);
|
|
38
46
|
const redirectUri = `${currentUrl.protocol}//${currentUrl.host}/auth-callback`;
|
|
39
47
|
|
|
48
|
+
// Use the management API audience for cross-tenant operations
|
|
49
|
+
// This allows the admin UI to manage tenants and their resources
|
|
50
|
+
const audience =
|
|
51
|
+
import.meta.env.VITE_AUTH0_AUDIENCE || "urn:authhero:management";
|
|
52
|
+
|
|
40
53
|
const auth0Client = new Auth0Client({
|
|
41
54
|
domain: fullDomain,
|
|
42
55
|
clientId: getClientIdFromStorage(domain),
|
|
43
56
|
cacheLocation: "localstorage",
|
|
44
|
-
useRefreshTokens:
|
|
57
|
+
useRefreshTokens: false,
|
|
45
58
|
authorizationParams: {
|
|
46
|
-
audience
|
|
59
|
+
audience,
|
|
47
60
|
redirect_uri: redirectUri,
|
|
48
61
|
scope: "openid profile email auth:read auth:write",
|
|
49
62
|
},
|
|
@@ -101,6 +114,53 @@ export const createAuth0Client = (domain: string) => {
|
|
|
101
114
|
return auth0Client;
|
|
102
115
|
};
|
|
103
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Create an Auth0Client for a specific organization with isolated cache.
|
|
119
|
+
* This ensures tokens for different organizations don't interfere with each other.
|
|
120
|
+
*/
|
|
121
|
+
export const createAuth0ClientForOrg = (
|
|
122
|
+
domain: string,
|
|
123
|
+
organizationId: string,
|
|
124
|
+
) => {
|
|
125
|
+
const cacheKey = `${domain}:${organizationId}`;
|
|
126
|
+
|
|
127
|
+
// Check cache first
|
|
128
|
+
if (auth0OrgClientCache.has(cacheKey)) {
|
|
129
|
+
return auth0OrgClientCache.get(cacheKey)!;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Build full domain URL with HTTPS
|
|
133
|
+
const fullDomain = buildUrlWithProtocol(domain);
|
|
134
|
+
|
|
135
|
+
// Get redirect URI from current app URL
|
|
136
|
+
const currentUrl = new URL(window.location.href);
|
|
137
|
+
const redirectUri = `${currentUrl.protocol}//${currentUrl.host}/auth-callback`;
|
|
138
|
+
|
|
139
|
+
// Use the management API audience for cross-tenant operations
|
|
140
|
+
// The org_id claim in the token determines which tenant's resources are accessed
|
|
141
|
+
const audience =
|
|
142
|
+
import.meta.env.VITE_AUTH0_AUDIENCE || "urn:authhero:management";
|
|
143
|
+
|
|
144
|
+
const auth0Client = new Auth0Client({
|
|
145
|
+
domain: fullDomain,
|
|
146
|
+
clientId: getClientIdFromStorage(domain),
|
|
147
|
+
useRefreshTokens: false,
|
|
148
|
+
// Use organization-specific cache to isolate tokens
|
|
149
|
+
// Note: Don't use cacheLocation when providing a custom cache
|
|
150
|
+
cache: new OrgCache(organizationId),
|
|
151
|
+
authorizationParams: {
|
|
152
|
+
audience,
|
|
153
|
+
redirect_uri: redirectUri,
|
|
154
|
+
scope: "openid profile email auth:read auth:write",
|
|
155
|
+
organization: organizationId,
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Store in cache
|
|
160
|
+
auth0OrgClientCache.set(cacheKey, auth0Client);
|
|
161
|
+
return auth0Client;
|
|
162
|
+
};
|
|
163
|
+
|
|
104
164
|
// Create a Management API client
|
|
105
165
|
export const createManagementClient = async (
|
|
106
166
|
apiDomain: string,
|
|
@@ -115,9 +175,11 @@ export const createManagementClient = async (
|
|
|
115
175
|
}
|
|
116
176
|
|
|
117
177
|
// Use oauthDomain for finding credentials, fallback to apiDomain
|
|
118
|
-
const domainForAuth = oauthDomain || apiDomain;
|
|
178
|
+
const domainForAuth = formatDomain(oauthDomain || apiDomain);
|
|
119
179
|
const domains = getDomainFromStorage();
|
|
120
|
-
const domainConfig = domains.find(
|
|
180
|
+
const domainConfig = domains.find(
|
|
181
|
+
(d) => formatDomain(d.url) === domainForAuth,
|
|
182
|
+
);
|
|
121
183
|
|
|
122
184
|
if (!domainConfig) {
|
|
123
185
|
throw new Error(
|
|
@@ -125,14 +187,34 @@ export const createManagementClient = async (
|
|
|
125
187
|
);
|
|
126
188
|
}
|
|
127
189
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (
|
|
131
|
-
|
|
190
|
+
let token: string;
|
|
191
|
+
|
|
192
|
+
if (tenantId) {
|
|
193
|
+
// When accessing tenant-specific resources, use org-scoped token
|
|
194
|
+
if (domainConfig.connectionMethod === "login") {
|
|
195
|
+
// For OAuth login, use organization-scoped client
|
|
196
|
+
const orgAuth0Client = createAuth0ClientForOrg(domainForAuth, tenantId);
|
|
197
|
+
const audience =
|
|
198
|
+
import.meta.env.VITE_AUTH0_AUDIENCE || "urn:authhero:management";
|
|
199
|
+
token = await orgAuth0Client.getTokenSilently({
|
|
200
|
+
authorizationParams: {
|
|
201
|
+
audience,
|
|
202
|
+
organization: tenantId,
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
} else {
|
|
206
|
+
// For token/client_credentials, use getOrganizationToken
|
|
207
|
+
token = await getOrganizationToken(domainConfig, tenantId);
|
|
208
|
+
}
|
|
209
|
+
} else {
|
|
210
|
+
// No tenantId - get non-org-scoped token for tenant management endpoints
|
|
211
|
+
let auth0Client: Auth0Client | undefined;
|
|
212
|
+
if (domainConfig.connectionMethod === "login") {
|
|
213
|
+
auth0Client = createAuth0Client(domainForAuth);
|
|
214
|
+
}
|
|
215
|
+
token = await getToken(domainConfig, auth0Client);
|
|
132
216
|
}
|
|
133
217
|
|
|
134
|
-
const token = await getToken(domainConfig, auth0Client);
|
|
135
|
-
|
|
136
218
|
// ManagementClient expects domain WITHOUT protocol (it adds https:// internally)
|
|
137
219
|
const managementClient = new ManagementClient({
|
|
138
220
|
domain: apiDomain,
|
|
@@ -149,6 +231,7 @@ export const createManagementClient = async (
|
|
|
149
231
|
// but callers typically only have access to the OAuth domain
|
|
150
232
|
export const clearManagementClientCache = () => {
|
|
151
233
|
managementClientCache.clear();
|
|
234
|
+
clearOrganizationTokenCache();
|
|
152
235
|
};
|
|
153
236
|
|
|
154
237
|
// Create a function to get the auth provider with the specified domain
|
|
@@ -158,7 +241,10 @@ export const getAuthProvider = (
|
|
|
158
241
|
) => {
|
|
159
242
|
// Get domain config to check connection method
|
|
160
243
|
const domains = getDomainFromStorage();
|
|
161
|
-
const
|
|
244
|
+
const formattedDomain = formatDomain(domain);
|
|
245
|
+
const domainConfig = domains.find(
|
|
246
|
+
(d) => formatDomain(d.url) === formattedDomain,
|
|
247
|
+
);
|
|
162
248
|
|
|
163
249
|
// If using token auth or client credentials, create a simple auth provider that uses the token
|
|
164
250
|
if (
|
|
@@ -332,17 +418,23 @@ const authorizedHttpClient = (url: string, options: HttpOptions = {}) => {
|
|
|
332
418
|
// Check if we're using token-based auth or client credentials
|
|
333
419
|
const domains = getDomainFromStorage();
|
|
334
420
|
const selectedDomain = getSelectedDomainFromStorage();
|
|
335
|
-
const
|
|
421
|
+
const formattedSelectedDomain = formatDomain(selectedDomain);
|
|
422
|
+
const domainConfig = domains.find(
|
|
423
|
+
(d) => formatDomain(d.url) === formattedSelectedDomain,
|
|
424
|
+
);
|
|
336
425
|
|
|
337
426
|
// If using login method and auth request is in progress, delay the request
|
|
338
427
|
if (
|
|
339
428
|
domainConfig?.connectionMethod === "login" &&
|
|
340
|
-
(authRequestInProgress || activeSessions.has(
|
|
429
|
+
(authRequestInProgress || activeSessions.has(formattedSelectedDomain))
|
|
341
430
|
) {
|
|
342
431
|
// Return a promise that waits for auth to complete
|
|
343
432
|
const delayedRequest = new Promise((resolve, reject) => {
|
|
344
433
|
const checkInterval = setInterval(() => {
|
|
345
|
-
if (
|
|
434
|
+
if (
|
|
435
|
+
!authRequestInProgress &&
|
|
436
|
+
!activeSessions.has(formattedSelectedDomain)
|
|
437
|
+
) {
|
|
346
438
|
clearInterval(checkInterval);
|
|
347
439
|
// Retry the request now that auth is complete
|
|
348
440
|
authorizedHttpClient(url, options).then(resolve).catch(reject);
|
|
@@ -478,16 +570,62 @@ const authorizedHttpClient = (url: string, options: HttpOptions = {}) => {
|
|
|
478
570
|
throw error;
|
|
479
571
|
});
|
|
480
572
|
} else {
|
|
481
|
-
// For Auth0 login method,
|
|
573
|
+
// For Auth0 login method, get a token WITHOUT organization scope
|
|
574
|
+
// This ensures we don't accidentally use an org-scoped token when listing tenants
|
|
482
575
|
const currentAuth0Client = createAuth0Client(selectedDomain);
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
(
|
|
490
|
-
|
|
576
|
+
request = getToken(domainConfig, currentAuth0Client)
|
|
577
|
+
.catch((error) => {
|
|
578
|
+
throw new Error(
|
|
579
|
+
`Authentication failed: ${error.message}. Please log in again.`,
|
|
580
|
+
);
|
|
581
|
+
})
|
|
582
|
+
.then((token) => {
|
|
583
|
+
const headersObj = new Headers();
|
|
584
|
+
headersObj.set("Authorization", `Bearer ${token}`);
|
|
585
|
+
const method = (options.method || "GET").toUpperCase();
|
|
586
|
+
if (method === "POST" || method === "PATCH") {
|
|
587
|
+
headersObj.set("content-type", "application/json");
|
|
588
|
+
}
|
|
589
|
+
return fetch(url, { ...options, headers: headersObj });
|
|
590
|
+
})
|
|
591
|
+
.then(async (response) => {
|
|
592
|
+
if (response.status < 200 || response.status >= 300) {
|
|
593
|
+
const text = await response.text();
|
|
594
|
+
|
|
595
|
+
// Check for 401 with "Bad audience" message on /v2/tenants endpoint
|
|
596
|
+
if (
|
|
597
|
+
response.status === 401 &&
|
|
598
|
+
text.includes("Bad audience") &&
|
|
599
|
+
url.includes("/v2/tenants")
|
|
600
|
+
) {
|
|
601
|
+
console.warn(
|
|
602
|
+
"Auth0 server detected without multi-tenant support. Navigating to Auth0 page",
|
|
603
|
+
);
|
|
604
|
+
pendingRequests.delete(requestKey);
|
|
605
|
+
window.history.pushState({}, "", "/auth0");
|
|
606
|
+
window.dispatchEvent(new PopStateEvent("popstate"));
|
|
607
|
+
throw new Error("Navigating to Auth0 configuration");
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
throw new Error(text || response.statusText);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const text = await response.text();
|
|
614
|
+
let json;
|
|
615
|
+
try {
|
|
616
|
+
json = JSON.parse(text);
|
|
617
|
+
} catch (e) {
|
|
618
|
+
json = text;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
return {
|
|
622
|
+
json,
|
|
623
|
+
status: response.status,
|
|
624
|
+
headers: response.headers,
|
|
625
|
+
body: text,
|
|
626
|
+
};
|
|
627
|
+
})
|
|
628
|
+
.catch((error) => {
|
|
491
629
|
if (error.message === "Failed to fetch" || error.name === "TypeError") {
|
|
492
630
|
const urlObj = new URL(url);
|
|
493
631
|
if (
|
|
@@ -495,8 +633,7 @@ const authorizedHttpClient = (url: string, options: HttpOptions = {}) => {
|
|
|
495
633
|
urlObj.hostname === "127.0.0.1"
|
|
496
634
|
) {
|
|
497
635
|
const certError = new Error(
|
|
498
|
-
`Unable to connect to ${urlObj.origin}. This may be due to an untrusted SSL certificate
|
|
499
|
-
`Please visit ${urlObj.origin} in your browser and accept the security warning to trust the certificate, then refresh this page.`,
|
|
636
|
+
`Unable to connect to ${urlObj.origin}. This may be due to an untrusted SSL certificate.`,
|
|
500
637
|
);
|
|
501
638
|
(certError as any).isCertificateError = true;
|
|
502
639
|
(certError as any).serverUrl = urlObj.origin;
|
|
@@ -504,8 +641,7 @@ const authorizedHttpClient = (url: string, options: HttpOptions = {}) => {
|
|
|
504
641
|
}
|
|
505
642
|
}
|
|
506
643
|
throw error;
|
|
507
|
-
}
|
|
508
|
-
);
|
|
644
|
+
});
|
|
509
645
|
}
|
|
510
646
|
|
|
511
647
|
// Handle cleanup when request is done
|
|
@@ -518,4 +654,252 @@ const authorizedHttpClient = (url: string, options: HttpOptions = {}) => {
|
|
|
518
654
|
return request;
|
|
519
655
|
};
|
|
520
656
|
|
|
657
|
+
/**
|
|
658
|
+
* Creates an HTTP client that uses organization-scoped tokens.
|
|
659
|
+
* This is used when accessing tenant-specific resources to ensure
|
|
660
|
+
* the user has the correct permissions for that tenant.
|
|
661
|
+
*
|
|
662
|
+
* @param organizationId - The organization/tenant ID to scope tokens to
|
|
663
|
+
* @returns An HTTP client function that uses organization-scoped tokens
|
|
664
|
+
*/
|
|
665
|
+
export const createOrganizationHttpClient = (organizationId: string) => {
|
|
666
|
+
return (url: string, options: HttpOptions = {}) => {
|
|
667
|
+
const requestKey = `${organizationId}:${url}-${JSON.stringify(options)}`;
|
|
668
|
+
|
|
669
|
+
// If there's already a pending request for this URL and options, return it
|
|
670
|
+
if (pendingRequests.has(requestKey)) {
|
|
671
|
+
return pendingRequests.get(requestKey)!;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Prevent API calls while on the auth callback page
|
|
675
|
+
if (window.location.pathname === "/auth-callback") {
|
|
676
|
+
return Promise.reject({
|
|
677
|
+
json: {
|
|
678
|
+
message: "Authentication in progress. Please wait...",
|
|
679
|
+
},
|
|
680
|
+
status: 401,
|
|
681
|
+
headers: new Headers(),
|
|
682
|
+
body: JSON.stringify({ message: "Authentication in progress" }),
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Check if we're using token-based auth or client credentials
|
|
687
|
+
const domains = getDomainFromStorage();
|
|
688
|
+
const selectedDomain = getSelectedDomainFromStorage();
|
|
689
|
+
const formattedSelectedDomain = formatDomain(selectedDomain);
|
|
690
|
+
const domainConfig = domains.find(
|
|
691
|
+
(d) => formatDomain(d.url) === formattedSelectedDomain,
|
|
692
|
+
);
|
|
693
|
+
|
|
694
|
+
// If using login method and auth request is in progress, delay the request
|
|
695
|
+
if (
|
|
696
|
+
domainConfig?.connectionMethod === "login" &&
|
|
697
|
+
(authRequestInProgress || activeSessions.has(formattedSelectedDomain))
|
|
698
|
+
) {
|
|
699
|
+
const delayedRequest = new Promise((resolve, reject) => {
|
|
700
|
+
const checkInterval = setInterval(() => {
|
|
701
|
+
if (
|
|
702
|
+
!authRequestInProgress &&
|
|
703
|
+
!activeSessions.has(formattedSelectedDomain)
|
|
704
|
+
) {
|
|
705
|
+
clearInterval(checkInterval);
|
|
706
|
+
createOrganizationHttpClient(organizationId)(url, options)
|
|
707
|
+
.then(resolve)
|
|
708
|
+
.catch(reject);
|
|
709
|
+
}
|
|
710
|
+
}, 100);
|
|
711
|
+
|
|
712
|
+
setTimeout(() => {
|
|
713
|
+
clearInterval(checkInterval);
|
|
714
|
+
reject(new Error("Authentication timeout"));
|
|
715
|
+
}, 30000);
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
pendingRequests.set(requestKey, delayedRequest);
|
|
719
|
+
delayedRequest.finally(() => {
|
|
720
|
+
pendingRequests.delete(requestKey);
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
return delayedRequest;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
if (!domainConfig) {
|
|
727
|
+
return Promise.reject({
|
|
728
|
+
json: {
|
|
729
|
+
message:
|
|
730
|
+
"No domain configuration found. Please select a domain and configure authentication.",
|
|
731
|
+
},
|
|
732
|
+
status: 401,
|
|
733
|
+
headers: new Headers(),
|
|
734
|
+
body: JSON.stringify({ message: "No domain configuration found" }),
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
let request;
|
|
739
|
+
if (
|
|
740
|
+
["token", "client_credentials"].includes(
|
|
741
|
+
domainConfig.connectionMethod || "",
|
|
742
|
+
)
|
|
743
|
+
) {
|
|
744
|
+
// For token/client_credentials, use organization-scoped token
|
|
745
|
+
// This includes the org_id claim for accessing tenant-specific resources
|
|
746
|
+
request = getOrganizationToken(domainConfig, organizationId)
|
|
747
|
+
.catch((error) => {
|
|
748
|
+
throw new Error(
|
|
749
|
+
`Authentication failed: ${error.message}. Please configure your credentials in the domain selector.`,
|
|
750
|
+
);
|
|
751
|
+
})
|
|
752
|
+
.then((token) => {
|
|
753
|
+
const headersObj = new Headers();
|
|
754
|
+
headersObj.set("Authorization", `Bearer ${token}`);
|
|
755
|
+
const method = (options.method || "GET").toUpperCase();
|
|
756
|
+
if (method === "POST" || method === "PATCH") {
|
|
757
|
+
headersObj.set("content-type", "application/json");
|
|
758
|
+
}
|
|
759
|
+
return fetch(url, { ...options, headers: headersObj });
|
|
760
|
+
})
|
|
761
|
+
.then(async (response) => {
|
|
762
|
+
if (response.status < 200 || response.status >= 300) {
|
|
763
|
+
const text = await response.text();
|
|
764
|
+
throw new Error(text || response.statusText);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
const text = await response.text();
|
|
768
|
+
let json;
|
|
769
|
+
try {
|
|
770
|
+
json = JSON.parse(text);
|
|
771
|
+
} catch (e) {
|
|
772
|
+
json = text;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
return {
|
|
776
|
+
json,
|
|
777
|
+
status: response.status,
|
|
778
|
+
headers: response.headers,
|
|
779
|
+
body: text,
|
|
780
|
+
};
|
|
781
|
+
})
|
|
782
|
+
.catch((error) => {
|
|
783
|
+
if (
|
|
784
|
+
error.message === "Failed to fetch" ||
|
|
785
|
+
error.name === "TypeError"
|
|
786
|
+
) {
|
|
787
|
+
const urlObj = new URL(url);
|
|
788
|
+
if (
|
|
789
|
+
urlObj.hostname === "localhost" ||
|
|
790
|
+
urlObj.hostname === "127.0.0.1"
|
|
791
|
+
) {
|
|
792
|
+
const certError = new Error(
|
|
793
|
+
`Unable to connect to ${urlObj.origin}. This may be due to an untrusted SSL certificate.`,
|
|
794
|
+
);
|
|
795
|
+
(certError as any).isCertificateError = true;
|
|
796
|
+
(certError as any).serverUrl = urlObj.origin;
|
|
797
|
+
throw certError;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
throw error;
|
|
801
|
+
});
|
|
802
|
+
} else {
|
|
803
|
+
// For OAuth login, use an organization-specific client with isolated cache
|
|
804
|
+
const orgAuth0Client = createAuth0ClientForOrg(
|
|
805
|
+
selectedDomain,
|
|
806
|
+
organizationId,
|
|
807
|
+
);
|
|
808
|
+
|
|
809
|
+
// Use the management API audience for cross-tenant operations
|
|
810
|
+
// The org_id in the token will determine which tenant's resources are being accessed
|
|
811
|
+
const audience =
|
|
812
|
+
import.meta.env.VITE_AUTH0_AUDIENCE || "urn:authhero:management";
|
|
813
|
+
|
|
814
|
+
// First, check if we have a valid session for this organization
|
|
815
|
+
request = orgAuth0Client
|
|
816
|
+
.getTokenSilently({
|
|
817
|
+
authorizationParams: {
|
|
818
|
+
audience,
|
|
819
|
+
organization: organizationId,
|
|
820
|
+
},
|
|
821
|
+
})
|
|
822
|
+
.catch(async (error) => {
|
|
823
|
+
// If silent token acquisition fails, we need to redirect to login with org
|
|
824
|
+
// Get the base auth0 client to get the user's email for login hint
|
|
825
|
+
const baseClient = createAuth0Client(selectedDomain);
|
|
826
|
+
const user = await baseClient.getUser().catch(() => null);
|
|
827
|
+
|
|
828
|
+
// Redirect to login with organization
|
|
829
|
+
await orgAuth0Client.loginWithRedirect({
|
|
830
|
+
authorizationParams: {
|
|
831
|
+
organization: organizationId,
|
|
832
|
+
login_hint: user?.email,
|
|
833
|
+
},
|
|
834
|
+
appState: {
|
|
835
|
+
returnTo: window.location.pathname,
|
|
836
|
+
},
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
// This won't be reached as loginWithRedirect redirects the page
|
|
840
|
+
throw new Error(
|
|
841
|
+
`Redirecting to login for organization ${organizationId}`,
|
|
842
|
+
);
|
|
843
|
+
})
|
|
844
|
+
.then((token) => {
|
|
845
|
+
const headersObj = new Headers();
|
|
846
|
+
headersObj.set("Authorization", `Bearer ${token}`);
|
|
847
|
+
const method = (options.method || "GET").toUpperCase();
|
|
848
|
+
if (method === "POST" || method === "PATCH") {
|
|
849
|
+
headersObj.set("content-type", "application/json");
|
|
850
|
+
}
|
|
851
|
+
return fetch(url, { ...options, headers: headersObj });
|
|
852
|
+
})
|
|
853
|
+
.then(async (response) => {
|
|
854
|
+
if (response.status < 200 || response.status >= 300) {
|
|
855
|
+
const text = await response.text();
|
|
856
|
+
throw new Error(text || response.statusText);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
const text = await response.text();
|
|
860
|
+
let json;
|
|
861
|
+
try {
|
|
862
|
+
json = JSON.parse(text);
|
|
863
|
+
} catch (e) {
|
|
864
|
+
json = text;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
return {
|
|
868
|
+
json,
|
|
869
|
+
status: response.status,
|
|
870
|
+
headers: response.headers,
|
|
871
|
+
body: text,
|
|
872
|
+
};
|
|
873
|
+
})
|
|
874
|
+
.catch((error) => {
|
|
875
|
+
if (
|
|
876
|
+
error.message === "Failed to fetch" ||
|
|
877
|
+
error.name === "TypeError"
|
|
878
|
+
) {
|
|
879
|
+
const urlObj = new URL(url);
|
|
880
|
+
if (
|
|
881
|
+
urlObj.hostname === "localhost" ||
|
|
882
|
+
urlObj.hostname === "127.0.0.1"
|
|
883
|
+
) {
|
|
884
|
+
const certError = new Error(
|
|
885
|
+
`Unable to connect to ${urlObj.origin}. This may be due to an untrusted SSL certificate.`,
|
|
886
|
+
);
|
|
887
|
+
(certError as any).isCertificateError = true;
|
|
888
|
+
(certError as any).serverUrl = urlObj.origin;
|
|
889
|
+
throw certError;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
throw error;
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
request.finally(() => {
|
|
897
|
+
pendingRequests.delete(requestKey);
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
pendingRequests.set(requestKey, request);
|
|
901
|
+
return request;
|
|
902
|
+
};
|
|
903
|
+
};
|
|
904
|
+
|
|
521
905
|
export { authProvider, authorizedHttpClient };
|