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
75 changes: 75 additions & 0 deletions __tests__/client/spa-provider.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// @vitest-environment happy-dom
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import type { Mock } from 'vitest';
import { render, screen, act, cleanup } from '@testing-library/react';
import { Auth0Client } from '@auth0/auth0-spa-js';
import { Auth0Provider } from '../../src/client/Auth0Provider.js';
import { useAuth0 } from '../../src/client/use-auth0.js';

Expand Down Expand Up @@ -238,3 +240,76 @@ describe('Auth0Provider — SPA mode', () => {
expect(mockGetTokenSilently).toHaveBeenCalledTimes(1);
});
});

// ─── Session restoration ──────────────────────────────────────────────────────

describe('Auth0Provider — SPA mode — session restoration', () => {
it('calls getTokenSilently and retries getUser when getUser returns undefined on init', async () => {
mockGetUser
.mockResolvedValueOnce(undefined)
.mockResolvedValueOnce({ sub: 'auth0|restored', name: 'Restored' });

const { getCtx } = await renderSpaProvider();

expect(mockGetTokenSilently).toHaveBeenCalledTimes(1);
expect(getCtx().user).toEqual({ sub: 'auth0|restored', name: 'Restored' });
expect(getCtx().isAuthenticated).toBe(true);
});

it('sets user to null when getTokenSilently throws during restoration', async () => {
mockGetUser.mockResolvedValue(undefined);
mockGetTokenSilently.mockRejectedValue(new Error('login_required'));

const { getCtx } = await renderSpaProvider();

expect(mockGetTokenSilently).toHaveBeenCalledTimes(1);
expect(getCtx().user).toBeNull();
expect(getCtx().isAuthenticated).toBe(false);
});

it('does not call getTokenSilently when getUser returns a user on init', async () => {
await renderSpaProvider();
expect(mockGetTokenSilently).not.toHaveBeenCalled();
});
});

// ─── Auth0Client config ───────────────────────────────────────────────────────

describe('Auth0Provider — SPA mode — Auth0Client config', () => {
function getConstructorOpts() {
return (Auth0Client as unknown as Mock).mock.calls[0][0];
}

it('passes useRefreshTokens: true by default', async () => {
await renderSpaProvider();
expect(getConstructorOpts().useRefreshTokens).toBe(true);
});

it('passes useRefreshTokensFallback: false by default', async () => {
await renderSpaProvider();
expect(getConstructorOpts().useRefreshTokensFallback).toBe(false);
});

it('passes cacheLocation: memory by default', async () => {
await renderSpaProvider();
expect(getConstructorOpts().cacheLocation).toBe('memory');
});

it('respects VITE_AUTH0_USE_REFRESH_TOKENS=false', async () => {
vi.stubEnv('VITE_AUTH0_USE_REFRESH_TOKENS', 'false');
await renderSpaProvider();
expect(getConstructorOpts().useRefreshTokens).toBe(false);
});

it('respects VITE_AUTH0_USE_REFRESH_TOKENS_FALLBACK=true', async () => {
vi.stubEnv('VITE_AUTH0_USE_REFRESH_TOKENS_FALLBACK', 'true');
await renderSpaProvider();
expect(getConstructorOpts().useRefreshTokensFallback).toBe(true);
});

it('respects VITE_AUTH0_CACHE_LOCATION=localstorage', async () => {
vi.stubEnv('VITE_AUTH0_CACHE_LOCATION', 'localstorage');
await renderSpaProvider();
expect(getConstructorOpts().cacheLocation).toBe('localstorage');
});
});
17 changes: 16 additions & 1 deletion src/client/Auth0Provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@ function SpaAuth0Provider({ children }: Auth0ProviderProps) {
new Auth0Client({
domain: import.meta.env.VITE_AUTH0_DOMAIN as string,
clientId: import.meta.env.VITE_AUTH0_CLIENT_ID as string,
useRefreshTokens: import.meta.env.VITE_AUTH0_USE_REFRESH_TOKENS !== 'false',
useRefreshTokensFallback: import.meta.env.VITE_AUTH0_USE_REFRESH_TOKENS_FALLBACK === 'true',
cacheLocation: (import.meta.env.VITE_AUTH0_CACHE_LOCATION as 'memory' | 'localstorage') ?? 'memory',
authorizationParams: {
redirect_uri:
import.meta.env.VITE_AUTH0_REDIRECT_URI ??
Expand Down Expand Up @@ -164,7 +167,19 @@ function SpaAuth0Provider({ children }: Auth0ProviderProps) {
navigate(appState.returnTo, { replace: true });
}
}
const u = await client.getUser<Auth0User>();
let u = await client.getUser<Auth0User>();
if (!u) {
// On page refresh the in-memory cache is empty. getTokenSilently()
// uses the stored refresh token to silently re-authenticate and
// repopulate the cache. If useRefreshTokensFallback is enabled it
// will also fall back to Auth0's SSO session (silent iframe).
try {
await client.getTokenSilently();
Comment thread
Piyush-85 marked this conversation as resolved.
u = await client.getUser<Auth0User>();
} catch {
u = undefined;
}
}
setUser(u ?? null);
} catch {
setUser(null);
Expand Down
3 changes: 3 additions & 0 deletions src/vite-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ interface ImportMetaEnv {
readonly VITE_AUTH0_REDIRECT_URI?: string;
readonly VITE_AUTH0_AUDIENCE?: string;
readonly VITE_AUTH0_SCOPE?: string;
readonly VITE_AUTH0_USE_REFRESH_TOKENS?: string;
readonly VITE_AUTH0_USE_REFRESH_TOKENS_FALLBACK?: string;
readonly VITE_AUTH0_CACHE_LOCATION?: string;
}

interface ImportMeta {
Expand Down