Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 129 additions & 1 deletion EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
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.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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
<Auth0Provider
domain="YOUR_AUTH0_DOMAIN"
clientId="YOUR_AUTH0_CLIENT_ID"
enterpriseConnect={true}
authorizationParams={{ redirect_uri: window.location.origin }}
>
<App />
</Auth0Provider>
```

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 (
<form onSubmit={handleSubmit}>
<input name="email" type="email" required />
<button type="submit">Continue</button>
</form>
);
}
```

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 <p>Loading...</p>;

return <div>{/* your app */}</div>;
}
```

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.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions __mocks__/@auth0/auth0-spa-js.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
21 changes: 21 additions & 0 deletions __tests__/auth-provider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
86 changes: 86 additions & 0 deletions __tests__/use-enterprise-connect.test.tsx
Original file line number Diff line number Diff line change
@@ -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',
},
});
});
});
33 changes: 14 additions & 19 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
5 changes: 5 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -76,6 +80,7 @@ export type {
RevokeRefreshTokenOptions,
FetcherConfig,
InteractiveErrorHandler,
IsFederatedDomainOptions,
// MFA Types
MfaApiClient,
Authenticator,
Expand Down
Loading
Loading