@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.
@@ -1,9 +1,13 @@
1
1
  import { UpdateParams, withLifecycleCallbacks } from "react-admin";
2
- import { authorizedHttpClient } from "./authProvider";
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 domainConfig = domains.find((d) => d.url === auth0Domain);
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 === auth0Domain);
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 the auth0Provider with the API URL, tenant ID, and domain
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
- authorizedHttpClient,
100
+ organizationHttpClient,
92
101
  tenantId,
93
102
  auth0Domain,
94
103
  );
@@ -1,32 +1,93 @@
1
1
  import { DomainConfig } from "./domainUtils";
2
- import { Auth0Client } from "@auth0/auth0-spa-js";
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
- // If using OAuth login, get token from auth0Client
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(