@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/src/dataProvider.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { UpdateParams, withLifecycleCallbacks } from "react-admin";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
authorizedHttpClient,
|
|
4
|
+
createOrganizationHttpClient,
|
|
5
|
+
} from "./authProvider";
|
|
3
6
|
import auth0DataProvider from "./auth0DataProvider";
|
|
4
7
|
import {
|
|
5
8
|
getDomainFromStorage,
|
|
6
9
|
buildUrlWithProtocol,
|
|
10
|
+
formatDomain,
|
|
7
11
|
} from "./utils/domainUtils";
|
|
8
12
|
|
|
9
13
|
async function removeExtraFields(params: UpdateParams) {
|
|
@@ -29,7 +33,8 @@ export function getDataprovider(auth0Domain?: string) {
|
|
|
29
33
|
if (auth0Domain) {
|
|
30
34
|
// Check if there's a custom REST API URL configured for this domain
|
|
31
35
|
const domains = getDomainFromStorage();
|
|
32
|
-
const
|
|
36
|
+
const formattedAuth0Domain = formatDomain(auth0Domain);
|
|
37
|
+
const domainConfig = domains.find((d) => formatDomain(d.url) === formattedAuth0Domain);
|
|
33
38
|
|
|
34
39
|
if (domainConfig?.restApiUrl) {
|
|
35
40
|
// Use the custom REST API URL if configured
|
|
@@ -67,8 +72,8 @@ export function getDataproviderForTenant(
|
|
|
67
72
|
if (auth0Domain) {
|
|
68
73
|
// Check if there's a custom REST API URL configured for this domain
|
|
69
74
|
const domains = getDomainFromStorage();
|
|
70
|
-
|
|
71
|
-
const domainConfig = domains.find((d) => d.url ===
|
|
75
|
+
const formattedAuth0Domain = formatDomain(auth0Domain);
|
|
76
|
+
const domainConfig = domains.find((d) => formatDomain(d.url) === formattedAuth0Domain);
|
|
72
77
|
|
|
73
78
|
if (domainConfig?.restApiUrl) {
|
|
74
79
|
// Use the custom REST API URL if configured
|
|
@@ -85,10 +90,14 @@ export function getDataproviderForTenant(
|
|
|
85
90
|
// Ensure apiUrl doesn't end with a slash
|
|
86
91
|
apiUrl = apiUrl.replace(/\/$/, "");
|
|
87
92
|
|
|
88
|
-
// Create
|
|
93
|
+
// Create an organization-scoped HTTP client for this tenant
|
|
94
|
+
// This ensures the user has the correct permissions for accessing tenant resources
|
|
95
|
+
const organizationHttpClient = createOrganizationHttpClient(tenantId);
|
|
96
|
+
|
|
97
|
+
// Create the auth0Provider with the API URL, tenant ID, domain, and org-scoped client
|
|
89
98
|
const auth0Provider = auth0DataProvider(
|
|
90
99
|
apiUrl,
|
|
91
|
-
|
|
100
|
+
organizationHttpClient,
|
|
92
101
|
tenantId,
|
|
93
102
|
auth0Domain,
|
|
94
103
|
);
|
package/src/utils/tokenUtils.ts
CHANGED
|
@@ -1,32 +1,93 @@
|
|
|
1
1
|
import { DomainConfig } from "./domainUtils";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
Auth0Client,
|
|
4
|
+
ICache,
|
|
5
|
+
Cacheable,
|
|
6
|
+
MaybePromise,
|
|
7
|
+
} from "@auth0/auth0-spa-js";
|
|
8
|
+
|
|
9
|
+
// Import the CACHE_KEY_PREFIX constant for consistency with the Auth0 SDK
|
|
10
|
+
const CACHE_KEY_PREFIX = "@@auth0spajs@@";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Custom cache provider that wraps localStorage and adds organization isolation.
|
|
14
|
+
* This ensures that tokens and auth state are kept separate between organizations.
|
|
15
|
+
* Based on the original LocalStorageCache implementation from auth0-spa-js.
|
|
16
|
+
*/
|
|
17
|
+
export class OrgCache implements ICache {
|
|
18
|
+
private orgId: string;
|
|
19
|
+
|
|
20
|
+
constructor(orgId: string) {
|
|
21
|
+
this.orgId = orgId;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public set<T = Cacheable>(key: string, entry: T) {
|
|
25
|
+
const orgKey = `${key}:${this.orgId}`;
|
|
26
|
+
localStorage.setItem(orgKey, JSON.stringify(entry));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public get<T = Cacheable>(key: string): MaybePromise<T | undefined> {
|
|
30
|
+
const orgKey = `${key}:${this.orgId}`;
|
|
31
|
+
const json = window.localStorage.getItem(orgKey);
|
|
32
|
+
|
|
33
|
+
if (!json) return;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const payload = JSON.parse(json) as T;
|
|
37
|
+
return payload;
|
|
38
|
+
} catch (e) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public remove(key: string) {
|
|
44
|
+
const orgKey = `${key}:${this.orgId}`;
|
|
45
|
+
localStorage.removeItem(orgKey);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public allKeys() {
|
|
49
|
+
const orgSuffix = `:${this.orgId}`;
|
|
50
|
+
return Object.keys(window.localStorage).filter(
|
|
51
|
+
(key) => key.startsWith(CACHE_KEY_PREFIX) && key.endsWith(orgSuffix),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
3
55
|
|
|
4
56
|
async function fetchTokenWithClientCredentials(
|
|
5
57
|
domain: string,
|
|
6
58
|
clientId: string,
|
|
7
59
|
clientSecret: string,
|
|
60
|
+
organizationId?: string,
|
|
8
61
|
): Promise<string> {
|
|
62
|
+
const body: Record<string, string> = {
|
|
63
|
+
grant_type: "client_credentials",
|
|
64
|
+
client_id: clientId,
|
|
65
|
+
client_secret: clientSecret,
|
|
66
|
+
// Use the management API audience for cross-tenant operations
|
|
67
|
+
audience: "urn:authhero:management",
|
|
68
|
+
scope:
|
|
69
|
+
"read:users update:users create:users delete:users read:user_idp_tokens " +
|
|
70
|
+
"read:clients update:clients create:clients delete:clients " +
|
|
71
|
+
"read:connections update:connections create:connections delete:connections " +
|
|
72
|
+
"read:resource_servers update:resource_servers create:resource_servers delete:resource_servers " +
|
|
73
|
+
"read:rules update:rules create:rules delete:rules " +
|
|
74
|
+
"read:email_templates update:email_templates " +
|
|
75
|
+
"read:tenant_settings update:tenant_settings " +
|
|
76
|
+
"read:logs read:stats read:branding update:branding read:forms",
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Add organization if specified - this will include org_id in the token
|
|
80
|
+
if (organizationId) {
|
|
81
|
+
body.organization = organizationId;
|
|
82
|
+
}
|
|
83
|
+
|
|
9
84
|
const response = await fetch(`https://proxy.authhe.ro/oauth/token`, {
|
|
10
85
|
method: "POST",
|
|
11
86
|
headers: {
|
|
12
87
|
"Content-Type": "application/json",
|
|
13
88
|
"X-Auth0-Domain": domain,
|
|
14
89
|
},
|
|
15
|
-
body: JSON.stringify(
|
|
16
|
-
grant_type: "client_credentials",
|
|
17
|
-
client_id: clientId,
|
|
18
|
-
client_secret: clientSecret,
|
|
19
|
-
audience: `https://${domain}/api/v2/`,
|
|
20
|
-
scope:
|
|
21
|
-
"read:users update:users create:users delete:users read:user_idp_tokens " +
|
|
22
|
-
"read:clients update:clients create:clients delete:clients " +
|
|
23
|
-
"read:connections update:connections create:connections delete:connections " +
|
|
24
|
-
"read:resource_servers update:resource_servers create:resource_servers delete:resource_servers " +
|
|
25
|
-
"read:rules update:rules create:rules delete:rules " +
|
|
26
|
-
"read:email_templates update:email_templates " +
|
|
27
|
-
"read:tenant_settings update:tenant_settings " +
|
|
28
|
-
"read:logs read:stats read:branding update:branding read:forms",
|
|
29
|
-
}),
|
|
90
|
+
body: JSON.stringify(body),
|
|
30
91
|
});
|
|
31
92
|
|
|
32
93
|
if (!response.ok) {
|
|
@@ -37,6 +98,64 @@ async function fetchTokenWithClientCredentials(
|
|
|
37
98
|
return data.access_token;
|
|
38
99
|
}
|
|
39
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Clear the organization token cache (e.g., on logout).
|
|
103
|
+
* This clears all organization-specific localStorage entries.
|
|
104
|
+
*/
|
|
105
|
+
export function clearOrganizationTokenCache(): void {
|
|
106
|
+
// Clear all org-cached tokens from localStorage
|
|
107
|
+
const keysToRemove = Object.keys(window.localStorage).filter(
|
|
108
|
+
(key) => key.startsWith(CACHE_KEY_PREFIX) && key.match(/:[^:]+$/),
|
|
109
|
+
);
|
|
110
|
+
keysToRemove.forEach((key) => localStorage.removeItem(key));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Get an organization-scoped token for the given domain configuration.
|
|
115
|
+
* This token will have the org_id claim set to the specified organization.
|
|
116
|
+
* Used for accessing tenant-specific resources.
|
|
117
|
+
*/
|
|
118
|
+
export async function getOrganizationToken(
|
|
119
|
+
domainConfig: DomainConfig,
|
|
120
|
+
organizationId: string,
|
|
121
|
+
): Promise<string> {
|
|
122
|
+
if (
|
|
123
|
+
domainConfig.connectionMethod === "client_credentials" &&
|
|
124
|
+
domainConfig.clientId &&
|
|
125
|
+
domainConfig.clientSecret
|
|
126
|
+
) {
|
|
127
|
+
// For client credentials, fetch a token with organization parameter
|
|
128
|
+
const token = await fetchTokenWithClientCredentials(
|
|
129
|
+
domainConfig.url,
|
|
130
|
+
domainConfig.clientId,
|
|
131
|
+
domainConfig.clientSecret,
|
|
132
|
+
organizationId,
|
|
133
|
+
);
|
|
134
|
+
return token;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// For token-based auth, we can't add org_id dynamically
|
|
138
|
+
// The token must already have the correct org_id claim
|
|
139
|
+
if (domainConfig.connectionMethod === "token") {
|
|
140
|
+
// Static tokens cannot have dynamic org_id - this is a limitation
|
|
141
|
+
throw new Error(
|
|
142
|
+
"Token-based auth cannot provide organization-scoped tokens. " +
|
|
143
|
+
"Use client_credentials or login authentication method for multi-tenant access.",
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// For login method, organization-scoped tokens are handled separately
|
|
148
|
+
// via OAuth with organization parameter in createOrganizationHttpClient
|
|
149
|
+
throw new Error(
|
|
150
|
+
"Organization-scoped tokens require client_credentials or login authentication method.",
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Get a token for the given domain configuration.
|
|
156
|
+
* For OAuth login, this gets a token WITHOUT organization scope.
|
|
157
|
+
* Use createAuth0ClientForOrg for organization-scoped tokens.
|
|
158
|
+
*/
|
|
40
159
|
export default async function getToken(
|
|
41
160
|
domainConfig: DomainConfig,
|
|
42
161
|
auth0Client?: Auth0Client,
|
|
@@ -57,9 +176,14 @@ export default async function getToken(
|
|
|
57
176
|
);
|
|
58
177
|
return token;
|
|
59
178
|
} else if (domainConfig.connectionMethod === "login" && auth0Client) {
|
|
60
|
-
//
|
|
179
|
+
// Get a regular token WITHOUT organization scope
|
|
180
|
+
// This is used for tenant management endpoints which require non-org-scoped tokens
|
|
61
181
|
try {
|
|
62
|
-
const token = await auth0Client.getTokenSilently(
|
|
182
|
+
const token = await auth0Client.getTokenSilently({
|
|
183
|
+
authorizationParams: {
|
|
184
|
+
organization: undefined,
|
|
185
|
+
},
|
|
186
|
+
});
|
|
63
187
|
return token;
|
|
64
188
|
} catch (error) {
|
|
65
189
|
throw new Error(
|