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
20 changes: 18 additions & 2 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2195,6 +2195,7 @@ Access anonymous session operations through the `anonymous` property from `useAu
- [Getting an access token](#getting-an-anonymous-access-token)
- [Explicit session creation with metadata](#explicit-anonymous-session-creation-with-metadata)
- [Multiple audiences](#multiple-anonymous-audiences)
- [Linking to an authenticated user](#linking-to-an-authenticated-user)
- [Ending the session](#ending-the-anonymous-session)
- [Storage modes](#anonymous-session-storage-modes)

Expand Down Expand Up @@ -2267,9 +2268,24 @@ const { accessToken: tokenB } = await anonymous.getTokenSilently({
});
```

### Ending the anonymous session
### Linking to an authenticated user

When the user logs in, the SDK automatically mints a short-lived transfer ticket from the stored session token and passes it to `/authorize` as `anon_transfer_token`. This works for both `loginWithRedirect()` and `loginWithPopup()` — no extra configuration needed.

Auth0 delivers the anonymous identity to your Post-Login Action as `event.anonymous_session`:

> **Note:** If you want the anonymous identity to be available for linking during login, call `loginWithRedirect()` before `anonymous.logout()`. Auth0 reads the anonymous session cookie during the login flow. Clearing it first means the identity will not be available in Post-Login Actions.
```js
exports.onExecutePostLogin = async (event, api) => {
if (event.anonymous_session) {
api.idToken.setCustomClaim('https://anon/metadata', event.anonymous_session.metadata);
api.idToken.setCustomClaim('https://anon/user_id', event.anonymous_session.user_id);
}
};
```

The anonymous session is **not** automatically cleared after login. Call `anonymous.logout()` explicitly once you have finished using the session data.

### Ending the anonymous session

```jsx
const { anonymous } = useAuth0();
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 @@ -44,6 +44,7 @@ const anonymousGetTokenSilently = jest.fn(() => Promise.resolve({ accessToken: '
const anonymousLogout = jest.fn(() => Promise.resolve());
const anonymousHasSession = jest.fn(() => false);
const anonymousGetClaims = jest.fn(() => null);
const anonymousMintTransferToken = jest.fn(() => Promise.resolve('transfer-ticket-jwe'));

export const Auth0Client = jest.fn(() => {
return {
Expand Down Expand Up @@ -95,6 +96,7 @@ export const Auth0Client = jest.fn(() => {
logout: anonymousLogout,
hasSession: anonymousHasSession,
getClaims: anonymousGetClaims,
mintTransferToken: anonymousMintTransferToken,
},
};
});
Expand Down
72 changes: 69 additions & 3 deletions __tests__/anonymous.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Auth0Client, AnonymousSession } from '@auth0/auth0-spa-js';
import { Auth0Client, AnonymousSession, AnonymousSessionError } from '@auth0/auth0-spa-js';
import { act, renderHook, waitFor } from '@testing-library/react';
import useAuth0 from '../src/use-auth0';
import { createWrapper } from './helpers';
Expand Down Expand Up @@ -63,7 +63,7 @@ describe('Anonymous Session API', () => {
});

it('should rethrow errors from getTokenSilently', async () => {
clientMock.anonymous.getTokenSilently.mockRejectedValueOnce(new Error('session_expired'));
clientMock.anonymous.getTokenSilently.mockRejectedValueOnce(new Error('network_error'));

const wrapper = createWrapper();
const { result } = renderHook(() => useAuth0(), { wrapper });
Expand All @@ -74,7 +74,32 @@ describe('Anonymous Session API', () => {
act(async () => {
await result.current.anonymous.getTokenSilently({ audience: 'https://api.example.com' });
})
).rejects.toThrow('session_expired');
).rejects.toThrow('network_error');
});

it('should surface AnonymousSessionError when the session has expired', async () => {
const expiredError = new AnonymousSessionError(
'session_expired',
'The anonymous session has expired'
);
clientMock.anonymous.getTokenSilently.mockRejectedValueOnce(expiredError);

const wrapper = createWrapper();
const { result } = renderHook(() => useAuth0(), { wrapper });

await waitFor(() => expect(result.current.isLoading).toBe(false));

let caughtError: unknown;
await act(async () => {
try {
await result.current.anonymous.getTokenSilently({ audience: 'https://api.example.com' });
} catch (e) {
caughtError = e;
}
});

expect(caughtError).toBeInstanceOf(AnonymousSessionError);
expect((caughtError as AnonymousSessionError).code).toBe('session_expired');
});
});

Expand Down Expand Up @@ -185,4 +210,45 @@ describe('Anonymous Session API', () => {
expect(result.current.anonymous.getClaims()).toBeNull();
});
});

describe('anonymous.mintTransferToken', () => {
it('should be defined', async () => {
const wrapper = createWrapper();
const { result } = renderHook(() => useAuth0(), { wrapper });

await waitFor(() => {
expect(result.current.anonymous.mintTransferToken).toBeDefined();
});
});

it('should return a transfer ticket when a session exists', async () => {
const wrapper = createWrapper();
const { result } = renderHook(() => useAuth0(), { wrapper });

await waitFor(() => expect(result.current.isLoading).toBe(false));

let ticket: string | null | undefined;
await act(async () => {
ticket = await result.current.anonymous.mintTransferToken();
});

expect(ticket).toBe('transfer-ticket-jwe');
});

it('should return null when no session exists', async () => {
clientMock.anonymous.mintTransferToken.mockResolvedValueOnce(null);

const wrapper = createWrapper();
const { result } = renderHook(() => useAuth0(), { wrapper });

await waitFor(() => expect(result.current.isLoading).toBe(false));

let ticket: string | null | undefined;
await act(async () => {
ticket = await result.current.anonymous.mintTransferToken();
});

expect(ticket).toBeNull();
});
});
});
12 changes: 6 additions & 6 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.27.0"
"@auth0/auth0-spa-js": "^2.28.0"
}
}
1 change: 1 addition & 0 deletions src/auth0-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,7 @@ export const initialContext = {
logout: stub,
hasSession: stub,
getClaims: stub,
mintTransferToken: stub,
} as unknown as AnonymousSessionApiClient,
};

Expand Down
Loading