diff --git a/EXAMPLES.md b/EXAMPLES.md
index 462ba23b..20992283 100644
--- a/EXAMPLES.md
+++ b/EXAMPLES.md
@@ -23,6 +23,7 @@
- [MyAccount API](#myaccount-api)
- [Session Expiry from Upstream IdP (IPSIE)](#session-expiry-from-upstream-idp-ipsie)
- [Use Suspense for loading state (React 19+)](#use-suspense-for-loading-state-react-19)
+- [Enterprise Connect](#enterprise-connect)
## Use with a Class Component
@@ -1984,4 +1985,131 @@ If initialization fails and the user subsequently signs in by some other means
for example a `loginWithPopup` triggered from outside the boundary — the SDK
re-checks the session once. If that check succeeds, retrying your Error Boundary
renders the subtree normally; if it fails again, the boundary keeps showing the
-error.
\ No newline at end of file
+error.
+
+## Enterprise Connect
+
+> Enterprise Connect is an Early Access feature. Confirm the tenant-side
+> requirements with your Auth0 contact.
+
+Enterprise Connect layers enterprise SSO on top of your own auth server. The
+`useEnterpriseConnect` hook exposes `isFederatedDomain` (WebFinger domain
+discovery against your configured Auth0 domain) and `loginWithSSO` (a
+`loginWithRedirect` that sets `login_hint`).
+
+`isFederatedDomain` makes a browser-direct cross-origin WebFinger request to
+your Auth0 tenant domain. Configure `domain` to the tenant domain
+(`YOUR_TENANT.auth0.com` or a custom domain with the WebFinger route enabled),
+not a custom domain that omits it: requests to the wrong host will fail and
+`isFederatedDomain` will return `false`.
+
+Set `enterpriseConnect={true}` to put the SDK into this mode.
+
+Enterprise Connect issues no refresh token, so the access token expires at the
+configured token lifetime with no silent renewal. Plan to re-authenticate the
+user through the login flow when the token expires; `getAccessTokenSilently`
+will not refresh it.
+
+Treat Enterprise Connect as identity only: extract the ID token claims after
+login and issue your own application session or API tokens from them. Do not
+rely on the Auth0 access token for long-lived API authorization.
+
+```jsx
+
+
+
+```
+
+Login form: discover the domain, then route to SSO or your own login.
+
+```jsx
+import { useEnterpriseConnect } from '@auth0/auth0-react';
+
+export function LoginForm() {
+ const { isFederatedDomain, loginWithSSO } = useEnterpriseConnect();
+
+ const handleSubmit = async (event) => {
+ event.preventDefault();
+ const email = event.target.email.value;
+ const emailDomain = email.split('@')[1];
+
+ if (await isFederatedDomain(emailDomain)) {
+ await loginWithSSO(email, {
+ appState: { returnTo: window.location.pathname },
+ });
+ } else {
+ // Not a federated domain: hand off to your existing login flow
+ // (e.g. render your password form). Replace with your own routing.
+ }
+ };
+
+ return (
+
+ );
+}
+```
+
+After the SSO redirect, `Auth0Provider` completes the token exchange
+automatically. Read the settled auth state in your `App` component to validate
+the session and navigate.
+
+Validating the `org_id` claim is an **optional** application-level
+authorization step, not an SDK requirement. Add it only if your app restricts
+access to specific organizations. A federated user on a connection that
+predates `org_id` claims will not have one, so treat a missing `org_id` as "not
+org-scoped" rather than an automatic failure.
+
+```jsx
+import { useAuth0 } from '@auth0/auth0-react';
+import { useEffect } from 'react';
+
+// Optional: only if your app restricts access to specific organizations.
+const ALLOWED_ORGS = ['org_123'];
+
+export function App() {
+ const { isLoading, isAuthenticated, getIdTokenClaims, logout } = useAuth0();
+
+ useEffect(() => {
+ if (isLoading || !isAuthenticated) return;
+
+ (async () => {
+ const claims = await getIdTokenClaims();
+
+ // Optional org check. Remove this block if you do not gate on org.
+ if (claims?.org_id && !ALLOWED_ORGS.includes(claims.org_id)) {
+ // Federated logout ends the enterprise IdP session too, so the next
+ // login runs email discovery again instead of silently re-using SSO.
+ await logout({
+ logoutParams: { federated: true, returnTo: window.location.origin },
+ });
+ return;
+ }
+
+ // Session valid. Render your app or navigate to the post-login destination.
+ })();
+ }, [isLoading, isAuthenticated, getIdTokenClaims, logout]);
+
+ if (isLoading) return Loading...
;
+
+ return {/* your app */}
;
+}
+```
+
+Logout must be federated to end the enterprise IdP session:
+
+```jsx
+await logout({
+ logoutParams: { federated: true, returnTo: window.location.origin },
+});
+```
+
+The `returnTo` URL must be registered in the application's **Allowed Logout
+URLs** in the Auth0 Dashboard, or the logout redirect will be rejected.
\ No newline at end of file
diff --git a/README.md b/README.md
index 2b13abe9..09b3a7c5 100644
--- a/README.md
+++ b/README.md
@@ -153,6 +153,7 @@ Explore public API's available in auth0-react.
- [useAuth0Suspense](https://auth0.github.io/auth0-react/functions/useAuth0Suspense.html)
- [withAuth0](https://auth0.github.io/auth0-react/functions/withAuth0.html)
- [withAuthenticationRequired](https://auth0.github.io/auth0-react/functions/withAuthenticationRequired.html)
+- [useEnterpriseConnect](https://auth0.github.io/auth0-react/functions/useEnterpriseConnect.html)
## Feedback
diff --git a/__mocks__/@auth0/auth0-spa-js.tsx b/__mocks__/@auth0/auth0-spa-js.tsx
index 45f4174c..b1743577 100644
--- a/__mocks__/@auth0/auth0-spa-js.tsx
+++ b/__mocks__/@auth0/auth0-spa-js.tsx
@@ -103,3 +103,5 @@ export const PasskeyRegisterError = actual.PasskeyRegisterError;
export const PasskeyChallengeError = actual.PasskeyChallengeError;
export const PasskeyGetTokenError = actual.PasskeyGetTokenError;
export const MyAccountApiError = actual.MyAccountApiError;
+
+export const isFederatedDomain = jest.fn();
diff --git a/__tests__/auth-provider.test.tsx b/__tests__/auth-provider.test.tsx
index 8edc5ce7..d6775f63 100644
--- a/__tests__/auth-provider.test.tsx
+++ b/__tests__/auth-provider.test.tsx
@@ -89,6 +89,27 @@ describe('Auth0Provider', () => {
});
});
+ it('should forward the enterpriseConnect flag to Auth0Client', async () => {
+ const opts = {
+ clientId: 'foo',
+ domain: 'bar',
+ enterpriseConnect: true,
+ };
+ const wrapper = createWrapper(opts);
+ renderHook(() => useContext(Auth0Context), {
+ wrapper,
+ });
+ await waitFor(() => {
+ expect(Auth0Client).toHaveBeenCalledWith(
+ expect.objectContaining({
+ clientId: 'foo',
+ domain: 'bar',
+ enterpriseConnect: true,
+ })
+ );
+ });
+ });
+
it('should support redirectUri', async () => {
const warn = jest.spyOn(console, "warn").mockImplementation(() => undefined);
const opts = {
diff --git a/__tests__/use-enterprise-connect.test.tsx b/__tests__/use-enterprise-connect.test.tsx
new file mode 100644
index 00000000..33f9d834
--- /dev/null
+++ b/__tests__/use-enterprise-connect.test.tsx
@@ -0,0 +1,86 @@
+import { renderHook, waitFor } from '@testing-library/react';
+import {
+ isFederatedDomain as spaIsFederatedDomain,
+ Auth0Client,
+} from '@auth0/auth0-spa-js';
+import useEnterpriseConnect from '../src/use-enterprise-connect';
+import { createWrapper } from './helpers';
+
+jest.mock('@auth0/auth0-spa-js');
+
+const clientMock = jest.mocked(new Auth0Client({ clientId: '', domain: '' }));
+const federatedMock = jest.mocked(spaIsFederatedDomain);
+
+describe('useEnterpriseConnect', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ clientMock.getConfiguration.mockReturnValue({
+ domain: '__test_domain__',
+ clientId: '__test_client_id__',
+ });
+ });
+
+ it('calls isFederatedDomain with the configured domain and email domain', async () => {
+ federatedMock.mockResolvedValueOnce(true);
+ const wrapper = createWrapper();
+ const { result } = renderHook(() => useEnterpriseConnect(), { wrapper });
+
+ const federated = await result.current.isFederatedDomain('acme.com');
+
+ expect(federatedMock).toHaveBeenCalledWith(
+ '__test_domain__',
+ 'acme.com',
+ undefined
+ );
+ expect(federated).toBe(true);
+ });
+
+ it('forwards options to isFederatedDomain', async () => {
+ federatedMock.mockResolvedValueOnce(false);
+ const customFetch = jest.fn();
+ const wrapper = createWrapper();
+ const { result } = renderHook(() => useEnterpriseConnect(), { wrapper });
+
+ await result.current.isFederatedDomain('acme.com', { customFetch });
+
+ expect(federatedMock).toHaveBeenCalledWith('__test_domain__', 'acme.com', {
+ customFetch,
+ });
+ });
+
+ it('loginWithSSO calls loginWithRedirect with login_hint set from the email', async () => {
+ const wrapper = createWrapper();
+ const { result } = renderHook(() => useEnterpriseConnect(), { wrapper });
+ await waitFor(() =>
+ expect(clientMock.loginWithRedirect).not.toBeNull()
+ );
+
+ await result.current.loginWithSSO('jane@acme.com');
+
+ expect(clientMock.loginWithRedirect).toHaveBeenCalledWith({
+ authorizationParams: { login_hint: 'jane@acme.com' },
+ });
+ });
+
+ it('loginWithSSO preserves caller authorizationParams and other options', async () => {
+ const wrapper = createWrapper();
+ const { result } = renderHook(() => useEnterpriseConnect(), { wrapper });
+
+ await result.current.loginWithSSO('jane@acme.com', {
+ authorizationParams: {
+ connection: 'okta',
+ organization: 'org_123',
+ },
+ appState: { returnTo: '/dashboard' },
+ });
+
+ expect(clientMock.loginWithRedirect).toHaveBeenCalledWith({
+ appState: { returnTo: '/dashboard' },
+ authorizationParams: {
+ connection: 'okta',
+ organization: 'org_123',
+ login_hint: 'jane@acme.com',
+ },
+ });
+ });
+});
diff --git a/package-lock.json b/package-lock.json
index f0d16caf..c4e8d63f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,7 +9,7 @@
"version": "2.25.0",
"license": "MIT",
"dependencies": {
- "@auth0/auth0-spa-js": "^2.25.0"
+ "@auth0/auth0-spa-js": "^2.26.0"
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^15.0.1",
@@ -80,20 +80,18 @@
"license": "ISC"
},
"node_modules/@auth0/auth0-auth-js": {
- "version": "1.10.0",
- "integrity": "sha512-FDzc0lbaG1E++4sjx3zW7BB9gVK2ODLmuPStGxYPeclKwLMFKkmVyWR8bCyQVNXvTS1KCHeX0dC9HMTJ0WBCJg==",
- "license": "MIT",
+ "version": "1.15.0",
+ "integrity": "sha512-obn/Qhxx3eNs2wV6/lLsIDIcEiSJvBEEy15hppB5Me1j2No2toa7KqQcXHBCyGX4mBBdVnxZJBjEX6E4WanYwg==",
"dependencies": {
"jose": "^6.0.8",
"openid-client": "^6.8.0"
}
},
"node_modules/@auth0/auth0-spa-js": {
- "version": "2.25.0",
- "integrity": "sha512-ISqLDRQcxFfF678AgtYDCdXESHX9W8II339xj/F6ujIptjK/ZkcTwq4HJs3RG7F24xRq1Xx1Vsc2uSCpyCMcUA==",
- "license": "MIT",
+ "version": "2.26.0",
+ "integrity": "sha512-pv2Si9bJioQqTgk/doh0nIKgnpF1TEW/CvPmC1eYUgwPk/cUIwIzNSPShFBM4G2MnR+pbeD2HhFH6ChJAzIeqg==",
"dependencies": {
- "@auth0/auth0-auth-js": "^1.10.0",
+ "@auth0/auth0-auth-js": "^1.15.0",
"browser-tabs-lock": "^1.3.0",
"dpop": "^2.1.1",
"es-cookie": "~1.3.2"
@@ -7812,9 +7810,8 @@
}
},
"node_modules/jose": {
- "version": "6.2.3",
- "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==",
- "license": "MIT",
+ "version": "6.2.12",
+ "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==",
"funding": {
"url": "https://github.com/sponsors/panva"
}
@@ -9089,9 +9086,8 @@
"license": "MIT"
},
"node_modules/oauth4webapi": {
- "version": "3.8.6",
- "integrity": "sha512-iwemM91xz8nryHti2yTmg5fhyEMVOkOXwHNqbvcATjyajb5oQxCQzrNOA6uElRHuMhQQTKUyFKV9y/CNyg25BQ==",
- "license": "MIT",
+ "version": "3.8.8",
+ "integrity": "sha512-8N28E+a/oxfXWBgOMt+ZP/JUf/XR+IFbvkAEPP3gznXOMv9BpAAwiIj0TFNz3tGTPc0ZQ8zmWBNgN1nAys0gng==",
"funding": {
"url": "https://github.com/sponsors/panva"
}
@@ -9290,12 +9286,11 @@
"dev": true
},
"node_modules/openid-client": {
- "version": "6.8.4",
- "integrity": "sha512-QSw0BA08piujetEwfZsHoTrDpMEha7GDZDicQqVwX4u0ChCjefvjDB++TZ8BTg76UpwhzIQgdvvfgfl3HpCSAw==",
- "license": "MIT",
+ "version": "6.8.8",
+ "integrity": "sha512-ZsucJA5Ad04Uv7YN4ql+s4GXNmb9uAYQwyJTsJx7CH/MX3JZioLE2pVKi1SC+YrbSLA4Px3Gi30dMjgOZtb6pA==",
"dependencies": {
- "jose": "^6.2.2",
- "oauth4webapi": "^3.8.5"
+ "jose": "^6.2.12",
+ "oauth4webapi": "^3.8.8"
},
"funding": {
"url": "https://github.com/sponsors/panva"
diff --git a/package.json b/package.json
index 738e3e0d..fc22a05b 100644
--- a/package.json
+++ b/package.json
@@ -95,6 +95,6 @@
"react-dom": "^16.11.0 || ^17 || ^18 || ~19.0.1 || ~19.1.2 || ^19.2.1"
},
"dependencies": {
- "@auth0/auth0-spa-js": "^2.25.0"
+ "@auth0/auth0-spa-js": "^2.26.0"
}
}
diff --git a/src/index.tsx b/src/index.tsx
index 5c167feb..ab92f3ac 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -11,6 +11,10 @@ export {
default as useAuth0Suspense,
Auth0SuspenseContextInterface,
} from './use-auth0-suspense';
+export {
+ default as useEnterpriseConnect,
+ UseEnterpriseConnect,
+} from './use-enterprise-connect';
export { default as withAuth0, WithAuth0Props } from './with-auth0';
export {
default as withAuthenticationRequired,
@@ -76,6 +80,7 @@ export type {
RevokeRefreshTokenOptions,
FetcherConfig,
InteractiveErrorHandler,
+ IsFederatedDomainOptions,
// MFA Types
MfaApiClient,
Authenticator,
diff --git a/src/use-enterprise-connect.tsx b/src/use-enterprise-connect.tsx
new file mode 100644
index 00000000..4b0e2496
--- /dev/null
+++ b/src/use-enterprise-connect.tsx
@@ -0,0 +1,75 @@
+import { useCallback, useContext } from 'react';
+import {
+ isFederatedDomain as spaIsFederatedDomain,
+ IsFederatedDomainOptions,
+} from '@auth0/auth0-spa-js';
+import Auth0Context, {
+ Auth0ContextInterface,
+ RedirectLoginOptions,
+} from './auth0-context';
+
+/**
+ * The shape returned by the `useEnterpriseConnect` hook.
+ */
+export interface UseEnterpriseConnect {
+ /**
+ * Runs WebFinger domain discovery for the given email domain against the
+ * Auth0 domain configured on the `Auth0Provider`. Returns `true` only if
+ * the domain is managed by Auth0 for enterprise SSO. A routing hint, not a
+ * security control: it returns `false` on any failure.
+ */
+ isFederatedDomain: (
+ emailDomain: string,
+ options?: IsFederatedDomainOptions
+ ) => Promise;
+ /**
+ * Starts the enterprise SSO redirect, passing the email as `login_hint`
+ * so Home Realm Discovery can resolve the connection and organization. Any
+ * `authorizationParams` supplied by the caller are preserved.
+ */
+ loginWithSSO: (
+ email: string,
+ options?: RedirectLoginOptions
+ ) => Promise;
+}
+
+/**
+ * ```js
+ * const { isFederatedDomain, loginWithSSO } = useEnterpriseConnect();
+ * ```
+ *
+ * Convenience hook for the Enterprise Connect flow. `isFederatedDomain` reads
+ * the Auth0 domain from the `Auth0Provider` configuration, so callers pass
+ * only the email domain. `loginWithSSO` is sugar over `loginWithRedirect`
+ * that sets `login_hint` to the provided email.
+ */
+const useEnterpriseConnect = (
+ context = Auth0Context
+): UseEnterpriseConnect => {
+ const { getConfiguration, loginWithRedirect } = useContext(
+ context
+ ) as Auth0ContextInterface;
+
+ const isFederatedDomain = useCallback(
+ // domain is read at call time, not when the callback is memoized
+ (emailDomain: string, options?: IsFederatedDomainOptions) =>
+ spaIsFederatedDomain(getConfiguration().domain, emailDomain, options),
+ [getConfiguration]
+ );
+
+ const loginWithSSO = useCallback(
+ (email: string, options?: RedirectLoginOptions) =>
+ loginWithRedirect({
+ ...options,
+ authorizationParams: {
+ ...options?.authorizationParams,
+ login_hint: email,
+ },
+ }),
+ [loginWithRedirect]
+ );
+
+ return { isFederatedDomain, loginWithSSO };
+};
+
+export default useEnterpriseConnect;