Skip to content
Draft
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
86 changes: 85 additions & 1 deletion EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -1984,4 +1984,88 @@ 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`).

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 {
// your existing login flow
showPasswordForm(email);
}
};

return (
<form onSubmit={handleSubmit}>
<input name="email" type="email" required />
<button type="submit">Continue</button>
</form>
);
}
```

Callback route: complete the login, validate the organization, then read the
enriched claims.

```jsx
import { useAuth0 } from '@auth0/auth0-react';
import { useEffect, useRef } from 'react';

const ALLOWED_ORGS = ['org_123'];

export function Callback() {
const { handleRedirectCallback, getIdTokenClaims, logout } = useAuth0();
const handled = useRef(false);

useEffect(() => {
if (handled.current) return;
handled.current = true;

(async () => {
await handleRedirectCallback();
const claims = await getIdTokenClaims();

if (!claims?.org_id || !ALLOWED_ORGS.includes(claims.org_id)) {
await logout({ logoutParams: { returnTo: window.location.origin } });
return;
}

console.log('Logged in as', claims.email, 'in org', claims.org_id);
})();
}, [handleRedirectCallback, getIdTokenClaims, logout]);

return <p>Completing login...</p>;
}
```

Logout must be federated to end the enterprise IdP session:

```jsx
await logout({
logoutParams: { federated: true, returnTo: window.location.origin },
});
```
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();
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',
},
});
});
});
4 changes: 4 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
74 changes: 74 additions & 0 deletions src/use-enterprise-connect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
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<boolean>;
/**
* 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<void>;
}

/**
* ```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(
(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;
Loading